hvccをlogue SDK向けに拡張する

hvccを使って、Pure Dataからlogue SDK用のCファイルを生成してみていますが、生成したコードをlogue SDKから利用するためには追加で手作業が必要となっています。

この手作業の部分を自動化するのに、hvccのexternal generatorを作成中です。external generatorはhvccのプラグインのようなもので、これをhvccから呼び出して、hvccが生成するコードをカスタマイズすることができます。いくつかのプラットフォーム(DaisyやEmscriptenなど)向けのgeneratorは最初から用意されています。

今回はとりあえずexternal generatorの中身を調べて、できそうなところから手を付け始めましたので、途中経過として記録しておきます。

外部ジェネレータの目的

logue SDK用のexternal generatorができれば、こちらの記事で手作業でやっていたことが自動化できます。具体的には以下のような作業です。

■ビルドに必要なファイルのコピー
・Makefile
・ld/配下のosc_api.symsrules.lduserosc.ld
・tpl/_unit.c
■hvccが生成したファイルの編集
・HvUtils.hでhv_alloc、hv_realloc、hv_freeの定義を追加
■テンプレートからのファイル作成
・hvccコンテキストのlogue SDK向けラッパ
 初期化、音声信号の生成、パラメータ設定(*)に関するコード
 ヒープとして用いるスタティック変数のサイズ設定(*)
・project.mk
 プロジェクト名
 コンパイルするソースファイルのリスト
・manifest.json
 オシレータ名
 パラメータやノートイベントに関する設定(*)

上の作業の中には単にファイルをコピーするような簡単なものもありますが、(*)を付けたものは手間がかかりそうです。

外部ジェネレータのしくみ

external generatorは、hvccのcompiler.pyのcompile_dataflowから呼び出されます。呼び出された時点で、hvccによるCのコードの生成は済んでいます。external generatorは、生成されたコードや呼び出し元から与えられる情報を元に、必要な処理を行っていきます。

呼び出し元から与えられる情報ですが、external generatorは以下のような関数で呼び出されますので、これらのパラメータの値がexternal generatorに与えられる情報ということになります。

    def compile(
            cls,
            c_src_dir: str,
            out_dir: str,
            externs: ExternInfo,
            patch_name: Optional[str] = None,
            patch_meta: Meta = Meta(),
            num_input_channels: int = 0,
            num_output_channels: int = 0,
            copyright: Optional[str] = None,
            verbose: Optional[bool] = False
    )

コードをみたところ、概略以下のような感じのようです。

(cls:クラスオブジェクト)
c_src_dir: 生成したCのソースファイルのディレクトリ
out_dir: -oオプションで指定された出力先ディレクトリ
externs: Pure Dataパッチ内で、パッチ外から与えられると宣言されたパラメータに関する情報 ※
patch_name: -nオプションで指定されたパッチ名称
patch_meta: -mオプションで与えられたメタデータ(現状、特定のプラットフォームでのみ有効)
num_input_channels: 入力チャネル数
num_output_channels: 出力チャネル数
copyright: –copyrightオプションで与えられた著作権表示の文字列
verbose: -vオプションの値

※@hv_param、@hv_event、@hv_tableなどで宣言する。

外部ジェネレータの中身

外部ジェネレータは上記のAPIで呼び出されるPythonの関数です。Pythonでできることは何でもできますが、特にlogue SDK対応としては、externsに含まれる情報とc_src_dirに入っているソースコードが、外部ジェネレータが扱う主な素材になります。

externsに含まれる重要な情報は、Pure Dataのreceiveオブジェクトで受け取るパラメータの情報で、これはexterns.parameters.inParamで参照できます。inParamの中身は、例えばこんな感じになっています。

