分離プロセスのオーディオプラグインにおけるStateとPresetsに関する覚書

parameters, states, presets

オーディオプラグインの挙動は、主にパラメーターとそれ以外の状態 (state) に基づいている。GUI上で調整しているとこれらの違いをあまり意識しないが、パラメーターは主に32bit float、使い方によっては64bit floatひとつで設定するもので、それ以外の状態のデータ型や形式はプラグイン次第だ。これは一般的なオーディオプラグインフォーマットに共通する挙動といってよい。大抵は拡張機能の方式で規定されている。

32bit float(あるいは64bit float、面倒なので以降は言及しない)と決まっているパラメーターは、DAWによって、オートメーションで処理できたり、演奏処理中に動的に変更できることが多い。それ以外の状態を演奏中に動的にプログラマブルに変更することは、理論上は可能だが、状態の変更は多くの場合は全パラメーターの変更などを伴い、場合によってはデータファイルのロードも伴い、リアルタイムで処理できない程度に重いことが多いので、一般的ではない。

状態は基本的にユーザーがGUI上で(GUIが機能を提供する範囲で)自由に設定でき、セーブ・ロードが可能で、この中にはパラメーターの現在の値なども(一般的には)含まれる。これに対して、プラグイン側が最初から実用的な楽器・エフェクトの「状態」をプリセットとして提供していることがある。言い換えれば、プリセット機能は一般的には状態の機能の応用として提供されている。

VST3の場合、プリセットはvstpreset等のファイルに保存されるが、これは状態と同じデータを保存しているようだ。LV2の場合、プリセットはLV2で幅広く用いられるRDF Turtleのデータ形式で共有されることが多いが、Pluginオブジェクトにそのまま反映できるようにPluginオブジェクトに類する構造として保存されるようだ(これによってControlPortの内容などをそのまま適用できる)。

stateのセーブとロード

プラグインの状態の保存処理は、DAWで音楽を打ち込んで保存するとき、各トラックにおける各プラグインについて発生する。逆に楽曲をロードすると、それらのプラグインの状態が復元されることになるが、フリーズしたトラックにおけるプラグインなどを考慮すると、すべてがロード時に復元するとは限らない。

GUIの表示・非表示の切替時には、状態が変更されるわけではないが、プラグインの実装次第ではGUI表示処理から何らかのパラメーター変更処理が走る可能性はゼロではない(バグの可能性も高い)。パラメーターとは異なり、通常はホストから状態が直接操作されることはないが、プリセットの操作というかたちではホストから状態が操作されることがある。

また、DAWの操作次第では、プラグインが「複製」されることがある(たとえばトラックを複製した場合)。この場合、状態も全てプラグインの新しいインスタンスにコピーされることになる。

stateデータの内容

どのプラグインフォーマットでも、状態を保存した結果はバイナリblobになる。この内容が、もう少し具体的なセマンティクスに基づく、型情報や文脈のあるデータの集合体になっていることもある。

LV2のState拡張では、状態の全体をsave関数で保存しrestore関数でロードすることになっているが、その中では状態の各要素をkey-valueストアのようにkey、size、typeを明示してstore / retrieveすることになっている。型としてはLV2 Atomの使用が想定されている。LV2 AtomであればLV2の標準メタデータフォーマットであるRDF Turtleファイル(.ttlなど)で読み書きでき、プラグインが提供するプリセットのデータとしても有用だ。

AudioUnitにおける状態管理も似ていて、AUAudioUnitならfullStateあるいはfullStateForDocumentで、全プロパティと全パラメーターの情報が格納されることになる。それなりの大きさになることが想定されるので、たとえば分離プロセスで動作するプラグインの場合に、IPCのメッセージにこのバイナリを格納して送受信するというのは筋が悪い。ホストとプラグインの間に共有メモリのチャンネルを用意しておいてデータ本体はそこで受け渡し、IPCでは制御命令のみを送受信するというアプローチが妥当だろう。

(モバイルプラットフォームにおけるプラグインフォーマットの場合、そもそもプラグインは巨大なメモリ空間を占有すべきではなく、したがってStateデータも小さく抑えられるべきで、場合によっては共有メモリ上でStateを全展開するのはふさわしくないかもしれない。いずれにせよIPCでやり取りするのは適切ではない。)

