KORG NTS-1 のプリセット波形で logue SDK カスタム・オシレータの基本をおさらい

# はじめに
KORG NTS-1 上の logue SDK では 6カテゴリ90波形 (*1) のプリセット波形を利用することができます。このプリセット波形を鳴らすだけのシンプルなオシレータを作りながら、logue SDK でカスタム・オシレータを作る際の基本をおさらいします。

(*1 : DSP のライブラリに含まれる sine, square, triangle, saw などはカウントせず）

## tl;dr; ポイントまとめ
カスタム・オシレータを作る際の基本となる、このポスト内の重要ポイントをまとめます。

 - サンプリング周波数は 48kHz
 - カスタム・オシレータで生成する音の高さは `params->pitch` で指定される
 - logue SDK 内での位相は [0.0, 1.0) で表現されている
    - カスタム・オシレータもそれに倣うのが楽
 - カスタム・オシレータ内では音の周波数ではなくてフレームあたりの位相変化量で音高を管理するのが良い
   - `osc_w0f_for_note` 関数でフレームあたりの位相変化量を取得できる
 - バッファに書き込む値は (-1.0, 1.0) とすること
    - `osc_softclipf` 関数で両端を切り落とせる

## 動作環境

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

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

# できあがりのカスタム・オシレータ
最終的にはこのようなオシレータができあがりました。ここを目指して組み立ていく中で、カスタム・オシレータを作る際のポイントなどの基本を説明していきます。
[preset_waves.cpp](https://github.com/miz884/logue-sdk-custom-modules/blob/master/preset_waves/preset_waves.cpp)

```cpp
#include "userosc.h"

typedef struct State {
  float phase;
  uint16_t flags;

  const float * const * wavetable;
  uint16_t wavetable_index;
} 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;

  state.wavetable = wavesA;
  state.wavetable_index = 0;
}

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;
  const float * const * wavetable = state.wavetable;
  const uint16_t wavetable_index = state.wavetable_index;

  // 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) {
    // Main signal
    float sig  = osc_wave_scanf(wavetable[wavetable_index], phase);
    sig  = osc_softclipf(0.05f, 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;
}

void OSC_PARAM(uint16_t index, uint16_t value) {
  switch (index) {
  case k_user_osc_param_id1:
    state.wavetable = wavesA;
    state.wavetable_index = value;
    break;
  case k_user_osc_param_id2:
    state.wavetable = wavesB;
    state.wavetable_index = value;
    break;
  case k_user_osc_param_id3:
    state.wavetable = wavesC;
    state.wavetable_index = value;
    break;
  case k_user_osc_param_id4:
    state.wavetable = wavesD;
    state.wavetable_index = value;
    break;
  case k_user_osc_param_id5:
    state.wavetable = wavesE;
    state.wavetable_index = value;
    break;
  case k_user_osc_param_id6:
    state.wavetable = wavesF;
    state.wavetable_index = value;
    break;
  case k_user_osc_param_shape:
  case k_user_osc_param_shiftshape:
  default:
    break;
  }
}
```


# カスタム・オシレータの処理の流れ
まずはカスタム・オシレータがどのようなフローで処理を行うのか、おおまかな流れを整理します。


1. `OSC_INIT` でカスタム・オシレータの初期化を行う
**カスタム・オシレータの初期化**は `OSC_INIT` 関数内で行います。これは OSC モードのときに TYPE ノブでカスタム・オシレータが選択された際に呼ばれます。ここではオシレータ全体で最初に必要な初期化を行います。発音ごとに必要となる処理はここには書けません。

2. 発音の開始ごとに `OSC_NOTEON` がトリガーされる 
発音の要求が来るたびに呼ばれます。発音ごとの初期化などはここに書けば良さそうなものですが、それだとあまりよろしくないのか、ここでは **NOTEON が来たというフラグを立てる**だけで、実際の初期化処理は次回の `OSC_CYCLE` 内で行われるのが定石のようです。

3. `OSC_CYCLE` で波形データを生成する 
**波形データを生成する**ための関数で、オシレータのいちばん重要な部分となるので後ほど詳細に説明します。音が継続する限り繰り返し呼ばれます。前述の通り、NOTEON 直後の呼び出しでは初期化処理を行う必要がある場合もあります。発音ごとの初期化処理の具体例は、音の波形をスタート地点まで戻す、などです。

4. 発音の終了は `OSC_NOTEOFF`
発音の後片付け等が必要であればここで行います。以降は `OSC_CYCLE` が呼ばれなくなり (*2)、音が止まるため、`OSC_NOTEOFF`内で明示的に音を止める処理などは不要です。今回のカスタム・オシレータでは何も行っていません。

(*2 : EG の設定によっては Release の間は呼ばれ続ける場合もあります）


# `OSC_CYCLE` の詳細
`OSC_CYCLE` は波形データを生成する関数で、カスタム・オシレータの心臓部分となる重要な部分です。[userosc.h](https://github.com/korginc/logue-sdk/blob/master/platform/nutekt-digital/inc/userosc.h) で次のように定義されています。

```cpp
#define OSC_CYCLE   __attribute__((used)) _hook_cycle
...
void _hook_cycle(const user_osc_param_t * const params, int32_t *yn, const uint32_t frames);
```

## `OSC_CYCLE` のパラメータ

### 入力 : `const user_osc_param_t * params`
`user_osc_param_t` はカスタム・オシレータので生成すべき音の基本性質を指定する構造体で、[userosc.h](https://github.com/korginc/logue-sdk/blob/master/platform/nutekt-digital/inc/userosc.h) で定義されています。このメンバの中でも重要な情報の一つは**鳴らすべき音の高さ**を示す `params->pitch` で、その音高に対応した周波数の音声データを生成するのがカスタム・オシレータの役割です。

`params->pitch` の上位バイトが MIDI ノート・ナンバーを示し、下位バイトは半音未満の音高の微調整を指定します。この情報を使うと生成すべき波形データの周波数を算出することができます。ただし、後ほど周波数と位相のところでも詳しく述べますが、実際のカスタム・オシレータの実装では音の周波数を直接扱う場面はあまりなさそうです。

### 出力用バッファ： `int32_t *yn` と `const uint32_t frames`
`yn` は音声データを収めるためのバッファで、カスタム・オシレータの内部で**生成した音声データをこのバッファに書き込む**ことで音を出力します。`frames` はバッファの長さを示しています。ここで扱う数値はサンプルごとの音声信号の大きさであり、ノート・ナンバーや周波数などを直接書き出すわけではありません。サンプリング周波数は 48kHz です。この１秒あたり 48k フレームのうちの `frames` 分の音声データを一度の `OSC_CYCLE` で生成することになります。

[ドキュメント](https://korginc.github.io/logue-sdk/ref/minilogue-xd/v1.1-0/html/group__osc__inst.html#ga5fdcf65ee3fb8562620dc05cb4377b2f)では *must support at least up to 64 frames* となっていますが、KORG NTS-1 の挙動を少し確認したところ 64 フレーム以外の長さが渡されてくるケースは確認できませんでした。

## `OSC_CYCLE` の呼び出しサイクルと内部状態の保持

前述の通り、一度の `OSC_CYLE` の呼び出しで 64 フレーム（もしくはそれ以上）の音声データを生成します。このフレーム数の境界は一周期の波形データの境界とは必ずも一致しないため、一周期の波形データを複数回の `OSC_CYCLE` に分けて生成できるような仕組みが必要となります。そのために、ある呼び出しでは波形をどこまで生成し終えて、次の呼び出しでは波形のどこから生成を再開するかを保持する必要があります。こうした１波形内における現在位置が**位相**です。

サンプルコードでは位相を含めた内部状態を以下の構造体で管理しています。

```cpp
typedef struct State {
  float phase;
  uint16_t flags;

  const float * const * wavetable;
  uint16_t wavetable_index;
} State;
```

ある `OSC_CYCLE` コールで生成し終えた位相を `phase` に保持し、次回のコールではその `phase` から生成を開始します。それ以外のメンバについては追って説明します。

## logues SDK における位相の管理

logue SDK の内部では位相を **[0.0, 1.0)** で管理することを前提に各種の関数が用意されています。正弦波など一般的には一周期を 0 ≦ θ ≦ 2π として扱う関数にも [0.0, 1.0) を渡すので多少混乱しましたが、直感的には１波形の何%まで進んだかを示していると考えるとわかりやすいです。例えば位相が 0.5 のときは全体の 50% まで来ていると考えることができます。これが 1.0 にたどり着いたときに１波形の生成が終了し、同時に位相は 0.0 に戻ることになります。次の例では正弦波を扱っていますが、後述のプリセット波形を取得する関数も同様に [0.0, 1.0) を引数に取ります。

```cpp
# 例：
# 正弦波１波形の 25% の時点の値を返す。つまり sin(M_PI/2) = 1.0
osc_sinf(0.25);
# 同様に 50%。sin(M_PI) = 0.0
osc_sinf(0.5);
# 100% は 0.0 に周期が戻り sin(0) = 0.0
osc_sinf(0.0);
```

ただしこの [0.0, 1.0) は標準で用意されたライブラリを活用する場合の制約であり、自前の関数で波形成性を行う場合はこの限りではありません。例えば一周期を [-1.0, 1.0) としたり [-M_PI, M_PI) としたりすることもできます。ただし、後述のフレームあたりの位相変化量の扱いでややこしくなるため、内部では位相を [0.0, 1.0) で扱いつつ、自前の関数への入力は定数倍するなどして定義域を変換する方法が簡単だと思います。

```cpp
# 例：my_sin の定義域 [-M_PI, M_PI) とする場合
my_sin((phase * 2.0f - 1.0f) * M_PI);
```

## 音の周波数と位相の進め方の関係
たとえば 400Hz の音の場合、一秒間に 400 個の波形を出力することになります。カスタム・オシレータのサンプリング周波数が 48kHz なので、１波形あたりに使えるフレーム数は 48,000(フレーム/s) / 400(波形/s) = 120(フレーム/波形) となります。１波形 = 120 フレームの間に位相が 0.0 から 1.0 まで変化するので、１フレームあたりでは位相を 1 / 120 ずつ進めることになります。同様の計算を行うことで、音の周波数に応じて１フレームあたりに変化させる位相の量を求められます。

カスタム・オシレータの実装では、音の周波数そのものではなくて、この１フレームあたりの位相の変化量から生成するデータの位相を制御します。出力する音の高さは `params->pitch` として渡ってきているので、これを使って１フレームあたりの位相変化量を求める関数が用意されています。

```cpp
const float w_delta = osc_w0f_for_note((params->pitch)>>8, params->pitch & 0xFF);
```

この関数の戻り値を使って、１フレームの処理が終わるたびに `w_delta` だけ位相を進めます。１波形分を生成し終わると位相が 1.0 となるので、-1.0 することで位相を最初に戻します。これを行っているのが以下の部分です。

```cpp
    phase += w_delta;
    phase -= (uint32_t) phase; // to keep phase within 0.0-1.0.
```

### 余談
`osc_w0f_for_note` に渡している第二引数は、半音未満の音程の微調整に使われます。半音を 256 段階に区切り、与えられた値の分だけ音高を上げる、という処理を行います。ただし注意する点は、256段階の分割は**周波数の線形補完である**ことです。本来はセントなどのような対数上での補完を行うものなんじゃないかと思っていたのですが、計算の簡単のためなのかシンプルなアプローチが取られています。このため、例えば 128 を指定したとしても、それは第一引数で指定された音とその半音上の音の中間の高さにはなりません。

### 余談２
KORG NTS-1 は MIDI ノートナンバーに対応する周波数を Hz としてルックアップするテーブルを内部に持っています。`osc_w0f_for_note` 内ではそのルックアップテーブルから MIDI ノートナンバーに対応する周波数を得て、それとサンプリング周波数を用いて１フレームあたりの位相変化量を計算しています。カスタム・オシレータからは音の周波数は見えないようになっていますが、内部的に周波数を使っているんですね。

## バッファに書き出す波形データの値域と表現形式

波形データのバッファは `int32_t ` の配列として渡ってきます。ここに書き込むデータは値域 (-1.0, 1.0) の [Q31](https://en.wikipedia.org/wiki/Q_(number_format)) 形式が期待されているようです。明確にドキュメントされているものを見つけられていませんが、サンプルコードを読む限りはそうなっています。logue SDK ではこの値域に合わせて両端を切り落とす関数が用意されています。

```cpp
 __fast_inline float osc_softclipf(const float c, float x)
```

この関数は任意の`float` 値をとって [-(1.0 - c), (1.0 - c)] の `float` 値を返します。例えば次のような場合、値域は [-0.95, 0.95] となります。

```cpp
sig  = osc_softclipf(0.05f, sig);
```

こうして得られた (-1.0, 1.0) の `float` 値を Q31 形式に変換してバッファに書き込みます。

```cpp
*(y++) = f32_to_q31(sig);
```

ここで副作用としてバッファを一つ進めていることに留意します。

# ここまでのコード

ここまでの議論で、カスタム・オシレータの基本的な枠組みは出来上がります。コードに落とすと次のようになります。

```cpp
void OSC_CYCLE(const user_osc_param_t* params,
               int32_t* yn,
               const uint32_t frames) {  
  const float w_delta = osc_w0f_for_note((params->pitch)>>8, params->pitch & 0xFF);
  q31_t* __restrict y = (q31_t*) yn;
  const q31_t* y_e = y + frames;

  while (y != y_e) {
    float sig  = your_wave(phase); // <-- ここで位相から波形データを生成する関数を呼ぶ
    *(y++) = f32_to_q31(osc_softclipf(0.05f, sig));

    state.phase += w_delta;
    state.phase -= (uint32_t) state.phase;
  }
}
```

これに加えて、実際の波形データ生成を担う `float sig  = your_wave(phase)` の部分を実装するとカスタム・オシレータの完成です。

このポストでは `your_wave(phase)` にプリセット波形を用いることになります。次の節からはその方法を説明します。

# プリセット波形
プリセット波形は `wavesA` から `wavesF` の6カテゴリ、合計90波形が用意されています。カテゴリごとの波形数は次の通りです。

|カテゴリ|波形数|
|---|---|
|wavesA | 16 |
|wavesB | 16 |
|wavesC | 14 |
|wavesD | 13 |
|wavesE | 15 |
|wavesF | 16 |

## プリセット波形の形状
それぞれの波形は次のチャートのようになっています。１波形分を切り出したものをカテゴリごとにつないでまとめています。

![wavesA.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118471/90873c49-dd6e-9e46-8d91-b89c61b0b5b0.png)
![wavesB.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118471/db6edee6-88d5-71ca-2b87-0d4793c8676e.png)
![wavesC.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118471/85018609-5f2b-e2c8-669a-8bb816ac8292.png)
![wavesD.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118471/3cd33273-cbb8-bb92-bc17-9733d57d1f2d.png)
![wavesE.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118471/c6e131a1-639f-3623-79a2-ffcf31562df6.png)
![wavesF.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118471/bde23cb9-550a-fbaa-11a9-9d992dcb7fea.png)

## プリセット波形の読み出し
logue SDK ではプリセット波形にアクセスする関数 `osc_wave_scanf` が用意されています。この関数を呼ぶには次の３つが必要です。

 - 波形のカテゴリ（wavesA - wavesF)
 - カテゴリごとの波形番号 (0-15、ただしカテゴリによって最大値は異なる）
 - 位相

波形のカテゴリは、その名前がそのまま波形へのポインタの配列を指し示す変数として定義されていて、例えば `wavesA[0]`, `wavesF[12]` といった形で波形を参照することができます。

```cpp
#define k_waves_a_cnt       16
  
  extern const float * const wavesA[k_waves_a_cnt];
```

位相はこれまで同様に [0.0, 1.0) で指定します。例えば wavesA の二番目の波形には以下のようにアクセスします。

```cpp
    float sig  = osc_wave_scanf(wavesA[1], phase);
```

この値をバッファに次々と書き込むことで、プリセット波形を音として鳴らすことができます。前述の通り、波形データは `osc_softclipf ` 関数を使って両端を切って Q31 形式に変換するので、このようなコードになります。

```cpp
    float sig  = osc_wave_scanf(state.wavetable[state.wavetable_index], phase);
    sig  = osc_softclipf(0.05f, sig);
    *(y++) = f32_to_q31(sig);
```

`state.wavetable[state.wavetable_index]` の部分は、後述のエディット・パラメータを使ってユーザーが変更可能になっています。エディット・パラメータを扱う関数と `OSC_CYCLE` とで共通の値を参照する必要があるので、内部状態を保持する構造体に値を格納しています。

### 余談
これらのプリセット波形は内部的にそれぞれ 128 個の float 値の配列として定義されていて、データポイントの間の値を要求された場合は線形補間が行われているようです。

# `OSC_NOTEON` と `OSC_NOTEOFF`

発音の開始と終了ではそれぞれ `OSC_NOTEON` と `OSC_NOTEOFF` が呼ばれます。MIDI のノートオンとノートオフに対応する関数です。

`OSC_NOTEON` では発音の最初に必要となる初期処理を行うことが期待されます。例えば位相を初期位置の `0.0` に戻す処理などが考えられます。

`OSC_NOTEOFF` では発音の後始末に必要な処理が期待されます。例えば不要となった内部バッファ領域の解放などが考えられます。

ただし、どちらの処理も `OSC_CYCLE` で音声データを生成している途中に内部状態が変更されると、音のつながりがおかしくなる恐れがあります。そのため `OSC_NOTEON` と `OSC_NOTEOFF` ではフラグを立てるにとどめて、実際の初期化処理、後始末は次の `OSC_CYCLE` で行うことが望ましいです。

今回のカスタム・オシレータでもフラグを `enum` として定義して、`OSC_NOTEON` 、 `OSC_NOTEOFF` ではフラグを操作するだけで、実際の処理は `OSC_CYCLE` に委ねています。

```cpp

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

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;
  }
  ...
}

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;
}
```

# エディット・パラメータ
カスタム・オシレータは、ユーザーが NTS-1 演奏時に B ノブを操作することで変更可能なエディット・パラメータを、最大で6つまで持つことができます。これらのパラメータの操作に基づいて `OSC_CYCLE` 内で波形の生成方法を変化させるなど、リアルタイムにカスタム・オシレータの挙動を変化させることが可能です。

使用するエディット・パラメータの個数、それぞれのエディット・パラメータの名前、最大値、最小値などは `manifest.json` ファイル内で定義します。

ユーザーによって設定値が変化した場合は `OSC_PARAM` がパラメータ番号とパラメータ値を伴って呼ばれます。この挙動の詳細は「[KORG NTS-1 で OSC_PARAM に渡ってくる値の実際](https://qiita.com/mizba/items/e99b22e31fc1b17ea6ed)」のポストを参照してください。


## 今回のカスタム・オシレータでのエディット・パラメータの扱い
今回のカスタム・オシレータでは6つのパラメータにそれぞれプリセット波形のカテゴリをアサインし、パラメータ値がそのままカテゴリ内の波形番号を指し示すようにしました。

|パラメータ |カテゴリ |最小値|最大値|
|---|---|---|---|
| k_user_osc_param_id1 |wavesA | 0 | 15 |
| k_user_osc_param_id2 |wavesB | 0 | 15 |
| k_user_osc_param_id3 |wavesC | 0 | 13 |
| k_user_osc_param_id4 |wavesD | 0 | 12 |
| k_user_osc_param_id5 |wavesE | 0 | 14 |
| k_user_osc_param_id6 |wavesF | 0 | 15 |


この場合、manifest.json は以下のように設定します。

```json
        "num_param" : 6,
        "params" : [
          ["A", "0", "15", ""],
          ["B", "0", "15", ""],
          ["C", "0", "13", ""],
          ["D", "0", "12", ""],
          ["E", "0", "14", ""],
          ["F", "0", "15", ""]
        ]
```

パラメータ値の変更を扱う `OSC_PARAM` は以下のように実装します。パラメータ番号に基づいてプリセット波形のカテゴリを切り替え、パラメータ値でカテゴリ内の波形番号を指定しています。カテゴリ、波形番号、ともに `OSC_PARAM` と `OSC_CYCLE` の双方から参照する必要があるため、内部状態を保持する構造体に格納しています。

```cpp

void OSC_PARAM(uint16_t index, uint16_t value) {
  switch (index) {
  case k_user_osc_param_id1:
    state.wavetable = wavesA;
    state.wavetable_index = value;
    break;
  case k_user_osc_param_id2:
    state.wavetable = wavesB;
    state.wavetable_index = value;
    break;
  case k_user_osc_param_id3:
    state.wavetable = wavesC;
    state.wavetable_index = value;
    break;
  case k_user_osc_param_id4:
    state.wavetable = wavesD;
    state.wavetable_index = value;
    break;
  case k_user_osc_param_id5:
    state.wavetable = wavesE;
    state.wavetable_index = value;
    break;
  case k_user_osc_param_id6:
    state.wavetable = wavesF;
    state.wavetable_index = value;
    break;
  case k_user_osc_param_shape:
  case k_user_osc_param_shiftshape:
  default:
    break;
  }
}
```

# ビルドとデプロイ
ここまででプリセット波形を鳴らすカスタム・オシレータの挙動は一通り出来上がりました。ソースファイルと一緒に公開されている Makefile などを正しく入手してあれば、以下の手順でカスタム・オシレータをビルドしてKORG NTS-1 実機にデプロイすることができます。

```sh
$ make
...
$  ./logue-cli load -s 1 -u path/to/binary/preset_waves.cpp.ntkdigunit
```

## 操作方法
 1. OSC モードで、TYPE ノブにより preset カスタム・オシレータを選択
 1. OSC + TYPE ノブでエディット・パラメータを選択。これによりプリセット波形のカテゴリが選択される。
 1. B ノブで カテゴリ内の波形番号を指定
 1. 鍵盤を触って音を鳴らしてみる
 1. 他の波形にも切り替えて試してみる

# まとめ
以上で、KORG NTS-1 の logue SDK 実装にデフォルトで用意されているプリセット波形を鳴らすカスタム・オシレータが完成しました。内部での位相の表現形式やバッファが期待する値域など、ドキュメントなどで明示されていなくて暗黙の前提になっているようなあたりを色々と調べることは良い勉強になりました。

再度、ポイントをまとめておきます。

 - サンプリング周波数は 48kHz
 - カスタム・オシレータで生成する音の高さは `params->pitch` で指定される
 - logue SDK 内での位相は [0.0, 1.0) で表現されている
    - カスタム・オシレータもそれに倣うのが楽
 - カスタム・オシレータ内では音の周波数ではなくてフレームあたりの位相変化量で音高を管理するのが良い
   - `osc_w0f_for_note` 関数でフレームあたりの位相変化量を取得できる
 - バッファに書き込む値は (-1.0, 1.0) とすること
    - `osc_softclipf` 関数で両端を切り落とせる

以上