KORG NTS-1 で OSC_PARAM に渡ってくる値の実際 :: shape_lfo 編

# はじめに

KORG NTS-1 上で動く logue SDK カスタム・オシレータは、SDK 側で予め用意された LFO を利用することができます。これは `OSC_CYCLE` 関数への引数 `const user_osc_param_t *params` のメンバ `shape_lfo` としてカスタム・オシレータに渡されます。

この文書では[公式のドキュメント](https://github.com/korginc/logue-sdk/tree/master/platform/nutekt-digital)に明記されていない範囲も含めて NTS-1 実機上のパラメータの挙動を説明します。ドキュメントされていない挙動に関しては今後変更される可能性がある点はご留意ください。

## 動作環境

このポスト内のコードは以下の環境で動作を確認しました。

```cpp
$ ./logue-cli probe
> Device: nutekt digital
> System version: 1.02
> Logue API version: 1.01-0
> Available modules:
...
```

# カスタム・オシレータ内での `params->shape_lfo` の扱い

`params->shape_lfo` は [userosc.h](https://github.com/korginc/logue-sdk/blob/master/platform/nutekt-digital/inc/userosc.h) で以下のように定義されています。

```cpp
  typedef struct user_osc_param {
    /** Value of LFO implicitely applied to shape parameter */
    int32_t  shape_lfo;
    /** Current pitch. high byte: note number, low byte: fine (0-255) */
    uint16_t pitch;
    /** Current cutoff value (0x0000-0x1fff) */
    uint16_t cutoff;
    /** Current resonance value (0x0000-0x1fff) */
    uint16_t resonance;
    uint16_t reserved0[3];
  } user_osc_param_t;
```

## `params->shape_lfo` の表現形式と値域

実際のところ、`params->shape_lfo` は [0.0, 1.0) の [Q31](https://en.wikipedia.org/wiki/Q_(number_format))  形式の値としてカスタム・パラメータに渡ってくるようです。`q31_to_f32` 関数を用いることで [0.0, 1.0) の `float` 形式に変換することができます。

```cpp
  float lfo = q31_to_f32(params->shape_lfo);
```

## `params->shape_lfo` の解像度

`params->shape_lfo` は `OSC_CYCLE` 関数の呼び出しごとに与えられ、関数実行中は変化しません。`OSC_CYCLE` は 64 フレームもしくはそれ以上を一度に処理するため、最低でも `64 / 48,000 = 0.00133...` 秒間は同じ値を参照することになります。この問題を解消するには、前回の `OSC_CYCLE` 関数の呼び出し時に与えられた `params->shape_lfo` を保持しておき、その値と今回の `params->shape_lfo` との間の値を 64 フレームに渡って線形補間する方法があります。ただしその場合でも、LFO の波形の変曲点などは取りこぼす可能性がある点は留意しましょう。

## `params->shape_lfo` の実際の波形

KORG NTS-1 上での `params->shape_lfo` の実際の値をプロットすると、波形は次のような三角波になっていました。振幅が 1.0、周期がおよそ 1 秒の場合の例です。

![chart (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118471/6296888f-7cee-b2d1-6071-0059ea483370.png)

横軸は時間で、このグラフは一秒間分の波形を示しています。ただし、グラフの開始点が位相0の点とは一致してない点に注意してください。

この LFO の振幅と周波数はユーザにより変更可能です。以下の節でその操作方法を説明します。

# KORG NTS-1 実機における LFO の操作

KORG NTS-1 実機上では OSC + A knob および OSC + B knob で `params->shape_lfo` の挙動を変更することができます。

| 操作 | 説明 | 最小値 | 最大値 | 
|---|---|---|---|
| OSC + A knob | LFO の周波数を設定します | 0.0 | 30.0 |
| OSC + B knob | LFO の振幅を設定します | 0 | 100 |

以下でそれぞれの詳細を説明します。

## LFO の周波数 (OSC + A knob)

OSC + A knob は LFO の周波数を変更します。設定できる値は [0.0, 30.0] です。
以下に設定値 1.0, 30.0, 0.5 の場合の波形を示します。それぞれ一秒間に相当する波形です。

設定値 1.0 の場合
![chart (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118471/6296888f-7cee-b2d1-6071-0059ea483370.png)

設定値 30.0 の場合
![chart (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118471/4d3ba4a7-e06b-aa29-4d4a-21ac97dd2813.png)

設定値 0.5 の場合
![chart 0.5 (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118471/97ded6bb-3b34-8ab4-0a1f-7980209f7a17.png)

これらより、一秒につきおおよそ設定値と同数の波形が生成されると考えて良さそうです。ただし、設定値 30.0 の波形をよく見ると分かるように、厳密に 30 波形とはなっていません。多少誤差があるようです。

また、解像度の節でも言及しましたが、設定値 30.0 の場合は波形の上端と下端がそれぞれ 1.0, 0.0 に必ずしも一致していないことに留意しましょう。

### 設定値 0.0 の場合

設定値 0.0 の場合は特殊で、LFO はその時の値を維持したまま変化しなくなります。

## LFO の振幅 (OSC + B knob)

OSC + A knob は LFO の振幅を変更します。設定できる値は [0, 100] です。
以下に設定値 100, 45 の場合の波形を示します。これらもそれぞれ一秒間に相当する波形です。

設定値 100 の場合
![chart (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118471/6296888f-7cee-b2d1-6071-0059ea483370.png)

設定値 45 の場合
![chart (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118471/937e700a-9ad9-4cef-0741-d3a20fabd398.png)

これらより、波形の値域が [0.0, 設定値 / 100] となると考えられます。中心値を挟んで対称的に波形が小さくなるのではなく、最小値 0.0 は変わらずに 最大値が設定値により変化する、という挙動です。

# サンプル・コード

`params->shape_lfo` を使って音量をコントロールして、トレモロのような挙動をするサンプル・コードです。値域を [0.0, 設定値] から [(1.0 - 設定値), 1.0] に変換して利用しています。

[shape_lfo.cpp](https://github.com/miz884/logue-sdk-custom-modules/blob/master/shape_lfo/shape_lfo.cpp)

```cpp
#include "userosc.h"

typedef struct State {
  float phase;
  uint16_t flags;
} State;

static State state;

enum {
  flags_clear = 0,
  flag_noteon = 1 << 0,
  flag_noteoff = 1 << 1,
};

void OSC_INIT(uint32_t platform, uint32_t api) {
  state.phase = 0.f;
  state.flags = flags_clear;
}

void OSC_CYCLE(const user_osc_param_t* params,
               int32_t* yn,
               const uint32_t frames) {  
  // Handle the reset flag on NOTEON.
  if (state.flags & flag_noteon) {
    state.flags &= ~(flag_noteon);
    state.phase = 0.f;
  }

  // Restore the last state.
  float phase = state.phase;

  // Calculate the phase delta.
  const float w_delta = osc_w0f_for_note((params->pitch)>>8, params->pitch & 0xFF);

  // Prepare the result buffer.
  q31_t* __restrict y = (q31_t*) yn;
  const q31_t* y_e = y + frames;

  while (y != y_e) {
    // LFO
    const float lfo = 1.f - q31_to_f32(params->shape_lfo);

    // Main signal
    float sig  = osc_sawf(phase);
    sig  = osc_softclipf(0.05f, lfo * sig); 
    *(y++) = f32_to_q31(sig);

    // Next step.
    phase += w_delta;
    phase -= (uint32_t) phase; // to keep phase within 0.0-1.0.
  }

  // Store the state.
  state.phase = phase;
}

void OSC_NOTEON(const user_osc_param_t * const params) {
  state.flags |= flag_noteon;
}

void OSC_NOTEOFF(const user_osc_param_t * const params) {
  state.flags |= flag_noteoff;
}
```

## 操作方法
 1. OSC モードで、TYPE ノブにより shape_lfo カスタム・オシレータを選択
 1. OSC + A ノブで LFO の周波数を指定。
 1. OSC + B ノブで LFO の振幅を指定。
 1. 鍵盤を触って音を鳴らしてみる
 1. OSC + A ノブ、OSC + B ノブ を動かしていろいろな設定値を試してみる

# まとめ

KORG NTS-1 上で動く logue SDK カスタム・オシレータに渡される `params->shape_lfo` の挙動を整理しました。繰り返しになりますが、ドキュメントされていない挙動に関しては今後変更される可能性がある点はご留意ください。