logue SDKを使ったNTS-1用カスタムOSC作成テスト(2)


以前、logue SDKでオシレータを作成する入門記事を書きましたが、今回はその続編になります。ごく簡単なFM音源を書いたので、これを例題にします。

この音源の音自体は、NTS-1にも最初から入っているVPMとほぼ同じです。

実は、前回作成したウエーブテーブルオシレータにFM音源を重ねてみたらどうかと思って試してみたのですが、あまり馴染みませんでした。といって、単体としては新しい音ではないので、新オシレータとしてリリースするものでもなく、参考用コードとしてこの記事で扱うことにしました。

音はこんな感じです。表示されている数字は、後で説明しますがモジュレータとキャリアの周波数比です。

FM synthesis sounds of different modulator frequencies

前回の記事は以下のリンクですが、そのときの主な話題は、関数OSC_CYCLE()forループの中で、phasew0ずつ増やしていき、音声出力バッファを埋めていく処理でした。

logue SDKを使ったNTS-1用カスタムOSC作成テスト
前回はlogue SDKを使ったOSCのビルドと転送を試してみましたが、今回はこのSDKの基本的な使い方を見てみます。 SDKには、OSCのサンプルとしてbipolar, maxsize, sin, squareという4つのOSC...

今回の記事では、ノブを操作して入力する「Shape」「Alt(NTS-1以外の機種ではShift-Shape)」の2つのパラメータの扱い、およびLFO(NTS-1ではOSCボタンを押しながらノブA/Bで設定)の使い方をメインに書いていきます。

なおコードは100行以上ありますが、この記事の末尾につけておきます。

FM音源の原理

本題に入る前に、今回作成したFM音源について簡単に触れておきます。一般にFM音源と呼ばれていますが、FMラジオなどのいわゆる「FM変調」とは別の原理で動作しており、ヤマハは「位相変調」と呼んでいるそうです。NTS-1やPrologue/minilogue xdに入っている「VPM」も、Variable Phase Modulation(可変位相変調)の略称ですね。

FM音源の原理の解説はWikipediaの記事のこの図が分かりやすいと思います。

緑が出力で、複雑な波形になっていますが、もし青の波形がサイン波でなく常に一定であれば、赤の波形が緑へコピーされるだけです。青の波形の役割は、緑の波形が赤の波形をコピーする際に、赤の波形の上を「進んだり戻ったり」させることです。実装上は、赤のサイン波(キャリア)をウエーブテーブルから読みだすときに、その読み出し位置を青のサイン波(モジュレータ)で変調しているわけです。

全体構成

今回作成したオシレータでは、Shapeパラメータでモジュレータの強度(上の図の青のサイン波の振幅の大きさ)を、Altパラメータでモジュレータの周波数を変更できるようにします。また、LFOはモジュレータの強度に対してかけます。

ブロック図を描くと以下のようになります。図中のmod intをShapeパラメータ、mod freq.をAlt(Shift-shape)パラメータで与えます。

mod freq.の値で、でキャリアの周波数(鍵盤で押された音の周波数)からモジュレータの周波数を生成する際の倍数を制御しています。この周波数の比は単純な整数比にしないと波形が不安定になります。NTS1のVPMでは、モジュレータはキャリアの整数倍になっているっぽいです。VPMと全く同じにするのもつまらないので、今回は2/3とか4/3とか3/2、5/3といった、整数倍ではない値も入れています。

ちなみに、NTS-1の内蔵VPMは大体これと同じ構成のように思われます。prologue/minilogue xdのVPMはもう少し複雑で、モジュレータの出力をモジュレータのPhaseにフィードバックしたり、キャリアのphase入力にノイズを付加したり、キャリアの波形を選択したりできるようになっています。

また、有名なヤマハのFM音源は、モジュレータとキャリアのペアを2つ重ねたり、2つのモジュレータを加算した結果を用いたり、逆にキャリアを2つ用意して同じモジュレータで変調したり、モジュレータの前にさらにモジュレータを置いたり、といった構成(アルゴリズムと呼ばれています)を取ることができるようになっています。

パラメータの処理

logue SDKで使えるパラメータは以下のような2種類があります。

・ノブでリアルタイムに入力するタイプ・・・ShapeとAlt(Shift-Shape)
・値をあらかじめ数値で入力するタイプ・・・パラメータ1~パラメータ6

前者と後者は以下のような違いがあります。

・解像度: リアルタイム入力するタイプは1024段階(10bit)。数値で入力するタイプは最大200段階(-100~100)。
・使用方法: 数値入力するタイプは、manifest.jsonファイルで名称や値域等の設定が必要。リアルタイム入力するタイプは共通パラメータなので設定は必要ない。

