Pure DataのPatchからNTS-3のlogue SDK generic FXユニットを作ってみた

hvccを使うとPure DataパッチをCのソースコードに変換することができ、かつhvccのexternal generatorを定義してやると、変換結果のCのコードをさらに特定のプラットフォーム向けに再変換することが可能です。前回まで、このhvccを使ったPure Data⇒logue SDK(v1)オシレータユニット用変換サーバを作ってきました。

今回からは、NTS-3用の変換サーバを作成していきます。この記事ではまず、hvccで生成したコードを手作業でNTS-3用に直して動かしてみます。今後の予定としては、次にこの手作業をexternal generatorを作って自動化し、最後にビルド作業全体をWebアプリ化していきます。

今回、NTS-3に取り組む理由です。

そもそもの大目標としては、logue SDK v2系のすべてのデバイスでPure Dataパッチを動かせるようにしていきたいと思っています。logue SDK v2系のデバイスとしては、現在drumlogue/NTS-1 mkII/NTS-3の3つがあります。このうちdrumlogueはハードウェアが桁違いにリッチな環境なので、Pure Dataのサブセットをサポートするhvccではなく、libpdを使ったフルセットのサポートを試したい気持ちもあります。

そのため、hvccを使うこれまでの流れの中では、次のターゲットはNTS-1 mkIIかNTS-3と考えていました。どちらから始めるのが良いか考えてみると、NTS-3はユニットの種類が1種類だけで、かつ利用できるメモリもlogue SDK v1のオシレータと同じ32KBであり、検討の範囲を限定できます。NTS-1 mkIIは、オシレータが48KB、Modが24KB、DelayとReverbが32KBと条件がまちまちなので、個別にサポートするのは手間がかかりそうです。

NTS-3をサポートできれば、NTS-1 mkIIのDelayとReverbもすぐサポートできそうですので、まずはNTS-3から手を付けてみることにしました。NTS-3はlogue SDKを使ったエフェクトがまだあまり出ていない(現状、3つsinevivesの製品くらい)ので、テコ入れしたい気持ちもあります。

hvccで生成したコードを手動でlogue SDK用に直すのは、NTS-1 mkIIで既に試していますので、今回はこれをベースにしていきます。実際のところPure Dataはあまり関係なくて、NTS-1 mkIIとNTS-3の違いをケアしていくのが主な作業になります。

併せて、以下の記事で書いたリンカ関連情報の削減について、内容的には重複しますが、改めて整理しておきます。

Pure DataのPatchからNTS-1 mkIIのOSCユニットを作ってみた(2)サイズの削減
先日、Pure DataのパッチをhvccでCに変換してNTS-1 mkIIで動かしてみましたが、単なる鋸波のオシレータに29Kbも使っていました。$ size ./sawosc.nts1mkiiunit text data bss dec...

hvccを利用する

今回使うPure Dataパッチは、以前使用したこちらのパッチです。

これを以下のようにhvccでCに変換します。この例だと

$ hvcc ./resampler.pd -o resampler -n resampler

みたいな感じです。これで./resampler/c/の中にソースコードが生成されます。

$ cd resampler/c
$ ls
HeavyContext.cpp           HvLightPipe.h     HvSignalPhasor.c
HeavyContext.hpp           HvMath.h          HvSignalPhasor.h
HeavyContextInterface.hpp  HvMessage.c       HvSignalSamphold.c
Heavy_resampler.cpp        HvMessage.h       HvSignalSamphold.h
Heavy_resampler.h          HvMessagePool.c   HvTable.c
Heavy_resampler.hpp        HvMessagePool.h   HvTable.h
HvHeavy.cpp                HvMessageQueue.c  HvUtils.c
HvHeavy.h                  HvMessageQueue.h  HvUtils.h
HvHeavyInternal.h          HvSignalDel1.c
HvLightPipe.c              HvSignalDel1.h

生成したコードの呼び出し方

hvccで生成されたコードのlogue SDKからの呼び出し方ですが、肝となる部分だけ書くとこんな感じになります。初期化したHvContextに、毎回入力用・出力用のバッファと、レンダリングするサンプル数を渡すようなイメージです。
※logue SDKで提供しているテンプレートでは、effect.h内で定義されるクラスのインスタンスをunit.ccから呼び出す構成になっていますが、以下の例ではunit.ccから直接HvContextを呼び出す形にしています。

#include "Heavy_resampler.h"

static HeavyContextInterface* hvContext;

__unit_callback int8_t unit_init(const unit_runtime_desc_t * desc) {
    hvContext = hv_resampler_new(desc->samplerate);
    return k_unit_err_none;
}

__unit_callback void unit_render(const float * in, float * out, uint32_t frames) {
    hv_processInlineInterleaved(hvContext, (float *) in, out, frames);
}

パラメータの受け渡し方

logue SDKからhvccコンテキストにパラメータを渡す場合は、

hv_sendFloatToReceiver(hvContext, HV_RESAMPLER_PARAM_IN_RATE, f);

というように受信側のPure Dataオブジェクトのハッシュ値と、渡したい値を引数として関数に渡します。

