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

[VST3制作の過去記事一覧はこちら](https://qiita.com/aike@github/items/05a14911bb89db8b6c2c)

VST3SDK/VSTGUIのメッセージ機能実装例です。

## はじめに

VST3プラグインでは、信号処理用のProcessorとパラメータ管理・UI管理用のControllerは別コンポーネントとなっており、両者の間でデータをやりとりするときは[ParamChanges](https://steinbergmedia.github.io/vst3_doc/vstsdk/classSteinberg_1_1Vst_1_1ParamChanges.html)または[Message](https://steinbergmedia.github.io/vst3_doc/vstinterfaces/classSteinberg_1_1Vst_1_1IMessage.html)を使うことになっています。
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'?](https://steinbergmedia.github.io/vst3_dev_portal/pages/FAQ/Communication.html#q-how-should-i-communicate-between-the-processing-and-the-user-interface)
[Communication between the components - Private communication](https://steinbergmedia.github.io/vst3_dev_portal/pages/Technical+Documentation/API+Documentation/Index.html#private-communication)

そこでシンプルなサンプルプログラムを書いてみました。タイマースレッドでProcessorからControllerメッセージ送信するVST3/VSTGUIプラグイン実装例です。

## ソースコード一式

https://github.com/aike/VST3NotifyExample

## プラグイン仕様

- VST InstrumentプラグインとしてDAWから起動できる
- MIDIキーボードやピアノロールからノートオンを受信すると、押されたノート番号をプラグイン画面に表示する
- 複数の鍵盤を押した場合、一番高い音のノート番号のみを表示する。
![nt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22455/83f9132b-8816-80f8-035a-e57fea1bd3b2.png)

## データ送信～表示の仕組み

1. EditorクラスではUI更新用に定期的なタイマーイベントが動いている。ここでProcessorに毎回空のメッセージを送信する
2. ProcessorはNoteOn/NoteOffイベントのたびに各鍵盤の状態を配列に記憶しておく
3. メッセージを受信したProcessorは、鍵盤状態に変更があれば鍵盤状態配列をメッセージデータとしてControllerに返信する
4. Controllerはノート番号表示用パラメータクラスに、押されている鍵盤のうち最も高い音のノート番号を渡す
5. ノート番号表示用パラメータクラスは自身の状態を変更すると自動的に画面に反映される
![notify.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22455/53c9815e-0527-c098-d176-a4ff5a28c480.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クラスのタイマー処理はうつぼかずらさんのページを参考にしました。

https://www.utsbox.com/?p=2051

「notify返し」方式はengineerさんのこの記事で知りました。ただ、この記事にあるlock-freeは今回の実装例ではきちんと考慮できていないかも。

https://qiita.com/engineer/items/bf637b588d5f492c7c5d#%E5%95%8F%E9%A1%8C

## おわりに

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

[VST3制作の過去記事一覧はこちら](https://qiita.com/aike@github/items/05a14911bb89db8b6c2c)