LoginSignup
2
1

VST3プラグインでProcessorからControllerへバイト列メッセージを送る例

Last updated at Posted at 2023-05-31

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

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キーボードやピアノロールからノートオンを受信すると、押されたノート番号をプラグイン画面に表示する
  • 複数の鍵盤を押した場合、一番高い音のノート番号のみを表示する。
    nt.png

データ送信~表示の仕組み

  1. EditorクラスではUI更新用に定期的なタイマーイベントが動いている。ここでProcessorに毎回空のメッセージを送信する
  2. ProcessorはNoteOn/NoteOffイベントのたびに各鍵盤の状態を配列に記憶しておく
  3. メッセージを受信したProcessorは、鍵盤状態に変更があれば鍵盤状態配列をメッセージデータとしてControllerに返信する
  4. Controllerはノート番号表示用パラメータクラスに、押されている鍵盤のうち最も高い音のノート番号を渡す
  5. ノート番号表示用パラメータクラスは自身の状態を変更すると自動的に画面に反映される
    notify.png

Editorクラス

コンストラクタでタイマー間隔を設定する。

MyEditor.cpp
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()に伝わる仕組みになっている。

MyEditor.cpp
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()はリアルタイムスレッドで実行される。

Processor.cpp
      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へ返信する。

Processor.cpp
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()の中でノート番号表示パラメータクラスを追加する

Controller.cpp
  // NoteNumberParameter型のパラメータを追加
  noteNumberParam_ = new NoteNumberParameter(STR("NoteNumber"), kNoteNumberId);
  parameters.addParameter(noteNumberParam_);
}

createView()の中で、デフォルトのエディタクラスではなくMyEditorクラスを生成する。

Controller.cpp
    // MyEditor型のビューを生成
    auto* view = new MyEditor(this, "view", "nteditor.uidesc");
    return view;

Processorからのメッセージを受信するnotify()。受信データを加工してノート番号表示パラメータクラスにセットする。

Controller.cpp
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を変更することで表示が更新される。

NoteNumberParameter.cpp
void NoteNumberParameter::setNoteNumber(int noteNumber)
{
  // データ保持用メンバ変数を更新
  noteNumber_ = noteNumber;
  // valueNormalizedに変更がないと表示が更新されないので適当に値を変える
  setNormalized(1.0 - valueNormalized);
}

表示更新時に呼ばれる表示文字列生成関数。本来表示するnormValueを無視して、代わりにメンバ変数noteNumber_の値を表示するよう文字列を生成する。

NoteNumberParameter.cpp
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は今回の実装例ではきちんと考慮できていないかも。

おわりに

とにかく情報が少ない中、試行錯誤して実現した実装なので、もっと良い方法があれば教えてください。

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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1