logue SDK v1.0系列やNTS-1 mkIIでは、Shapeノブ、Altノブ、ピッチ、LFOなど様々なパラメータを渡す必要がありましたが、NTS-3ではこれらの特定の目的に特化したパラメータが無いので、ある意味シンプルです。(渡せるパラメータが最大8個しかないのは若干不自由ですが)

一方で、NTS-3でオシレータを作る場合は、フィルタやエンベロープまで考慮することが必要になります。タッチパッドのオン・オフをイベントとして取得できますので、これをPure Data側へbangとして送ってPure Dataでエンベロープ等を作ることは可能です。

なお、NTS-3側のモジュール共通パラメータで、タッチパッドに触っていない場合に音を出すか出さないかを設定できますので、ピッチを変更できるだけの鳴りっぱなしのオシレータでも、これを使えば簡易的には音をオン・オフできます。

メモリ管理

hvccコンテキストは動作中に内部でmalloc()をコールします。logue SDK 1.0系列ではmalloc()は動作しませんでしたが、logue SDK 2.0系列では、malloc()によるメモリ確保が可能です。これは公式にはドキュメントに記載が無いので、たまたま使えるだけなのかもしれません。ですがたまたまであっても、使えるのは有難いことです。

一方で、NTS-1 mkII/NTS-3では、SDRAM領域でのメモリ確保が専用の関数で用意されており、ディレイラインのような大容量のメモリ確保にはこちらを使用することになっています。また、その場合のメモリ確保は動的に行うのではなく初期化時に行う必要があるとのことです。(それならlogue SDK 1.0のときと同様に特殊なスタティック変数で確保できるほうが便利だったかもしれませんが・・・。)

hvccでは、メモリ確保は現状すべて1つの関数(デフォルトはmalloc())で行うようになっており、バッファ領域だけ異なる関数でメモリ確保することはできません。また、そのタイミングも初期化時に限定されているわけではありません。logue SDK側のAPIとどう整合させるか、今後検討が必要な項目です。

今回サンプルで取り上げているPure Dataパッチは、大容量のメモリ確保はありません。そのため、全てのメモリ確保はmalloc()経由で行われます。前述のように、logue SDKのユニットからmalloc()を使用することの可否は不明ですが、とりあえず動作はしています。

シンボルテーブルのサイズ削減

以下の記事に書いたように、SDKのデフォルトのMakefileのままだと、ユニット(実際にはelf形式)ファイルの中に、不要なシンボルの情報が大量に残ってしまいます。

Pure DataのPatchからNTS-1 mkIIのOSCユニットを作ってみた(2)サイズの削減
先日、Pure DataのパッチをhvccでCに変換してNTS-1 mkIIで動かしてみましたが、単なる鋸波のオシレータに29Kbも使っていました。$ size ./sawosc.nts1mkiiunit text data bss dec...

これを回避するために、

(1) デフォルトでシンボルを不可視にするコンパイルオプションの追加
(2) 残さなければならないシンボルへのマーキング

が必要になります。

(1) は -fvisibility=hidden を指定するだけですが、(2) は少々厄介です。

詳細は上記の記事で書きましたが、マーキングするには関数や変数にvisibility(“default”)というアトリビュートを付けます。これは関数・変数本体だけでなく、ヘッダファイルにも行う必要があります。ヘッダファイルはSDKの一部ですが、SDK自体を書き換えるのは避けたいので、SDKのファイルをローカルのファイルで差し替えることになります。

具体的には、SDKのcommon/attributes.hの中で定義されているマクロが、コールバック関数およびユニットのヘッダ情報(リンカスクリプトで.unit_headerセクションに配置される)に付与するアトリビュートを決めていますので、これに以下のようにvisibility(“default”)を追加します。

$ diff ../common/attributes.h ./attributes.h 
50,51c50,51
< #define __unit_callback __attribute__((used))
< #define __unit_header __attribute__((used, section(".unit_header")))
---
> #define __unit_callback __attribute__((used, visibility("default")))
> #define __unit_header __attribute__((used, section(".unit_header") ,visibility("default")))

この改変したattributes.hはローカルに配置しますので、./attributes.hをcommon/attributes.hよりも優先して読み込ませる必要があります。その優先順位は通常、Cコンパイラの-Iオプションで制御できます。しかし、この”-I”オプションよりもさらに優先されるのが、

#include “file.h” のように” “で指定されたヘッダファイルは、このinculdeが書かれているファイルと同じディレクトリが最優先

というルールです。SDKのヘッダファイルはattributes.hを” “で指定していますので、このルールが適用されます。

このような場合、attributes.hをインクルードしているファイル自体もローカルにコピーしてくる必要があります。(変更したマクロ__unit_callbackや__unit_headerを使用していないファイルについては無視できます。 )logue SDKでは、attributes.hまでの依存関係(includeのチェーン)は以下のようになっています。

unit.cc, _unit_base.c ⇒ unit_genericfx.h ⇒ unit.h, osc_api.h, fx_api.h ⇒ attributes.h

