CLAP 1.0公開を機にオーディオプラグイン規格の何たるかを知る (4) 拡張機能・各論 ほか

一般的な拡張機能

状態(state)の保存・復元

DAWで音楽を打ち込んだ内容をセーブすると、各トラックに設定されたプラグインのパラメーター等をひと通り操作した状態(state)を保存することになります。逆に楽曲データをロードすると、プラグインのパラメーター等を保存データから復元することになります。

DAWはひとつのプロセスの中にさまざまなプラグインをロードするので、他のアプリケーションに比べると頻繁にクラッシュします。そのため、楽曲をセーブする前に予防的に状態を保存することがあります。

状態の保存先は大抵のプラグインフォーマットではただのバイトストリームですが、LV2では保存する項目と値を構造的に格納するスタイルの仕様になっています。LV2は全体的にSemantic Webの流儀に従っており、LV2 Stateの仕様もその影響を受けていると考えられます。

プリセットの利用

プラグインの中にはMIDIでいうところの「プログラムチェンジ」に相当する機能を実装しているものがあります。シンセの場合は、これは単純にパラメーターの設定値の集合だけで実現していることも多く、その場合はプリセットと呼ぶほうが適切ともいえます。この機能をどう呼ぶかはプラグインフォーマット次第です。ここでは便宜上プリセットと呼びます。JUCE AudioProcessorならProgramと呼ばれます。

パラメーター設定の集合であると考えるとピンとくるかもしれませんが、プリセットが実装すべき機能は実のところ状態の機能とほぼ重複します。プリセットのロードとは、プログラムナンバー、プリセットの番号といったものを状態の代名として指定して状態を復元するのとほぼ同じです。

プラグインフォーマットによっては、ユーザープリセットの保存のような機能を可能にすることも考えられます(JUCEにはそのような機能が存在します。AudioProcessor::ChangeProgramName()など)。

GUI

GUIはオーディオプラグインの重要な機能のひとつですが、「無くても良い」機能でもあります。GUIが無い場合でも、外部のエディターからパラメーターを操作できる「エディットコントローラー」(これはVST3の用語です)の機能があれば、DAWプラグインのパラメーター方法をもとに自前でパラメーター操作のUIを用意できるためです。とはいえ、それでもプラグインはユーザーが操作しやすいUIを提供するのが一般的です。

プラグインフォーマットで専用のGUIフレームワークを提供することは多くありません。汎用プラグインフォーマットでは皆無に近いでしょう。プラグインフォーマットで専用のGUIフレームワークを提供するということは、GUIはその仕組みの上に則って構築するということになります。しかし、一般的にはDAWが利用する言語・開発環境は決め打ちにできないので、プラグインGUIはそのGUIと密結合できません。GUIフレームワークを提供しないプラグインフォーマットにできることは、せいぜいGUI操作においてホストとなるDAWとその小窓で出現するプラグインの間で生じる(その必要がある)インタラクションを、呼び出しや通知コールバックのかたちで定義するくらいです。

GUIフレームワークを開発するというのは大規模な作業になりうるもので、実際大規模な作業を経ずに基本機能だけで済ませたGUIフレームワークではさまざまな問題が噴出します。日本人向けにわかりやすい例を挙げれば、日本語入力にまともに対応できないことが多いです。アクセシビリティ対応、HiDPI対応、マルチプラットフォーム対応など、さまざまな難題があるのです。SteinbergはVSTGUIというオーディオプラグイン向けの汎用フレームワーク(そう、VST専用ではないのです)を作りましたが、やはりデスクトップ向けの一般的なGUIフレームワークと比べたらさまざまな点で妥協の産物です(たとえば2022年現在でもCairoが使われていたり)。

プラグインUI開発に最適な銀の弾丸は存在せず、プラグイン開発者は、自分のプラグインの最適解に近い任意のGUIフレームワークを利用する、という以上の一般化はできないといえます。

オーディオプラグインのオーディオ処理はリアルタイムで完了する必要があります。このリアルタイムとは「必ず一定の時間以内に完了する」というものであり、よくhard realtimeともいわれるものです。一方でGUI処理には一般的に「UIスレッドで動作しなければならない」という制約があります。必然的に、オーディオ処理とGUI処理は別々のスレッドで動作することになります。