筆者が開発しているAAPでは、stateデータのフォーマットをMIDI 2.0 UMPのストリームとしてしまって、パラメーターの状態はそのためのSysEx8メッセージ(MIDIメッセージとしてプラグインにリアルタイム送信するために別途規定してある)や、あるいはデフォルトでパラメーター番号に対応しているCCやAssignable Controller (NRPN) で保存し、それ以外のバイナリはSysEx8やMDSのblobとしてで保存してプラグインが自分で復元する、というやり方を計画している(すでにオーディオ処理で用いられているフォーマットなので実行に移すのは簡単だけど、破壊的変更になるのでバージョンを上げる機会を待っている)。Stateの内容をどの程度セマンティックにするかはプラグイン開発者次第だ。

YAMAHA DX7ではMIDI標準のSysExで音色パラメーターをダンプして復元できた。現代でも多くのエミュレーターがそれらを含むCartデータファイルをサポートしている。汎用的なメッセージフォーマットでデータのやり取りが可能になれば、将来的な再利用性も高まる。

Stateのロード方式とフォーマット

LV2のStateで使われるLV2_State_Flagsは面白い。Stateを保存するのは、データの永続化が目的である場合と、DAWプラグインのプロセス間あるいはプラグイン本体とUIの間で受け渡しする場合で異なり、また永続化する場合もきちんとクロスプラットフォームで読み書きできる形式とそうでないPOD (plain old data) の場合とがありうる。

CLAPの場合、state拡張には特徴的なものが何もなく、単純にバイナリを保存するだけになるが、LV2_State_Flagsに類似するstate-contextという拡張がdraft状態で存在している。ほぼstate拡張の代替として利用できるものであり、プラグインインスタンスの「複製」の際に使う場合や、プリセットの受け渡しのために使う場合は、それぞれ通常の場合とは異なるフラグが渡されることになるようだ。

分離プロセスにおけるStateのロードとパラメーター変更通知

Stateをロードすると、パラメーターの値も変更されることになる。このとき、パラメーターの変更内容がプラグインから(プラグインUIからの変更のようには)通知されないと仮定すると、状態バイナリはホストにとってはblobでしかないので、これを渡されてもホストはパラメーターの変更内容までは知ることができない。そうなると、ホストは明示的にパラメーターの値を取得する必要がある。そうしないと、ホストのUI上は、プリセット選択が名前としては反映されているのに古いパラメーターが残り続けることになるし、その後のオートメーション処理などにも支障をきたすことになる。

プラグインとホストが同一プロセスにある場合、「パラメーターを取得する」のは単にgetParameterValue()のような関数を一つ呼び出すだけで足りるが、別々のプロセスにある場合は、その内容が共有メモリ上にでも無い限り、明示的に「問い合わせ」を行う必要がある。別々のマシンにある場合は、共有メモリというシナリオもあり得ないので、パラメーター取得のリクエストをIPCなどの手段で発行しなければならない。デスクトップのプラグインと同じようにリアルタイムで数百件のIPC呼び出しを指示することはできない(AAPの場合は1回 = 1ブロックのオーディオ処理で1往復のIPC呼び出ししか想定していない)。何らかの最適な手段を見つけなければならない。

ホストからの問い合わせという手段を用いずに、Stateのロードによって生じたパラメーター変更をオーディオ処理の結果として(LV2 Atom output portやMIDI output channnelのようなかたちで)通知するというアプローチもあり得る。ただ、パラメーターが数百も存在するようなプラグインで、パラメーター変更をパラメーターごとに分解して通知するのは、いかにも非効率的だ。ホストの種類によっては、プラグイン上の現在のパラメーターの値が特に意味を持たない場合もあり得る(たとえば単なる音楽プレイヤーでパラメーターの現在値を何も表示しない場合や、オーディオファイルにレンダリングするだけのツールなど)。

解決策のひとつとしては、State拡張機能に「状態をロードしたときはホストとの共有メモリ上にパラメーター値のリストをダンプできる」という逃げ道を設けておく、というのが考えられる。「できる」と書いているけど、パラメーター数がごく少ないプラグイン以外では、実質的に「要件」となるだろう。これなら、ホストがプラグインからStateのロード完了通知(これ自体はいつ完了するかわからないロード処理の最後に必要)を受け取ったときに、パラメーター値のリクエストを大量に発行する必要はなくなる。バイナリサイズの文脈で言及したような共有メモリのチャンネルが存在していることが前提になる。

IPCを前提としたプラグインフォーマットにおいて、このような「要件」を設けるのは「問題」ではないが、全プラグイン開発者にこれを手動で実装してもらうのは筋が悪いので、AudioUnit SDKfullStateのようにデフォルト実装としてこれらの面倒を見る標準ライブラリが存在していたほうが良さそうだ。

このレイヤーの課題を解決することによって、ようやくjuce::HostedAudioProcessorParameterのような存在をobservableにできる(addListener()が実現可能になる)。