Help us understand the problem. What is going on with this article?

VST3の用語について その2

VST3制作の過去記事一覧はこちら

音楽制作用DAWのプラグイン仕様VST3についてインターネットのヤホーで調べました。今回はVST SDKで使われる用語について整理してみる第2回目で、VST3になって非常に複雑になったパラメータ管理まわりの用語説明となります。
この記事はVST3プラグインを書く人向けの内容です。

素朴なパラメータ管理

VSTプラグインは、ホストからプラグインの状態を吸い上げたり、状態を復元したりできるので、プラグイン側がパラメータのファイル入出力を管理しないことも多いです。
ただ、状態をファイルに保存したものをファクトリープリセットとして提供することは有用なので、そのあたりはSDKとしてもサポートしています。

PresetFile

プラグインの状態をファイル入出力するクラス。主にホスト側が利用する機能のため、プラグイン側の公式サンプルコードではほとんど使用方法を見ることができません。

ファクトリープリセットを持つプラグインは、C:\ProgramData\VST3 Presets\などにファイルを置くことでホストに認識されます1。ただし手元のプラグインではこの形式のファクトリープリセットはほとんど使われていないようで、唯一Steinberg製オーディオインタフェースUR242に付属のリバーブエフェクトREV-Xが使っていました。

フォルダのプリセットファイル
preset_3.png
プリセット一覧表示
preset_1.png
個別のプリセットファイル読み書き
preset_2.png

Parameter

プラグインのノブやスイッチの状態はParameterとしてエクスポートしてホストが管理します。Parameterクラスは、パラメータの値だけでなく、表示名、単位(dBなど)、デフォルト値、連続値か離散値かリストかなどの情報(ParameterInfo)をメンバとして持ちます。
Parameterは、ホスト側のオートメーションやプラグインのGUI操作で状態が変更される可能性があるので、プラグインがエクスポートしてホストと共有します。

ParameterContainer

文字通りParameterのコンテナです。Parameterはノブ1個やスイッチ1個に対応しており単一の状態しか扱えないので、複数のコントローラを含むプラグイン全体の状態はParameterContainerで管理します。

ParameterやParameterContainerは主にホスト側の管理なので、プラグイン側はEditControllerのinitialize()メソッド内でparameters.addParameter()としてエクスポートするくらいです。

複雑なパラメータ管理

ProgramList

さて、もうすこし複雑なプラグインになるとパラメータの数も増えてきます。またプラグイン上にプリセット一覧を表示して選択できるようにしたいこともあります。
特にサンプリング音源のような読み込みに時間がかかるものは、あらかじめ複数のデータを読み込んでおいてプラグイン内でオンメモリで切り替え操作をするのが有効なケースもあります。そのような目的で用意されているのがProgramListです。2
ProgramListはプラグインに必須というわけではなく、実際シンプルなプラグイン用のEditControllerクラスでは利用していません。それより複雑なプラグイン用のEditControllerEx1クラスではProgramListを持つように拡張してあります。

ProgramListは昔のシンセでいうバンクの概念に近いかもしれません。バンク1に32個の音色が入っていて、バンク2にまた異なる32個の音色が入っているイメージです。ひとつのバンクがひとつのProgramListに相当します。

ProgramListはVST3SDKの中でもけっこうクセがあるクラスで取り扱いを理解するには注意が必要です。

ProgramListで扱うデータは一種のプリセットですが、Presetという用語は使われておらずProgramと呼ばれています。ただし、Programクラスがあるわけではないのがめちゃくちゃわかりづらいです。

ProgramListの構造はプログラムに関して、
・各プログラムの表示名
・各プログラムの属性値
・各プログラムのピッチネーム
をそれぞれ別のリストとして持っています。属性値とピッチネームはリストのリストです。
また、現在選択されているプログラムのIndexと、ProgramList自身の情報(ProgramListInfo)も持っています。
programlist2.png

それぞれについて説明します。

表示名