[ ( 'alt',
    IRReceiver(display='alt', hash='0xEF06E136', extern='param', attributes={'default': 0.0, 'max': 0.5, 'min': 0.0, 'type': 'float'}, ids=['Db7fCjel'])),
  ( 'harm1',
    IRReceiver(display='harm1', hash='0xF115A43D', extern='param', attributes={'default': 2.0, 'max': 8.0, 'min': 1.0, 'type': 'float'}, ids=['1IauOKrI'])),
  ( 'harm2',
    IRReceiver(display='harm2', hash='0x44556F7B', extern='param', attributes={'default': 4.0, 'max': 8.0, 'min': 1.0, 'type': 'float'}, ids=['v2kUWZnX'])),
  ( 'harm3',
    IRReceiver(display='harm3', hash='0xEC1D0E7B', extern='param', attributes={'default': 8.0, 'max': 8.0, 'min': 1.0, 'type': 'float'}, ids=['i3mK0BaS'])),
  ( 'pitch',
    IRReceiver(display='pitch', hash='0x8B2148DD', extern='param', attributes={'default': 0.5, 'max': 1.0, 'min': 0.0, 'type': 'float'}, ids=['7gl8w2LT'])),
  ( 'shape',
    IRReceiver(display='shape', hash='0xB063C1EC', extern='param', attributes={'default': 0.5, 'max': 1.0, 'min': 0.1, 'type': 'float'}, ids=['bJV5XG2e']))]

パラメータ名、値域、デフォルト値などの情報はreceiveオブジェクトで指定したものがそのまま入っています。ちなみにexternsを生成しているのはgenerate_extern_infoです。

このPure Data側のパラメータの情報は、logue SDKで入力できるパラメータに適切にマッピングする必要があります。logue SDKではパラメータの値域の制約(ShapeとAltは0~1023、一般のパラメータは-100~100)があるためです。なお、Pure Dataでは数値は基本的にfloatしかありません。

また、Pure Data側のreceiveオブジェクトと、

・logue SDKの8つのパラメータ(Shape、ShiftShape、6つのパラメータ)
・実行時に取得できるパラメータ(オシレータピッチおよびLFO)
・イベント(ノートON、ノートOFF)

等とのマッピングも考える必要があります。このあたりはちょっと設計に頭をひねるところですね。

ほとんどのパラメータは名称を固定で決めてしまえば済みますが、logue SDKでmanifest.jsonで指定する6つのパラメータの名称や、どのパラメータの何番目にするかなどは、ユーザが自由に決められるようにしたいところです。本当はメタデータが使えるといいのですが、外部ジェネレータから使う方法が分かりません。

外部ジェネレータの使い方

external generatorは、hvccを利用する際に以下のように呼び出します。

$ hvcc patch.pd -G example_hvcc_generator

カレントディレクトリにexample_hvcc_generator.py というexternal generatorが置かれている想定です。(external generatorはPythonで書きます。hvcc自体もPythonで書かれています。)

なお、上の例ではカレントディレクトリがPythonのサーチパスに含まれている想定です。
含まれていない場合は

export PYTHONPATH=$PYTHONPATH:$(pwd)

のように追加する必要があります。

現時点の進捗

現状、上に挙げたTo Doリストの中では、パラメータに関するもろもろの課題と、ヒープのサイズ設定の課題がまだ未実装ですが、それ以外は一通り動くようなジェネレータができています。下図は実験用のPure Dataパッチです。

ありがちなadditive synthesizerです。基音の他、2・4・8・1/2倍の周波数の音をミックスするようなオシレータになっています。

これをジェネレータにかけると、必要なファイルが生成され、make installでNTS-1にロード可能なオシレータユニットがビルドできます。
できたオシレータユニットを動かしている様子です。音が割れてしまっているところがありますが・・・。


気になるバイナリのサイズはこんな感じです。

$ size build/sinx4.elf
   text	   data	    bss	    dec	    hex	filename
  20200	   2128	   3136	  25464	   6378	build/sinx4.elf

この倍くらいまでの複雑さのパッチなら、何とかなりそうです。

コメント