概要
随時更新の雑なメモです。詳しく知りたければ、コメントなどでご連絡いただければちゃんと書きます。
VST3は、各DAWホストごとに関数の呼び出し方が微妙に違ったりするので、全てのDAWホストで正常に動くように作るのは難しいです。
VST3開発時にハマりやすいポイントを随時更新でリストアップしていきます。
対象VST3 SDKバージョン: 3.6.6
VST3 SDKとドキュメント: https://www.steinberg.net/en/company/developers.html
テストツール
VSTは複雑なので、テストツールがあると効率的に開発できます。
vstvalidator
VST3 SDKに含まれているテストツールです。
VST全般に対して適用できる、いろいろなテストケースがすでに実装されています。テストケースの追加もかんたんにできそうです。
Dr. Memoryと組み合わせて使うと便利です。
VST-Plugin Unit Test
VST2向けのテストツールです。各DAWに特化したテストも含まれます。かなり古いツールなので、false positiveも多いと思います。明らかにバグの無いシンプルなVSTのテスト結果と比較することで、false positiveを取り除くと良いと思います。
ダウンロードはこちら(grep "unit"): http://www.pcjv.de/applications/tools/
Dr. Memory
Dr. Memory - http://drmemory.org/docs/index.html
valgrindのWindowsでも使える版です。デバッグ情報を入れてビルドすると、ちゃんとエラーが起きた関数名と行が出てくるので便利です。vstvalidatorと組み合わせると良いです。
オプション一覧
http://drmemory.org/docs/page_options.html
-X オプションに対する反対のオプションは、-no_X オプションです。
コマンド例
/path/to/drmemory.exe -no_check_delete_mismatch -- /path/to/vstvalidator.exe /path/to/your_vst.vst3
よくあるバグ
今まで遭遇したバグのリストです。
罠一覧との違いは、罠は原因にフォーカスしたもので、よくあるバグは表面にあらわれる事象にフォーカスしたものです。
動作確認の参考にしてみてください。
自動テスト化をしたいですが、まだできていません。
- 入力バスをdeactiveにするとノイズが出力される
- DAWによっては、silentflagが立っているときに、入力バッファがゼロにクリアされていないことがあります(VST3仕様ではゼロクリアだったと思う)。 なので、silentflagは対応必須です。
- 変な入力(超大きい音、NaN、Inf)を入力すると、変な出力が出力され、変な入力が終わったあとも出力され続ける
- 変な入力は他のプラグインのバグなどによって引き起こされます
- NaNがIIRフィルタなどに保持されてしまうパターンがよくあります
- UIとパラメータが同期していない
- プロジェクトのロード直後、UIとパラメータがずれている
- インサート直後、UIとパラメータがずれている
- UIからパラメータを操作したときに、パラメータが変更されない
- オートメーションでパラメータを操作したときに、UIが変更されない
- プロジェクトを保存してロードすると、パラメータが復元されない
- ReaperでVSTの入力数を増やしたときに、チャンネル数が変わる(3つになったりする)
- モノラル入力をしたときにおかしくなる
罠一覧
DLL初期化方法と、エディタのopen close方法の罠
ReaperとFL Studioで挙動が違います。
詳細は忘れましたが、トレースログを仕込んで双方のDAWで比較してみると、わかります。ゆくゆくは、いろいろなDAWのAPI呼び出しシーケンスを再現するようなテストを作りたいです。
DAWによってことなるポイント
- 初期化順
- ライフタイム(Editor, Processor, DLL)
ロックフリーの罠
リアルタイム系のプログラムを書いている人にとっては常識かもしれませんが、私はロックフリーで書くのは初めてだったので新鮮でした。おなじく、初めての人向けのメモです。
基本
VST3 仕様準拠のためのlock-free
VST3では音声処理スレッドと、UIスレッドが別のスレッドに分かれています。音声処理スレッドは仕様上、lock-freeで実装するように決められています。どの関数がどのスレッドで呼ばれるか?の詳細は、API docのWorkflowを参照してください。
クラッシュ回避のためのlock-free
また、一部のDAW(FL Studioなど)では、Process関数内でメモリやスレッド周りの一部のシステムコールを呼ぶと落ちるみたいです。具体的に、何がどういう理由で落ちているのかは調べきれていません。lock-freeであることは、仕様に準拠したり処理落ちさせづらくするためだけではなく、クラッシュさせないために必須の対応です。
プチり回避のためのlock-free
スピンロックはクラッシュしませんが、lock-freeではないので、グレーです。Windowsのタスクスイッチ間隔は最短1msらしいですが、オーディオのバッファサイズをそのくらいに設定していると、UIスレッドがロックを保持したままタスクスイッチしたときにプチります。
lock-freeの実装方法
lock-freeで実装するには、普通のmallocや普通の同期方法(mutex、クリティカルセクション)を使わないように実装する必要があります。
boost::lockfreeを使うと、lock-freeの実装がしやすくなります。
Reaperのnotifyの罠
これはかなりはまりました。
やりたいこと
アナライザーVSTを作りたいです。
そのためには、少なくとも音声データ、または、音声データを解析したデータを、UI側に送る必要があります。
VST3ではProcessorとEditor/Controllerが同一プロセス内や同一PC内になくても動くような設計になっています(実際にそうするケースは少ないと思いますが)。この設計に沿う場合、メモリやプロセス間通信によって送るのはNGです。メッセージ送信APIでデータを送る必要があります。
※ 後述のVST間通信のように、妥協して仕様準拠しないのもありだと思いますが
問題
最初は、メッセージ送信APIをProcessorの音声処理関数(音声処理スレッド)から直接呼ぼうとしました。データ供給元からデータを送りつけるpush型の実装です。
実は、Cubaseでは音声処理スレッドからメッセージ送信APIを呼べます。Cubaseではメッセージが非同期的に処理されるからです。これが罠です。これに甘えて実装すると、Reaperにやられます。
Reaperのメッセージは同期的に処理されます。つまり普通の関数呼び出しのように処理されます。同期的に処理されるので、音声処理スレッドからメッセージ送信をすると、具体的にどうなるか忘れましたがなにか問題がおきました。少なくとも、Process関数の中身がlock-freeかどうかが、Reaper依存になってしまいます。
解決策「notify返し」
音声処理スレッドから直接メッセージ送信APIを呼べないので、逆にEditorからProcessorにメッセージ送信し、それを受け取ったProcessorが今度は逆に、Editorへメッセージ送信するようにしました。名づけて、notify返しです。
Cubaseでの流れは以下です。スレッド番号は仕様を満たした上で、最大限別のスレッドで実行するとした場合の番号付けです。実際は、スレッド1 = スレッド2 != スレッド3だと思います。
- EditorからProcessorへメッセージ送信APIを呼ぶ (スレッド1)
- Processorのnotifyコールバック (スレッド2)
- lock-free queueなどで音声処理スレッド(スレッド3)からデータを取得 (スレッド2)
- ProcessorからEditorへメッセージ送信APIを呼ぶ (スレッド2)
- Editorのnotifyコールバック (スレッド1)
Reaperの場合は以下です
- EditorからProcessorへメッセージ送信APIを呼ぶ (スレッド1)
- Processorのnotifyコールバック (スレッド1)
- lock-free queueなどで音声処理スレッド(スレッド3)からデータを取得 (スレッド1)
- ProcessorからEditorへメッセージ送信APIを呼ぶ (スレッド1)
- Editorのnotifyコールバック (スレッド1)
うれしいオマケ
Editorのタイマーイベントをトリガーにこの仕組みを動かしているので、Editorがアクティブでないときは、処理されません。If and only if 解析データが必要な場合のみ、処理が実行されるので効率的です。
メッセージ送信のほかの使い方
notify返しではありませんが、メッセージ送信はレイテンシー変更にも利用しています。
オートメーションによって、パラメータが変化し、レイテンシーが変更されるケースを考えます。
オートメーションによるパラメータ変更検知は、Processorでしかできません。レイテンシーが変更されたときは、ControllerからkLatencyChangedでrestartComponentを呼ぶ必要があります。
つまり、ProcessorからControllerへ情報を渡す必要があります。これもnotifyによって行っています。
Presonus Studio One 3の罠
Process呼び出し時のinputバッファとoutputバッファが同じです。
左右のチャンネルを交換する処理を書いていてハマりました。
outputL = inputR;
outputR = inputL;
ちゃんと仕様にも書かれています。
inputバッファとoutputバッファが同じ場合と違う場合で、処理結果が同じになるというテストを書けばよさそうです。
Presonus Studio One 3 アンチデバッグの罠
Presonus Studio One 3経由でVSTを起動すると、ハードウェアブレークポイントが設定されています。なので、アンチハードウェアブレークポイント機能をつけていると、false positiveが生じます。
クラックによる損失への漠然とした恐れ半分、技術的な興味半分で実装しましたが、ビジネス的に考えると割りに合わないと思います。ヒトは、損をするかもしれないリスクを過大評価しがちですし。
- メリット
- 海賊版流通による損失を防ぐ
- 技術的な好奇心を満たせる
- 損をするかもしれない不安を解消できる
- デメリット
- false positiveによるブランド毀損リスク
- 海賊版流通によるマーケティングの機会損失
- ロジックが増えて保守性低下
逆に圧倒的なクラック耐性があれば、別の意味でマーケティングに貢献しそうです(ゲーム業界など)。世界最強のアンチクラックを導入して、たったX日でクラックが突破される、ってエンターテイメントですよね。
moodycamel::concurrentqueueの罠
誤認かもしれませんが、moodycamel::concurrentqueueにバグがあるかもしれません。バグがあるにしろないにしろ、より有名で枯れているであろう、boost::lockfree::queueを使うのが良いと思います。boost::interprocessでプロセス間通信にも対応しているので、VST間通信を行う場合にも重宝します。
IPP DFTの罠
IPPはFFTWよりも高速なDFTが実装されていて、かつ、ライセンス的に商用でコード公開なしでstatic link可能なので、VST開発に便利です。ですが、IPPのDFTは、範囲外アクセスを行います。アラインを要求するだけではなく、長さがその環境のSIMDの長さ倍になっていることを要求します。仕様にアラインのことは書かれていますが、長さがSIMDの長さ倍になっていることは明記されていません。アラインといえば、普通この仕様なのでしょうか?IPPのmallocを使えばそれが保証されるみたいですが、独自にmallocした場合は要注意です。Dr. Memoryで発見しました。
仕様引用
Data vectors for these functions must be aligned to an appropriate number of bytes that is determined by the SIMD width that is supported by the customer's platform - use ippMalloc function for such alignment.
FL Studioブリッジ (ilbridge.exe)の罠
かなりハマりました。
ilbridge.exeには3つのバグがあります。
1. バスのアクティブ化
バスのアクティブ化/非アクティブ化を行えません。
FL Studioのプラグインの設定ではバスのアクティブ化/非アクティブ化を行えるのですが、VSTに伝わりません。
また、FL Studio 20では、VST側からデフォルトアクティブフラグを与えていなくても、強制的に全てアクティブになります。
2. バスが非アクティブなときのノイズ
FL Studio 11では、非アクティブなバスにはノイズ(おそらくuninitializedな領域)が入力されます。silentフラグもたっていません。1の問題により、バスが非アクティブかどうかを知る手段は無いので、直すには、プロセスがilbridge.exeかどうかを判定して、特別なロジックを入れるしかありません。
FL Studio 20では強制的に全てアクティブになるので、実質的に解決されています。
3. 右クリックメニューの罠
VST3には、ホストの右クリックメニューを表示するAPIがあります。FL Studio 11のBridge経由でインサートしているときに、DAWのメニュー表示をさせようとすると落ちます。最新のFL Studioでは試していません。プロセスがilbridge.exeかどうかを判定し、メニュー表示を無効化することで回避しました。
denormalの罠
そこまで隠れた罠ではないですが、音声処理関数では、パフォーマンス劣化を予防するために、非正規化数を作らないようなCPUのフラグを設定するのがオススメです。
boost::interprocess::managed_shared_memoryの罠
後述のVST間通信で共有メモリを使います。boost::interprocess::managed_shared_memoryを使おうと思ったらハマりました。
ポイントは、Windowsの共有メモリの挙動を意図するなら、boost::interprocess::managed_windows_shared_memoryを使うということです。最初、Windowsの共有メモリの仕様を読みながら、てっきりmanaged_shared_memoryがその仕様どおりにうごくと思って使っていて、仕様の違いにハマりました。managed_shared_memoryは、linuxの共有メモリの仕様にあわせてあるらしいです。windowsでは、memory mapped fileを使って実装されていて、プロセス終了後も残ります。
https://www.boost.org/doc/libs/1_63_0/doc/html/interprocess/sharedmemorybetweenprocesses.html#interprocess.sharedmemorybetweenprocesses.sharedmemory.emulation
https://www.boost.org/doc/libs/1_63_0/doc/html/interprocess/sharedmemorybetweenprocesses.html#interprocess.sharedmemorybetweenprocesses.sharedmemory.windows_shared_memory
モノラルトラックの罠
モノラルトラックの扱いはDAWによって異なります。CubaseとStudio Oneではモノラルトラックをモノラルで扱います。FL StudioとReaperは、モノラルトラックをステレオに変換して扱います。この違いによって、VSTの入力チャンネル数が変わってきます。
また、Cubaseでは、setBusArrangementがsetupProcessingの後に呼ばれます。チャンネル数に応じた初期化を、setBusArrangementでも行う必要があります。そうしないと、VST側で最初にバスをステレオで設定したあとで、Cubaseによってモノラル化されたケースで問題になります。
Cubase、Studio Oneでは、モノラルトラックがミキサーでステレオ化されるときに音量が変化します。モノラルトラックの音量メーターの仕様もDAWによって異なります。
モノラルトラックの音量メーター | mono -> stereo時の音量変化(linear scale) | |
---|---|---|
Cubase | stereo化後 | sqrt(0.5) |
Studio One | stereo化前 | sqrt(0.5) |
実装パターン
VST間でオーディオデータを渡したい
VST間でオーディオデータを渡したいということがあります。マルチトラックアナライザーや、iZotope Neutron 2 Visual Mixer のような場合です。
全てのDAWでVST3の仕様に準拠した形で渡すのはむずかしいです。VST3の仕様上は、オーディオを複数入力可能なのですが、任意のVSTの複数の入出力を自由にルーティングできるDAWは、多くありません。FL StudioやReaperではできます。
VSTは同一PC上になくても良いというVST3の設計思想から逸脱しますが、共有メモリを使って渡します。実用上は、ほとんどのケースでVSTは同一PC内、ブリッジとかを使わなければプロセス内で実行されるので、よしとします。
ポイントは3つです。
DLL間の通信
共有メモリとlock-freeアルゴリズムを使います。
boost::lockfree
boost::interprocess
を使うとかんたんに実装できます。
音声処理スレッドとUIスレッド間の通信と同じような実装になります。
https://www.boost.org/doc/libs/1_55_0/doc/html/lockfree/rationale.html#lockfree.rationale.interprocess_support
注意点: https://www.boost.org/doc/libs/1_63_0/doc/html/interprocess/sharedmemorybetweenprocesses.html#interprocess.sharedmemorybetweenprocesses.mapped_region_object_limitations
オフセットポインターなど勉強になりました。malloc注入をできるようにしているライブラリは多数ありますが、オフセットポインターを注入できるようにしているライブラリは少数です。初めてその観点に気付きました。。前述のmoodycamel::concurrentqueueは対応できていないので、共有メモリ用途では使えません。良く考えると私たちは普段メモリのアドレスが変わらないことに依存したコードを書きまくっていますね。ほとんどのケースで有効ですが。
ポイントは、罠にも記載しましたが、Windowsの共有メモリの挙動を意図するなら、boost::interprocess::managed_windows_shared_memoryを使うということです。
サンプル単位でのタイミング同期
Process関数には、ProcessDataが渡され、その中に現在の再生位置がサンプル単位で入っています。このデータを使って同期します。
全てのDAWでちゃんと送られる時刻情報は、projectTimeSampleのみです。
問題は、ループがあると時間がまきもどるので、別の時刻なのに同じprojectTimeSampleをとりうるということです。
これをうまくさばく必要があります。
むずかしくはないです。
因果関係の保証
因果関係を保証するためには、オーディオデータの送信元VSTが、送信先VSTよりも先に処理される必要があります。つまり、共有メモリに書き込んでから読み込む必要があります。
VSTの処理順はどうなっているのでしょうか?
DAWの実装次第ですが、もし、私がDAWの実装者だったら、VST間の依存関係グラフを作り、並列に実行可能な部分をかたっぱしからワーカーに渡すように実装します。
こう実装されていた場合、DAWの知らないところでVSTが勝手に通信していたら、DAWが依存関係を把握できずに、因果関係が逆になる形で、VSTが処理される可能性があります。
これを保証するには、うそでも良いから音を出力し、送信先のVSTに送信元の音を入力することです。そして、通信先のVSTでその音を無視します。DAWは、音が無視されていることを知らないので、因果関係を保証するために、必ず送信元を先に処理します。
iZotope Neutron 2について
iZotope Neutron 2では、Inter plugin communicationと表現されています。
https://s3.amazonaws.com/izotopedownloads/docs/neutron201/ja/visual-mixer/index.html
以下の情報によると、恐らく、通信できるのは同一プロセス内という制限があると思います。技術的な制約というよりも、わかりやすくするための、あえての制約だと思います。(別々に起動したDAW同士で音が混ざるのは直感的ではないので)
https://www.reddit.com/r/FL_Studio/comments/7cdwyt/izotopes_interplugin_communication_technology/
Future Work
やりたいことです。
VSTトレース
VSTのAPI呼び出しトレースログをとり、それを再生して自動テストできるようにする。DAW依存のバグを取り除ける。複雑なシナリオをかんたんに作れる。
シナリオをランダムに組み合わせて実行しまくれば、いろいろいじっても落ちないことの確認や、NaNが出力されないことの確認ができる。
用意したいテストケース
- inputバッファとoutputバッファが同じ場合と違う場合で、出力が同じになることの確認
- silentflag + 無音を入力したときと、silentflag + 無音以外を入力したときで、出力が同じになることの確認
- 音声処理スレッドから呼んではいけないものが呼ばれていないことの確認
- モノラル入力とステレオ入力で、出力が同じになることの確認
- モノラル出力とステレオ出力で、出力が同じになることの確認
JUCEを試す
使ったことがないですが、ここまで罠が多いと、JUCEなどの有料フレームワークを使ったほうが良いかもしれません。
あらためて考えると、
多くの環境から使われるDLLの開発なので、普通のプログラムよりも満たさないといけない要件が多く、テスト環境が多く、実装難易度が高い。ということは、かんたんに予想できます。