さて、一般的にプラグインのオーディオ処理とGUIは別々のスレッドで別々の処理ループによって動作することになりますが、プラグインフォーマットによってはGUIの分離がスレッドの分離より一層強く設計されていることがあります。LV2はこの意味では分離アプローチの最右翼で、UIのためのライブラリを別ファイル上で実装して、オーディオ処理部分とはコードを共有できないようにしています。オーディオ処理のコードを参照しなくても、TTLメタデータの情報をもとにUIを実装することが可能であるためです。もちろんそうはいっても、UIのライブラリを参照してそのAPIを利用するコードを書くのを妨げることはできません。

GUIサポートをクロスプラットフォームで一般化するのは、可能ではありますが、技術的にいくつかのアプローチがあり、これがまた一つ難しい要因です。プラグインフォーマットとして何か1つを規定しないわけにはいきません。

  • VST3ではプラグインIEditController::createView()からIPlugViewというインターフェースの実体としてプラットフォーム別のViewを生成して、それをホストに返します。ホストはGUIのViewを(一般的には)自前のウィンドウにreparentして使うことになります。
  • CLAPではホストがclap_plugin_gui_t.create()を呼び出すとプラグインが内部的にGUIを生成しますが、結果はboolでしか帰ってきません。それをホスト側のGUIに統合するには、reparentするウィンドウのハンドルをclap_plugin_gui_t.set_parent()で渡す必要があります。あるいはfloating windowとして扱うという選択肢もありますが、プラグインがサポートしていなければこれは利用できません。clap-juce-extensionsで(つまりJUCEで)構築したプラグインだとfloatingには対応していません。

CLAPでは、LV2のようなUIとDSPのコード分離ポリシーをAPIとして強制してはいません。これは意図的な設計であるとコミュニティでは説明されています。コードをどのように分離するかは各アプリケーションのアーキテクチャ次第ともいえます。

ホストから提供される「楽曲の」情報

オーディオプラグインは基本的にオーディオ処理関数(CLAPのprocess()関数など)に渡されるオーディオ入力やイベント入力をもとにオーディオ・イベント出力を出力するリアルタイムな処理であり、渡される時間情報は基本的にSMPTEに基づく時間(マイクロ秒など)の即値あるいはそれを変換したサンプル数となります。そこにテンポや拍子(time signature)に関する情報は一般的には不要ですが、プラグインによっては、テンポ等の値をもとに生成する音声やMIDIイベントを調整したいことがありえます。これを実現するためには、DAWからの情報提供機能が不可欠です。この情報はトランスポートとかプレイバックと呼ばれることがあります。各プラグインフォーマットでは、それぞれ次に示す型で実現しています。

  • VST3: ProcessContext
  • LV2: Time拡張機能
  • CLAP: clap_event_transport (events.h)

clap_event_transportは拡張ではなくオーディオ処理で渡されるイベントの種類で、トランスポート情報にアップデートがあったときにホストから渡されます。現在の小節位置なども含まれる = 更新の必要が頻繁に生じるので、このイベントをサポートするDAWからはprocess()で送られるclap_process_tin_eventsに含まれることが多いと考えて良いでしょう。

CLAPにはtrack-infoというトラック情報を取得できるAPIもありますが、これはDAW上の表示色など、だいぶ性質の異なる情報を取得するためのものです。

CLAPのユニークな拡張機能

複雑なノートイベント

CLAPのイベント機構は、イベントの種類をCLAPイベント、MIDIイベント、MIDI2イベントから選択できます(他のイベント体系も規定できますが、ホストとプラグインの両方が合意するものを規定する必要があります)。CLAPイベントには、ノート関連イベント、パラメーター関連イベント、トランスポート情報更新イベントなどがあります。パラメーターについては次の節で説明するとして、ノート関連イベントもCLAPに固有のものがあるので説明します。

CLAPのノート関連イベントは次の4つです:

  • CLAP_EVENT_NOTE_ON: ホストからのノートオン指示
  • CLAP_EVENT_NOTE_OFF: ホストからのノートオフ指示
  • CLAP_EVENT_NOTE_CHOKE: ホストからのノート即時停止指示
  • CLAP_EVENT_NOTE_END: プラグインからのノート完了通知