各プログラムの表示名は、programNamesというリストで持っています。
addProgram()、getProgramName()、setProgramName()メソッドでアクセスします。
addProgram()は名前からするとひとつのプログラムに関する情報をまるごとProgramListに追加するように思えますが、引数が文字列ひとつということからわかるとおりプログラム名をリストに追加するだけです。めちゃくちゃわかりづらいです(2回目)。

属性値

各プログラムの属性値はProgramInfoと呼ばれ、getProgramInfo()、setProgramInfo()でアクセスできます。
属性値はKey-Value Storeのようになっていて、KeyとしてはSteinberg::Vst::PresetAttributesであらかじめ定義されているものだけが使えるようです。
たとえば、Keyが"MusicalInstrument"ならValueは"Piano"だったり、Keyが"MusicalStyle"ならValueは"Jazz"のような例が挙げられています。表示時にタグのように分類・マスクして使用することを想定しているのだと思います。
属性値のリストに階層構造はありませんが、Valueに"Piano | Acoustic Piano"のように設定することで階層構造を表現することもできるとドキュメントに書かれています。もちろんホスト側で属性文字列を解析して階層表示する機能を実装すればできるという話です。

ピッチネーム

各プログラムのピッチネームは、ドラム音源などに使用する目的で音程ごとに名前を付けるものです。C2が"Bass Drum 1"でD2が"Acoustic Snare"みたいなやつですね。サンプラー音源で音域によってチェロの音だったりビオラの音だったりするのにも使えそうです。
ピッチネームは必須ではないので持っていないProgramListもあり、hasPitchNames()で持っているかどうかをチェックできます。

選択プログラムのIndex

選択されているプログラムのIndexは、とてもわかりづらいですがparameterというメンバ変数で持っています。
プラグインのパラメータ用Parameterクラスがリストの選択値を扱えるため、ProgramListのプログラム選択値もこのクラスを流用した実装になっていて、このような名前になっています。これによりAudioProcessor::process()で他のパラメータ変更通知と同じようにプログラムチェンジ情報を扱えるというメリットもあります。
選択されているプログラムのIndexを取得するメソッドはgetParameter()です。めちゃくちゃわかりづらいです(3回目)。

プログラムのパラメータ情報

ところで、ここまで読んで気づいたかもしれませんがProgramListは各プログラムで設定されるプラグインのパラメータ情報を持っていません。
たとえば各プログラムの表示名が"BRASS 1"、"BRASS 2"、"BRASS 3"、"STRINGS 1"……、となっていたとして、STRINGS 1を選択したときのADSRの値やフィルターの値といったパラメータを管理しているわけではないのです。

ProgramListのIndexと実際のパラメータ一覧をひもづけるするのは、IProgramListDataというインタフェースを実装したデータ側の役目です。めちゃくちゃわかりづらいです(4回目)。
以下のようにして、後述するBStream型のコンテナをProgramListのID、Indexとひもづけます。

programListData->setProgramData(programListID, programIndex, &bstream)
programListData->getProgramData(programListID, programIndex, &bstream)

IProgramListDataはアクセス仕様を定義しただけのインタフェースであり、実際に使える実装例はVST3SDKとして提供されていません。

Unit

前回も出てきたユニットです。マルチエフェクターのようなプラグインでパラメータをグループ化するのに使います。
通常はUnitIDが0のRootUnitにすべてのパラメータが含まれます。

パラメータを階層構造にしたい場合はプラグイン側で以下のように実装します。

MyEditController.cpp
tresult PLUGIN_API MyEditController::initialize (FUnknown* context)
{
    // 省略

    // ユニットを2個作成
    UnitID fx1UnitID = 100;
    UnitID fx2UnitID = 200;
    Unit* unit1 = new Unit(STR16("Modulation1"), fx1UnitID, kRootUnitId);
    Unit* unit2 = new Unit(STR16("Modulation2"), fx2UnitID, kRootUnitId);
    addUnit(unit1);
    addUnit(unit2);

    // パラメータ登録時に所属するユニットを指定
    parameters.addParameter(STR16("Depth1"), nullptr, 0, 0.5, kCanAutomate, kDepth1ID, fx1UnitID);
    parameters.addParameter(STR16("Rate1"),  nullptr, 0, 0.5, kCanAutomate, kRate1ID,  fx1UnitID);
    parameters.addParameter(STR16("Depth2"), nullptr, 0, 0.5, kCanAutomate, kDepth2ID, fx2UnitID);
    parameters.addParameter(STR16("Rate2"),  nullptr, 0, 0.5, kCanAutomate, kRate2ID,  fx2UnitID);

    return result;
}

