VST3SDK/VSTGUIのメッセージ機能実装例です。
はじめに
VST3プラグインでは、信号処理用のProcessorとパラメータ管理・UI管理用のControllerは別コンポーネントとなっており、両者の間でデータをやりとりするときはParamChangesまたはMessageを使うことになっています。
ParamChangesの仕組みは実装例が豊富であるものの、データ形式は基本的に0.0~1.0の正規化された実数だけであり、整数、配列、文字列といったデータは扱いづらいです。
一方でMessageの実装例は少なく、特にProcessor側からControllerへ伝えるシンプルな例は現状ほとんどありません。
オーディオ信号やMIDIイベントはProcessorのprocess()関数で検知します。これらを画面表示するためにはControllerへ渡す必要があります。ただしprocess()関数はリアルタイムスレッドで動いており、このスレッドで時間のかかるメッセージ送信処理は禁止されていて、代わりにタイマースレッドを使うようドキュメントには書いてあります。
FAQ - How should I communicate between the 'Processing' and the 'User Interface'?
Communication between the components - Private communication
そこでシンプルなサンプルプログラムを書いてみました。タイマースレッドでProcessorからControllerメッセージ送信するVST3/VSTGUIプラグイン実装例です。
ソースコード一式
プラグイン仕様
- VST InstrumentプラグインとしてDAWから起動できる
- MIDIキーボードやピアノロールからノートオンを受信すると、押されたノート番号をプラグイン画面に表示する
- 複数の鍵盤を押した場合、一番高い音のノート番号のみを表示する。
データ送信~表示の仕組み
- EditorクラスではUI更新用に定期的なタイマーイベントが動いている。ここでProcessorに毎回空のメッセージを送信する
- ProcessorはNoteOn/NoteOffイベントのたびに各鍵盤の状態を配列に記憶しておく
- メッセージを受信したProcessorは、鍵盤状態に変更があれば鍵盤状態配列をメッセージデータとしてControllerに返信する
- Controllerはノート番号表示用パラメータクラスに、押されている鍵盤のうち最も高い音のノート番号を渡す
- ノート番号表示用パラメータクラスは自身の状態を変更すると自動的に画面に反映される
Editorクラス
コンストラクタでタイマー間隔を設定する。
MyEditor::MyEditor(Steinberg::Vst::EditController* controller,
UTF8StringPtr templateName, UTF8StringPtr xmlFile)
: VST3Editor(controller, templateName, xmlFile)
{
// タイマー間隔を10msecに変更する(デフォルトは100msec)
setIdleRate(10);
}
Editorクラスのnotify()が10msec間隔で呼ばれるので、そこでメッセージを作成しControllerのsendMessage()を使って送信する。
ControllerのsendMessage()は対になるProcessorのnotify()に伝わり、ProcessorのsendMessage()は対になるControllerのnotify()に伝わる仕組みになっている。
CMessageResult MyEditor::notify(CBaseObject* sender, const char* message)
{
// 10msec毎に実行される処理
if (message == CVSTGUITimer::kMsgTimer)
{
// 送信用のメッセージを作成
auto message = controller->allocateMessage();
// IDは自由なので、今回は"timer"とした
message->setMessageID("timer");
// 特に送信するデータはないためデータをセットする処理は不要
// ControllerのsendMessageを呼ぶとProcessorに通知される
controller->sendMessage(message);
// メッセージのメモリを解放
message->release();
}
return VST3Editor::notify(sender, message);
}
Processorクラス
process()でMIDI NoteOn/NoteOffイベントを検知して鍵盤状態をスレッドセーフな配列に記憶する。process()はリアルタイムスレッドで実行される。
Vst::Event event;
if (events->getEvent(i, event) == kResultOk)
{
switch (event.type)
{
// Note Onイベント
case Vst::Event::kNoteOnEvent:
if (event.noteOn.velocity == 0.f)
{
// ベロシティ0のNoteOnはNoteOff扱い
notestat[event.noteOn.pitch].store(0);
}
else
{
// NoteOnはステータスを1にする
notestat[event.noteOn.pitch].store(1);
}
// 変更フラグを立てる
changed.store(true);
break;
// Note Offイベント
case Vst::Event::kNoteOffEvent:
// NoteOffはステータスを0にする
notestat[event.noteOff.pitch].store(0);
// 変更フラグを立てる
changed.store(true);
break;
}
}
Editorからのメッセージを受信するnotify()。Editorのタイマーイベントから呼ばれるのでタイマースレッドで実行される。
メッセージを受信したら、あらたなメッセージを生成し、鍵盤状態の配列をメッセージに格納してsendMessage()でControllerへ返信する。
tresult PLUGIN_API Processor::notify(Vst::IMessage* message)
{
auto msgID = message->getMessageID();
// Editorのタイマーイベントから(Controller経由で)通知されたときの処理
// process()とは別スレッドで実行される
if (strcmp(msgID, "timer") == 0)
{
// データに変更があるときのみ実行
if (changed.load())
{
// 変更フラグを元に戻す
changed.store(false);
// 送信用のメッセージを作成
auto res = allocateMessage();
// IDは自由なので、今回は"notestat"とした
res->setMessageID("notestat");
const uint32 size = 128;
char8 data[size];
// 送信用バッファに配列をコピー
for (uint32 i = 0; i < size; i++)
data[i] = notestat[i].load();
// メッセージにデータをセット
res->getAttributes()->setBinary("data", data, size);
// ProcessorのsendMessageはControllerに通知される
sendMessage(res);
// メッセージのメモリを解放
res->release();
}
return kResultTrue;
}
// "timer"メッセージ以外の処理は親クラスに任せる
return AudioEffect::notify(message);
}
Controllerクラス
initialize()の中でノート番号表示パラメータクラスを追加する
// NoteNumberParameter型のパラメータを追加
noteNumberParam_ = new NoteNumberParameter(STR("NoteNumber"), kNoteNumberId);
parameters.addParameter(noteNumberParam_);
}
createView()の中で、デフォルトのエディタクラスではなくMyEditorクラスを生成する。
// MyEditor型のビューを生成
auto* view = new MyEditor(this, "view", "nteditor.uidesc");
return view;
Processorからのメッセージを受信するnotify()。受信データを加工してノート番号表示パラメータクラスにセットする。
tresult PLUGIN_API Controller::notify(Vst::IMessage* message)
{
auto msgID = message->getMessageID();
// Processorから通知されたときに実行される処理
if (strcmp(msgID, "notestat") == 0)
{
uint32 size;
const void* p;
// データのポインタとサイズを取得
// 今回の例の場合、サイズは128が取得できるはず
message->getAttributes()->getBinary("data", p, size);
// void*型だと扱いにくいのでキャスト
char8* data = (char8*)p;
int noteNumber = -1;
// 配列を上から(高音側)からスキャンして、NoteOnステータスの
// ノートナンバーが見つかったらブレイク
for (int i = size - 1; i >= 0; i--) {
if (data[i] > 0) {
noteNumber = i;
break;
}
}
// パラメータ状態を変更して画面に表示
noteNumberParam_->setNoteNumber(noteNumber);
return kResultTrue;
}
// "notestat"メッセージ以外の処理は親クラスに任せる
return Vst::EditControllerEx1::notify(message);
}
NoteNumberParameterクラス
ノート番号セット関数。メンバ変数noteNumber_に値をセットする。valueNormalizedを変更することで表示が更新される。
void NoteNumberParameter::setNoteNumber(int noteNumber)
{
// データ保持用メンバ変数を更新
noteNumber_ = noteNumber;
// valueNormalizedに変更がないと表示が更新されないので適当に値を変える
setNormalized(1.0 - valueNormalized);
}
表示更新時に呼ばれる表示文字列生成関数。本来表示するnormValueを無視して、代わりにメンバ変数noteNumber_の値を表示するよう文字列を生成する。
void NoteNumberParameter::toString(ParamValue normValue, String128 string) const
{
// データ表示時に呼ばれる処理
// 表示するノートナンバーがない場合、戻り値用変数stringに"-"をセット
if (noteNumber_ < 0) {
// String128型は、長さ128のchar16型文字列を意味する
// そのためchar16用の関数で文字列をコピーする
strcpy16(string, STR("-"));
return;
}
char text[32];
// int型のメンバ変数noteNumber_の値をchar文字列に変換
itoa(noteNumber_, text, 10);
// char文字列をchar16文字列に変換するにはUStringクラスを使うと手っ取り早い
// 戻り値用変数stringにノートナンバー文字列をセット
Steinberg::UString(string, 128).fromAscii(text);
}
参考
Editorクラスのタイマー処理はうつぼかずらさんのページを参考にしました。
「notify返し」方式はengineerさんのこの記事で知りました。ただ、この記事にあるlock-freeは今回の実装例ではきちんと考慮できていないかも。
おわりに
とにかく情報が少ない中、試行錯誤して実現した実装なので、もっと良い方法があれば教えてください。