前回までは、Pure DataのパッチをhvccでCに変換してDrumlogue用のSynthユニットを作っていましたが、同じ方法でNTS-1 mkII用のオシレータユニットを作ってみました。
NTS-1 mkIIで動かす単なるSawオシレータですが、ポイントはこのオシレータはPure Dataのパッチから作っているということです。現在約29Kbなので、もう少し複雑なものが作れそう。 pic.twitter.com/GXNhKu3OhA
— boochowp (@boochowp) January 28, 2025
ベースとなっているのは以下の記事で使ったパッチです。

DrumlogueのSynthユニットとの違い
まず、DrumlogueとNTS-1 mkIIの主な違いをまとめておきます。
(1)DrumlogueのSynthユニットはそれ自体でシンセサイザとして完結している必要がありましたが、オシレータユニットは指定されたピッチの音を生成するだけです。
従って、オシレータユニットはノートON・ノートOFFの処理は必須ではありません。
ピッチの情報は、NTS-1 mkIIのオシレータユニットではAPIから取得できるので、Pure Data側でMIDIノート番号を周波数に変換する必要もありません。
(2)オシレータユニットの出力はモノラルです。
Pure Dataのdac~オブジェクトは、パラメータを1つ与えるとモノラルになります。
Pure Dataパッチの解説
以下が今回使うパッチです。周波数を受け取って、波形用のデータを生成し、それをDACに渡します。
パラメータを受信するオブジェクトが「r」となっていますが、これはreceiveの短縮形です。ちなみにsendの短縮形は「s」が使えます。
rの後に続くのは、pitchがパラメータをやり取りするメッセージキューの名前、@hv_paramはhvccにこのオブジェクトが外部からのメッセージ受信用であることを示すアノーテーション、以降3つの数字はそれぞれ最小値、最大値、デフォルト値です。
アノーテーション以降は省略可能で、これまでの記事では省略していました。アノーテーションを入れておくと、hvccが生成するCヘッダファイルの中に、オブジェクトのハッシュ値などが予め計算されて埋め込まれます。最小値、最大値、デフォルト値も関数hv_getParameterInfo()
で参照可能になります。
波形生成には、サイン波を生成するosc~の代わりにphasor~を使っています。phasor~はPure Dataでは簡易的に鋸波の生成によく使われています。NTS-1 mkIIの小さなスピーカーではサイン波は聴き取りにくいので、今回は波形を鋸波に変えました。
phasor~の本来の用途は、周波数に応じた位相(こちらの記事参照)の値を生成するもので、phasor=0は波形の先頭、phasor=1.0は波形の末尾を表します。値域は0~1.0なので、osc~の値域(-1.0~1.0)の半分しかなく、そのためphasor~を使うと音量がosc~の1/2になります。
logue SDKでの実装
パッチをhvccで変換して作られるものは、Hvコンテキストの実装です。今回のパッチからは以下のようなファイルが生成されました。
$ ls
HeavyContext.cpp HvHeavyInternal.h HvMessageQueue.c
HeavyContext.hpp HvLightPipe.c HvMessageQueue.h
HeavyContextInterface.hpp HvLightPipe.h HvSignalPhasor.c
Heavy_heavy.cpp HvMath.h HvSignalPhasor.h
Heavy_heavy.h HvMessage.c HvTable.c
Heavy_heavy.hpp HvMessage.h HvTable.h
HvHeavy.cpp HvMessagePool.c HvUtils.c
HvHeavy.h HvMessagePool.h HvUtils.h
Hvコンテキストを含めた全体の実装のイメージは下図のようになります。
前述のとおり、OSCユニットはランタイムコンテキストのAPIからピッチ情報を読み出して、それをHvコンテキストに渡すだけです。ただしランタイムコンテキストのピッチ情報はMIDIノート番号で表現されているので、それを周波数に変換する必要はあります。
DrumlogueとNTS-1 mkIIは、logue SDKのAPIはあまり変わりませんので、実装も表面的には大きく違いません。ただ、NTS-1 mkIIはOSCユニットで48KBというメモリ空間の制約がありますし、Drumlogueとはそもそもアーキテクチャが違っていますので、以下の工夫が追加で必要になります。
assertを無効にする
まず必要なのが、assertを無効にすることです。
hvccが生成するコードはassert文でパラメータをチェックするようになっていますが、これを残しておくとそれだけで48KBの制約を簡単にオーバーしてしまいます。
assertはコンパイラに-DNDEBUG
オプションを指定することで無効にできます。
printf()を排除する
以前も触れたことがありますが、様々な型を文字列に変換するsprintf()は便利ですがコードサイズも膨らみます。
hvccが生成するコードは、assertを無効にするとprintf()はほぼ不要になりますが、assert以外にもprintfを使っている部分がありました。
HvUtils.hの中で以下のようにhv_snprintf()が定義されています。
#define hv_snprintf(a, b, c, ...) snprintf(a, b, c, __VA_ARGS__)
hv_snprintf()は、HvMessage.cの中で関数msg_toString()
が使用しています。しかし、この関数は今回の実装では呼ばれることはありません。
snprintf()の実装抜きでOSCユニットをビルドしても、NTS-1 mkIIに転送する際にKontrol Editorが未解決シンボルのエラーになってしまいます。しかし、snprintf()の実装をリンクするとlogue SDKの48KB制限を超えてしまうので、代替方法を用意する必要があります。
対策はいくつかあります。
(1)hv_msg_toString()全体をコメントアウト(または削除)する
msg_toStringを呼び出しているのはここだけでしたので、この機能が必要無いのなら、これを削除するのが一番手っ取り早いです。
(2)hv_snprintf()をsnprintf()を使わない形に書き換える
この機能が必要な場合は、他のより簡易な実装に差し替えることが考えられます。候補としては、nanoprintfが利用できます。
(3)ダミーのsnprintf()を用意する
hv_snprintf()は、最終的にはsnprintf()に展開されますので、この関数をカラッポの関数として実装します。
(1)(2)はhvccが出力したコードを手作業で修正する必要があります。(1)の場合はHvHeavy.cppとHvHeavy.h、(2)の場合はHvUtils.hが修正対象です。
今回は、アドホックであまり安全ではないやり方ですが、(3)の方法を使いました。もちろん、もしhv_msg_toString()を使ったら、正しく動作しません。
libstdc++を追加でリンクする
hvccの実装では、HeavyContextInterface.hppの中で純粋仮想関数(pure virtual function)が使われています。純粋仮想関数を使う場合は、libstdc++のリンクにより、__cxa_pure_virtualが解決できるようになっている必要があります。これは純粋仮想関数がサブクラスで実装されずに直接呼ばれた場合のエラーハンドラとして働きます。
実装したコード
コードをビルドすると29KBほどになりました。サイズ上限まで20KB弱の余裕があるので、そこそこPure Dataで遊べる余地はありそうな気がします。
$ size ./sawosc.nts1mkiiunit
text data bss dec hex filename
28018 964 92 29074 7192 ./sawosc.nts1mkiiunit
意味があるのは初期化とProcess()のところだけですが、一応osc.hの全体を載せておきます。
もう少しいろいろサンプルを作ったらGitHubで公開するかもしれません。
コメント