ノートオンとノートオフはMIDIのものと同様の機能です。MIDI 2.0のノートメッセージとは異なり、アーティキュレーションの指定はできません。

。CHOKEとENDは説明が必要でしょう。まず「チョーク」ですが、これはノートを即座に停止する用途で追加されたイベントです。通常、ノートオフとは「リリース」の開始を意味するものであり、すなわちまだ無音になるまではしばらく時間がかかることを意味します。これに対してチョークが指示されると、そのノートの発音は即座に終了します。これが具体的に用いられる例として、CLAP仕様では(1)ドラムマシンのハイハットなど排他的に発音するノートや(2)MIDIにおけるall notes offのような命令として利用することを想定しています。MIDI 2.0であればノートオフのアーティキュレーションに「これはチョークである」という情報を追加することができたでしょうが、CLAPにはそのようなフィールドが存在しないので、こういう命令を追加するしかなくなります。

「エンド」のほうは、ホストから指示する命令ではなく、プラグイン側からホストへの通知で、リリース処理なども終わって完全にノートが無音になったときに送られます。ホストは、もしこれを受け取ってプラグインが何の音も生成していないことが分かったら、プラグインの処理をスキップするなどの最適化が可能になります。これはtail-length拡張によって実現できていた機能と、目的が類似しています。

パラメーター設定関連イベント

CLAPのパラメーター設定イベントもある程度バリエーションがあります:

  • CLAP_EVENT_PARAM_VALUE: 単純なパラメーターの設定
  • CLAP_EVENT_PARAM_MOD: パラメーターのモジュレーション操作(変化率を指定): 開発チームが "non-destructive automation" と呼んでいるもので、モジュレーションが完了したらパラメーターの値を元に戻せる(オートメーションをかけ終わった後に当初のパラメーター設定がなくならない)ことになります
  • CLAP_EVENT_PARAM_GESTURE_BEGIN, CLAP_EVENT_PARAM_GESTURE_END: ユーザーがDAW上のツマミなどでパラメーター操作を開始したことをプラグインに通知するイベント: この間に呼び出されたパラメーター変更イベントは履歴の記録などで厳密にトラッキングする必要がない、と考えられます

モジュレーションとジェスチャーは、表現力を高めるためのものではなく、DAWを利用するときのUXを改善するためのものといえます。(他の規格にも同様の機能を実現するものがあるかもしれません。)

また、パラメーターではありませんが、ノートエクスプレッションもCLAP_EVENT_NOTE_EXPRESSIONで設定できます。対象パラメーターの代わりに以下のいずれかを「エクスプレッションID」として指定します:

enum {  
  // with 0 < x <= 4, plain = 20 * log(x)  
  CLAP_NOTE_EXPRESSION_VOLUME,  
  // pan, 0 left, 0.5 center, 1 right  
  CLAP_NOTE_EXPRESSION_PAN,  
  // relative tuning in semitone, from -120 to +120  
  CLAP_NOTE_EXPRESSION_TUNING,    
  // 0..1  
  CLAP_NOTE_EXPRESSION_VIBRATO,  
  CLAP_NOTE_EXPRESSION_EXPRESSION,  
  CLAP_NOTE_EXPRESSION_BRIGHTNESS,  
  CLAP_NOTE_EXPRESSION_PRESSURE,  
};

ボイス(発音)数の管理

まだ1.0正式仕様には含まれていませんが、CLAPにはプラグインの発音数を管理できるvoice-infoという拡張機能があります。これが使えると、ホストでプラグインの現在の発音総数や最大発音数を取得できます。といっても。出来ることで音声処理に影響があるとは考えられません(雰囲気でパフォーマンスのある種の指標を得られるといったところでしょうか)。

enum {  
CLAP_VOICE_INFO_SUPPORTS_OVERLAPPING_NOTES = 1 << 0,  
};    
typedef struct clap_voice_info {  
  uint32_t voice_count;  
  uint32_t voice_capacity;    
  uint64_t flags;
} clap_voice_info_t;
typedef struct clap_plugin_voice_info {  
  bool (*get)(const clap_plugin_t *plugin, clap_voice_info_t *info);  
} clap_plugin_voice_info_t;

リアルタイム並列処理の制御 (thread_pool拡張)