osc_api.h, fx_api.hは__unit_callbackや__unit_headerを使用していないので無視できますが、それ以外のファイル(_unit_base.c、unit_genericfx.h、unit.h、attributes.h)はローカルに置くことが必要です。

また、visibilityアトリビュートはヘッダファイル内の宣言にも付与する必要がありますが、SDKのヘッダファイルでは各コールバック関数このアトリビュートが付与されていなかったので、以下のように追記します。

$ diff ../common/unit_genericfx.h ./unit_genericfx.h 
121c121
<   extern const genericfx_unit_header_t unit_header;
---
>   extern const __unit_header genericfx_unit_header_t unit_header;

$ diff ../common/unit.h ./unit.h
54,66c54,67
< int8_t unit_init(const unit_runtime_desc_t *);
< void unit_teardown();
< void unit_reset();
< void unit_resume();
< void unit_suspend();
< void unit_render(const float *, float *, uint32_t);
< int32_t unit_get_param_value(uint8_t);
< const char * unit_get_param_str_value(uint8_t, int32_t);
< void unit_set_param_value(uint8_t, int32_t);
< void unit_set_tempo(uint32_t);
< void unit_tempo_4ppqn_tick(uint32_t);
< void unit_touch_event(uint8_t,uint8_t,uint32_t,uint32_t);
<   
---
> __unit_callback int8_t unit_init(const unit_runtime_desc_t *);
> __unit_callback void unit_teardown();
> __unit_callback void unit_reset();
> __unit_callback void unit_resume();
> __unit_callback void unit_suspend();
> __unit_callback void unit_render(const float *, float *, uint32_t);
> __unit_callback int32_t unit_get_param_value(uint8_t);
> __unit_callback const char * unit_get_param_str_value(uint8_t, int32_t);
> __unit_callback void unit_set_param_value(uint8_t, int32_t);
> __unit_callback void unit_set_tempo(uint32_t);
> __unit_callback void unit_tempo_4ppqn_tick(uint32_t);
> __unit_callback void unit_touch_event(uint8_t,uint8_t,uint32_t,uint32_t);

上記に併せてMakefileも変更が必要になりますが、Makefileについては上記以外の変更もありますので、次節でまとめます。

Makefileの修正

Makefileの変更点は、大きく以下の2点です。

・追加の拡張子「.cpp」への対応(hvccが生成するコードが拡張子.cppのため)
・_unit_base.cのパスの変更(前節で書いた通り、変更したincludeファイルを読ませるため)

1点目の.cpp拡張子のビルドルール追加ですが、テンプレートのMakefileではC++の拡張子が.ccのみになっているので、.cppについてもC++のソースとして扱われるよう、以下のようにルールを追加します。

$ diff ../dummy-genericfx/Makefile ./Makefile
116a117
> vpath %.cpp $(sort $(dir $(CXXSRC)))
121c122,126
< CXXOBJS := $(addprefix $(OBJDIR)/, $(notdir $(CXXSRC:.cc=.o)))
---
> CXXOBJS := $(addprefix $(OBJDIR)/, \
>               $(notdir \
>                 $(patsubst %.cc,%.o,$(patsubst %.cpp,%.o,$(CXXSRC))) \
>               ) \
>             )
193c198,202
< $(CXXOBJS) : $(OBJDIR)/%.o : %.cc Makefile
---
> $(OBJDIR)/%.o: %.cc Makefile
> 	@echo Compiling $( 	@$(CXXC) -c $(CXXFLAGS) -I. $(INCDIR) $< -o $@
> 
> $(OBJDIR)/%.o: %.cpp Makefile

内容的に.ccのルールをコピペして拡張子を.cppに変えただけですが、Makefileはルールを順にマッチしていきますので、%.o: %.ccにマッチしなければ%.o: %.cppへのマッチを試みます。

2点目の_unit_base.cのパスの変更ですが、元のMakefileではSDKのファイルをソースコードとして追加しており、これを削除します。

$ diff ../dummy-genericfx/Makefile ./Makefile
109c109
< CSRC += $(realpath $(COMMON_SRC_PATH)/_unit_base.c)
---
> #CSRC += $(realpath $(COMMON_SRC_PATH)/_unit_base.c)

_unit_base.cは、ヘッダファイルをローカルから読ませるために、ローカルにコピーしてきて改変無しにそのまま使用します。ローカルのファイルなので、CSRCに追加するのではなくconfig.mkの中でUCSRCに追加することにします。

config.mkの設定

config.mkには、hvccが生成したコードや_unit_base.cを追加する他に、以下のような設定が必要になります。

・libstdc++のリンク
・サイズ削減のための–gc-sections
・visibilityのデフォルトをhiddenに(既出)
・assert文を無効に(-DNDEBUG)

これらに関連する差分は以下のようになります。

$ diff ../dummy-genericfx/config.mk ./config.mk
39a42,43
> ULIBS  += -lstdc++
> ULIBS  += -Wl,--gc-sections
45c49
< UDEFS = 
---
> UDEFS = -DNDEBUG -fvisibility=hidden

以上でhvccで生成したCのコードをlogue SDKでNTS-3用にビルドできるようになります。
実際にビルドしたものを動かしている様子です。

コメント