これで以下のような構造がホストとの間で共有できます。
unit_tree3.png

Unitコンストラクタの第3引数にkRootUnitId以外のユニットIDを指定すると、入れ子になったさらに深い階層構造も作ることができます。

実際に階層構造で管理してみると、別ユニットであってもパラメータIDはプラグイン全体でユニークにしなければならないので、思っていたほどありがたみはない感じです。(上の例でいうとDepth1とDepth2のIDは重複不可)

各ユニットはProgramListとひもづけることができます。ひもづく先がない場合はkNoProgramListIdが設定されます。
これにより、たとえばコンプレッサーとディレイとリバーブを持つマルチエフェクターで、コンプレッサーユニットとディレイユニットの設定はそのままでリバーブユニットだけ個別にプリセットを読み込むようなことが実現できます。

ひとつのユニットはひとつだけ参照先のProgramList持てますが、複数のユニットで同一のProgramListを参照することもできます。公式ドキュメントの図を見た当初はコンプレッサーが2つあるマルチエフェクターで、コンプ1とコンプ2が同一のプリセット一覧を参照するような使い方かと思いましたが、ProgramListの方で選択されているプログラムの情報を持っているのでそうではなさそうです。アンプシミュレータプラグインで、ストンプとアンプヘッドとキャビネットがあり、ストンプユニットは独立でプリセットを切り替えるけれど、アンプヘッドユニットとキャビネットユニットはプリセットが連動している、とかそういう使い方かもしれません。3 4

unit_and_programlist.png

IBStream

メモリやファイルなどにバイナリデータを入出力するインタフェースで、read(),write(),seek()といったアクセスができます。
以下のようにアクセス時にバイト数を指定して、複数の異なる型のバイナリデータを格納していくことができます。

example.cpp
 bstream.write(floatData, sizeof(float), &writtenBytes);
 bstream.write(int32Data, sizeof(int32), &writtenBytes);

第3引数は実際に書き込まれたバイト数が返ります。0だったら書き込み失敗というような判断ですね。

このインタフェースの実装としては、FileStream5、ReadOnlyBStream6、MemoryStream7、BufferStream8、などがあります。

IBStreamer

IBStreamはそのままでは扱いにくいので、型を明示してアクセスしやすいようにするヘルパークラス。先頭にIがついていますがインタフェースではありません。
readInt16()、writeInt16()、readFloat()、writeFloat()など型を指定した入出力が可能です。
格納時のバイトオーダーを指定できるのでCPUアーキテクチャの差異を吸収する役割も持っています

example.cpp
    IBStreamer streamer(fileStream, kLittleEndian);
    streamer.writeFloat(floatData);
    streamer.writeInt32(in32tData);

これらIBStreamとIBStreamerは、EditController::setComponentState()やAudioProcessor::getState()、setState()で利用するので目にすることも多いと思います。

VST3制作の過去記事一覧はこちら


  1. https://steinbergmedia.github.io/vst3_doc/vstinterfaces/vst3loc.html#presetloc 

  2. https://steinbergmedia.github.io/vst3_doc/vstinterfaces/vst3Presets.html#vst3ProgramLists 

  3. この辺のドキュメントに明記されていない部分の解釈は正しくない可能性もあります 

  4. ProgramListのメンバ変数にも参照元と思われるunitIdがありますが、単一のUnitとだけしかひもづけないのはドキュメントの説明と矛盾しているし、SDKを検索してもほぼ使われておらず謎のメンバ変数です  

  5. データにファイルを使用 

  6. データはコンストラクタで渡す 

  7. データアクセスにmalloc,memcpyなど低レベルメモリ管理を使用 

  8. データに多機能メモリ管理クラスSteinberg::Bufferを使用 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away