hvccを使って、Pure Dataのパッチをlogue SDKで動かしてみるシリーズです。これまでは、NTS-1 mkIIのオシレータユニットを作成し、サイズの削減を試みて、前回はNTS-1 mkII上でディレイユニットも動作させました。
ディレイユニットの制限サイズである32Kbは、初代NTS-1のオシレータの制限サイズと同じです。なので、初代NTS-1でもPure Dataパッチからhvccで生成したコードを動作させることができるのでは、と試してみました。
動かしてみたのは、以前も使ったこのSAWオシレータです。

Pure Dataパッチをhvccで変換してlogue SDKに移植。logue SDK v1系列(prologue/minilogue xd/NTS-1)でもphasor~を使ったSAWオシレータが動きました。でもさすがに32Kb制限は厳しいかな。ちなみにmodfxは6Kb、delfx/revfxは12Kbの制限があるので、さすがにビルドは無理そうです。 pic.twitter.com/m9SKpLUwnV
— boochowp (@boochowp) February 14, 2025
主なハードルは、やはりメモリ空間の小ささです。初代NTS-1(logue SDK 1.x)では、各ユニットのサイズ制限は以下のようになっています。(ld/userXXX.ldを調べると判ります。)
・オシレータ: 実行可能領域32KB、 SDRAM領域なし
・Mod FX: 実行可能領域6KB、 SDRAM領域128KB
・ディレイFX: 実行可能領域12KB、 SDRAM領域2432KB
・リバーブFX: 実行可能領域12KB、 SDRAM領域2432KB
他方、hvccで生成したコードは、コンパイル後で最低でも12KB程度は必要になります。SDRAM領域には実行可能コードは置けませんので、logue SDK 1.x系では、Pure Dataをhvccで変換するという今回の方法で、FXユニットを動作させるのは難しいということになります。(不要なコードを削りまくればあるいは動作するかもしれませんが、そこまでしてPure Dataを使う意義は薄そうに思います。)
もう一つlogue SDK 1.xと2.0系の大きな違いは、動的なメモリ管理です。MB単位のメモリを持つdrumlogueはもちろんですが、NTS-1 mkII/NTS-3も、malloc()で実行時にメモリを確保することができます。一方、prologue/minilogue xd/NTS-1では、すべてのメモリは静的に確保するのが基本で、malloc()はNULLを返してきます。
hvccは、ドキュメントによれば「メモリ確保は初期化の際にまとめて行い、以降のメモリ管理は内部で行う」と書かれています。
Notes on Memory Usage
All memory allocation is performed at initialisation. New memory is not allocated on the heap after _new() is called. In general, a Heavy context allocates memory up front and uses a custom memory manager to keep track of state. The stack is used extensively to keep track of temporary information, especially in relation to message passing.
生成されたコードを調べたのですが、
・HvUtils.hの中でhv_malloc(),hv_realloc(),hv_free()が#defineされている(デフォルトではmalloc, realloc, free)
・hv_malloc()によるメモリ確保は初期化の際にのみ行われている
・メモリ確保直後に初期値を書き込むケースがあり、malloc()の失敗は許容されない(assertでチェックしている)
・確保するメモリのトータルの量は、デフォルトで13KB弱だが減らすことは可能
・hv_realloc()は定義されているが今回のパッチでは使われない
ということが確認できました。
この初期化の際に確保されているメモリですが、Heavyコンテキストオブジェクトそのものの他、LightPipeやMessageQueueなどいくつかの構造体のためのバッファもあります。元になるPure Dataパッチが複雑になれば、確保するメモリがさらに増えていくことも想像できます。従って、hv_malloc()を使ってメモリを確保している箇所を、静的に確保した変数へのポインタを使うように個別に変更していくには、かなりコードの書き換えが必要ですし、メンテナンス性も低いと思われました。
そこで、今回はmalloc()の代替となる関数を簡易的に実装することで、hvccの生成したコードを動かしてみます。
以下が今回使った簡易malloc()です。静的に確保したバイト列をヒープメモリとし、logue_malloc()で必要なサイズを切り出していくだけのものです。
logue_mem.h:
logue_mem.c:
hvccが生成したコードでは、HvUtils.hの中でhv_malloc()、hv_realloc()、hv_free()を定義しています。これらが上に示した簡易logue_malloc()を呼ぶように、HvUtils.hの”// Memory Management”とあるパートの末尾(”// Assert”の直前)に以下のようなコードを追加します。
#include "logue_mem.h"
#undef hv_malloc
#define hv_malloc(_n) logue_malloc(_n)
#undef hv_realloc
#define hv_realloc(a, b) logue_malloc(b)
#undef hv_free
#define hv_free(x) logue_free(x)
ドキュメントに「メモリ管理は内部で行う」とあったので、logue_free()は実際にはメモリを返却しません。とはいえ、logue_malloc()を続けていけばすぐにメモリが枯渇してしまいますし、ヒープがどのくらい必要なのかもよく分かりません。ですので、以下のようにprintf()を入れて、hvccが生成したコードの実際のhv_malloc()とhv_free()の呼び出し状況をLinux上で調べてみました。
メイン関数は以下のようなものです。hv_heavy_new_with_options()はバッファサイズを指定できるコンストラクタで、パラメータは先頭からサンプリングレート、メッセージプールのバッファサイズ、入力Queueのバッファサイズ、出力Queueのバッファサイズです。バッファサイズの単位はKBで、デフォルト(サイズを指定しない場合のコンストラクタの設定)はこの順に10, 1, 0となっています。
これをLinux上で動かすと、結果はこんな感じになりました。
$ ./hvtest
malloc: 248 fa4b9040
malloc: 1024 fa4b9138
malloc: 1024 fa4b9538
malloc: 40 fa4b9938
malloc: 16 fa4b9960
malloc: 16 fa4b9970
malloc: 16 fa4b9980
malloc: 16 fa4b9990
malloc: 16 fa4b99a0
malloc: 16 fa4b99b0
malloc: 16 fa4b99c0
malloc: 16 fa4b99d0
malloc: 16 fa4b99e0
malloc: 16 fa4b99f0
malloc: 16 fa4b9a00
malloc: 16 fa4b9a10
malloc: 16 fa4b9a20
malloc: 16 fa4b9a30
malloc: 16 fa4b9a40
malloc: 16 fa4b9a50
合計2592バイトが確保され、while()ループを続けてもこれ以上メモリが確保されることはありませんでした。従って、今回のパッチではヒープサイズは3KBで足りています。
この確保されたメモリの中身ですが、
・248バイト:Heavyコンテキストオブジェクト
・1024バイト:メッセージプールバッファ
・1024バイト;入力Queueバッファ
・それ以外:hv_processInline()の初回の実行時
となっています。hv_processInline()は実際にパッチを動作させて音声を生成させる関数ですが、初回動作時に必要なメモリを追加で確保していると思われます。40バイトはMessageNodeという構造体、16バイトはMessageListNodeという構造体のサイズのようです。
ともあれ、これでメモリ関連の最小限の手当はできました。
今度はこれをlogue SDKの上で動かしてみます。
まずメインとなるソースです。実質的に、Hvコンテキストを作成し(OSC_INIT)、オシレータのピッチ情報を渡し、バッファに音声を書き込んでもらう(OSC_CYCLE)、という3行の処理しかしていませんが、音声データをfloatからQ31の固定小数点数に変換しなければならないので、その処理が加わっています。
hvwrapper.cpp:
コードの末尾に、リンクエラーが出たいくつかの関数のダミー実装を入れてあります。
・snsprintf()
float型の値を文字列表現に変換するための関数ですが、今回は使用していませんので中身は空です。詳しくは以前こちらの記事で書きました。
・__cxa_pure_virtual()
純粋仮想関数のためのエラーハンドラですが、libstdc++をリンクしても解決されなかったので、こちらにダミー実装を入れました。これについても以前こちらの記事で触れています。
・delete()、delete[]()
これらはオブジェクトのデストラクタです。通常はlibstdc++やlibsupc++をリンクすれば解決されるはずなのですが、logue SDK環境下ではリンクエラーになったので、ダミー実装を追加しました。実際には、logue SDKのユニットでデストラクタを呼ぶ必要はまずありません。
・_sbrk()
これはSTM32でmalloc()やfree()の内部から呼ばれる関数です。今回はmalloc()やfree()を使用しませんので、ダミー実装としています。実は、_sbrk()の実装は今回のlogue_malloc()とよく似たものになるはずなので、試しに_sbrk()を実装してみました。しかし残念ながら、ビルドは通るもののうまく動作しませんでした。ちょっと調べるのが面倒そうなので今回はいったん見送りましたが、_sbrk()をlogue SDK環境下でうまく実装できれば、今回のようにHvUtils.hに手を入れずに(malloc()やfree()を残したままで)、Hvコンテキストを動作させられるかもしれません。
project.mkも一応載せておきます。
project.mk:
こちらの記事やこちらの記事で書いた通り、-DNDEBUGでassertを無効にし、-fvisibility=hiddenでシンボルテーブルのサイズを削減しています。
以上から実際にユニットをビルドしてみると、以下のようになりました。
$ arm-none-eabi-size build/sawosc.elf
text data bss dec hex filename
13712 2120 3136 18968 4a18 build/sawosc.elf
元となるPure Dataパッチを複雑にしていくと、それだけバッファも増やす必要が出ますので、textとbssが両方膨らんでいくことは想定されますが、それでもあと13KBくらい余裕がありますので、ちょっとしたパッチなら動作させられる可能性は高そうに思います。


コメント