今回は、リアルタイムに入力するタイプだけ使用します。
Shapeパラメータは前述のとおり、上の図の青い波形の振幅を0.0~1.0で指定するのに使います。Altパラメータはキャリアの周波数とモジュレータの周波数の比を選択するために使います。選択された値にキャリアの周波数を掛けたものがモジュレータの周波数になります。

パラメータが変更されると、OSC_PARAM(index, value)が呼び出されます。indexが対象パラメータ、valueが変更後の値です。valueは正の整数です。

この処理はソースコードでは90行目からになります。
Shapeパラメータは0.0~1.0の値が欲しいので、param_to_f32()で浮動小数点数に変換した値を使っています。値はs_state.mod_intに保存されます。

Altパラメータは、10bitの値を6bitシフトして4bitの値(0~15)を得、それをインデックスとして配列modfreq[]の値を一つ取り出します。modfreq[]は25行目で定義しています。値はs_state.mod_valに保存されます。

なお、リアルタイム入力するタイプのパラメータは当然演奏中に変化することを想定しなければなりませんが、オシレータの種類によっては、パラメータが急激に変化すると出力音にノイズが乗る場合もあります。(実際、このオシレータもそうです。)

そのような場合は、指定されたパラメータをすぐ使用するのではなく、現在の値から一定値以下の差分を加減算することによって、だんだん指定の値に近づけていくような処理が必要になります。今回はこの処理は省略しています。

LFOの処理

LFOはパラメータを自動で揺らしてくれるので便利ですが、LFOの値をパラメータに反映する具体的な処理はオシレータの開発者が書く必要があります。NTS-1ではLFOはピッチとShapeのどちらかにかけられるようになっています。minilogue xdやprologueでは一つのLFOをピッチ、Shape、フィルタのカットオフのどれかにかけることができます。しかしその処理を書くのは開発者なので、実際にはLFOをShape以外のパラメータにかけることもできます。

なお、LFOの波形はNTS-1では値域0.0~1.0の三角波のみ、minilogue xdやprologueでは値域が-1.0~1.0、波形は鋸波、三角波、矩形波のいずれかから選択できます。機種で値域が異なっているのは困りもので、NTS-1では他の機種とは動作が異なります。

LFOの値はOSC_CYCLE()のパラメータとして渡されますので、バッファへ音声データを書き込むときに都度パラメータの値をサンプルデータに反映します。

ただ、バッファは最大64サンプル分を一回のOSC_CYCLE()コールで埋める必要がありますが、渡されるLFOの値は1つだけです。そのため、前回のOSC_CYCLE()で渡された値と新しい値との差分をバッファ長で割って、LFOの値がスムーズに変化するようにするのが望ましいです。このあたりの処理はソースコードの72~73行目にあります。新しいLFOの値がlfo、前回の値がs_state.lfozです。

バッファを埋める際には、1サンプル出力するごとにlfolfo_incを加算していきます。このループは47行目からです。LFOのパラメータへの反映のさせ方ですが、今回はShapeパラメータ(0.0~1.0)の値に応じて

実際のLFOの値=(1.0 – Shape)*LFOの値

としています。(コードの48行目)

シンセサイザーのLFOデプスの設定でLFOの振幅を設定できますので、実際のLFOの値は上記にさらにシンセサイザー側の設定値を掛け算したものになります。シンセサイザー側のLFOデプスの値を知る方法は無いようです。

LFOデプスが最大値のときの動作イメージとしては下図のようになります。緑はNTS-1のLFOの最終的な値、紫はNTS-1以外の機種の値です。横軸は時間軸です。

Shapeパラメータ(青の線)が0.0から1.0まで大きくなっていくとき、LFOの振幅は逆に小さくなっていきます。ただしNTS-1とその他の機種ではLFOの元の値域が違うため、振幅の大きさはこれらの機種間で異なります。

その他

コードの48行目で、NTS-1以外ではlfoの最低値が-1.0となるため、変数coeffの値の最低値もmod_intが0のとき-1.0となります。そこで49行目でmodの値が必ず0以上になるように、1.0を加算しています。50行目のサイン波を求める関数osc_sinf()は引数が負だとうまく動かないようなので、こうしています。

コードの55行目で、phaseが12.0を超えたら0に戻るようにしています。通常はphaseの値は最大1.0なのですが、これだと、例えばモジュレータの周波数がキャリアの1/4とした場合にモジュレータの波形のphaseが0.25止まりになってしまいます。この場合、モジュレータのphaseを1.0にするにはキャリアのphaseは4.0になる必要があります。
モジュレータとキャリアの周波数の比率はいろいろな値を取りえますが、今回選んだ比率は分母側に2, 3, 4が出現しますので、4と3の最小公倍数である12をphaseの最大値としています。

最後に、以下がオシレータのソースコードです。

コメント