u-heで頻繁に主張しているCLAPのアドバンテージのひとつが「ホストによって制御されるスレッドプール」です。これについて筆者は「スレッドプールはLV2 Workerなどでも実装されているし、さすがにそれはおかしいんじゃないか」と思ってだいぶコミュニティで掘り下げて議論して分かったのですが、結論からいえば一般的な意味でのスレッドプールでは全くありませんプラグインが非同期実行を実現するための仕組みではありません。

では何なのかというと、CLAPのthread_pool拡張のAPIは、リアルタイム処理を並列で実行するためのAPIです。プラグインがオーディオスレッドで動作しているprocess()の中からホストの機能を呼び出すかたちで利用します。次のような流れになります:

  • プラグインclap_host_thread_pool_t型のホスト拡張をclap_host_t.get_extension()で取得し、これがnullptrなら並列処理ではなく逐次処理を行う
  • clap_host_thread_pool_tを取得できたら、プラグインは続けてrequest_exec(host, numTasks)メンバーを呼び出す
  • ホストのrequest_exec(host, numTasks)の実装では、もし現在そのホストが指定されたnumTasks本のタスクをOpenMPなどの並列実行機構を用いた並列化を試みる
    • できないようなら、それ以上は何も実行せずにfalseを返す
    • 並列化できるようなら、そのプラグインclap_plugin_thread_pool_t型の拡張機能clap_plugin_t.get_extension()で取得する。これがnullptrならfalseを返す
    • clap_plugin_thread_pool_tを取得できたら、ホストは続けてそのexec(plugin, task_index)numTasks回呼び出し、request_exec()の戻り値としてtrueを返す

exec()で呼び出されるプラグインのタスクは、process()のサイクルで完了しなければならないものなので、並列であれ逐次であれ、処理全体をリアルタイムで実行完了しなければなりません。

CLAPのthread_pool拡張とは、こういった機能を実現するためのものです。一般的な意味でのスレッドプールのAPIはありません。一般的なスレッドプールのAPIであれば、タスク/ジョブのオブジェクトを生成してハンドルを渡すようなAPIになっていないと意味を為さないところですが、CLAPの場合はnumTasksという並列実行スロットの本数を渡すのみで、プラグイン側のタスクの呼び出しも同期的です。「thread poolという名前がおかしい」というのは概ねコミュニティにおける共通理解だと思ってよさそうです。

tuning

tuningはmicrotonal(微分音)を実現するための拡張機能です。この機能がオーディオプラグインフォーマットの一部として規定されるのは珍しいといえるでしょう。一般的に、これが拡張機能として規定されないのは、MIDI 1.0に基づくMMAの仕様としてMTS (MIDI Tuning Standards)というものがあって、DAWはこれに沿ってMIDIメッセージを送信し、プラグインはこれを受け取ったらその内容に応じた周波数変換テーブルを適用すれば良いので、独自にイベントを規定する必要がなかったためです。

CLAPの場合、MIDIイベントではなくCLAPイベントで全てを処理するユースケースに対応することを考えると、MTSに相当するメッセージを規定する必要があるといえるでしょう。tuning.hにはclap_event_tuningというMTS相当のイベントが規定されています。

関連情報

あんまし宣伝エントリにしたくないのですが、多分CLAPの位置付け等を理解するうえでそれなりに参考になると思うので並べておきます。

DAWシーケンサーエンジンを支える技術(第2版)」では、DAWがどうやってプラグインを利用するのか、オーディオプラグインはざっくりどういう仕組みとして作られているのか、楽曲のシーケンスはどう作られて保存されているのか、といった話をふわっと書いています(「ふわっと」というのは主観的な表現ですが、コードが出てこない程度の抽象論に終始する内容です)。

https://xamaritans.booth.pm/items/2397203

「LV2オーディオプラグイン開発者ガイド」は日本語でまとまった情報が多くないLV2オーディオプラグインについて解説し、プラグインの開発方法を説明しています。プラグイン開発に携わっていないと難しく、プラグイン開発をやっていると物足りないレベルかもしれません。

https://xamaritans.booth.pm/items/2394242

最後になりましたが、7/6に開催した勉強会のスライドを公開しておきます。内容はこの連載と合わせて作成していたため、かぶる項目が多いと思います。

speakerdeck.com