VST3プラグインでMIDIメッセージを受信する方法1
前回まででパラメーターの実装や保存など、ひととおりのパラメーターについての説明はさせていただいたと考えております。
【参考】
パラメーター実装方法1 | はじめてのVST3プラグインに基本となるパラメーターを実装する方法を記載します。 |
パラメーター実装方法2 | パラメーター実装方法1で実装したパラメーターは「0.0~1.0」しか表示できないので、 それ以外(例えば「10~100」や文字列のリストなど)を表示するパラメーターを実装する方法を記載します。 |
パラメーター実装方法3 | パラメータークラスを継承して自作のパラメータークラスを作成します。 「フィルタのカットオフ周波数」等で利用するスライダーの位置と出力の関係が線形でないパラメーターを実装する方法を記載します。 |
パラメーターの保存方法 | パラメーター実装方法1~3で追加したパラメーターはホスト(DAWなど)を終了すると初期化されてしまいます。 終了時に保存し、次回起動時にデータが読み込まれるようパラメーターを保存・読込する方法を記載します。 |
ですので、今回はMIDIのノートオン/ノートオフメッセージを受信する方法を記載したいと思います。
今回作成するプラグインはVST3プラグインのパラメーター実装方法で作成したプラグインをベースにします。
(コントロールの実装以外に余計なコードが少ないためです。)
- パラメーターはなし(ただしパラメーター操作クラスは定義します。)
- MIDIのノートオン/ノートオフメッセージに応じて音を鳴らすモノフィックシンセサイザー
- 音色はサイン波のみ。(エンベロープジェネレータ等の機能もなし)
- 入力バス、出力バスは1つで、共にステレオ(2ch)
今回ベースとなるプラグインの説明はこちらに記載しています。
このVST3プラグインのサンプルソースファイルはこちらからダウンロードできます→vst3dev_20210403
ZIPファイルの中の「vst3dev06_MIDIメッセージを受信する方法1」フォルダが今回のサンプルソースファイルになります。
コンパイル・ビルドの方法は簡単にこちらでご説明しております。ご参考までに。→サンプルソースファイルのビルド方法
音声処理クラスの定義
まずは今回作成するVSTはモノフィックシンセサイザーとして動作するため、音声処理クラスにて必要な変数等を下記の通り定義しております。
【processor.h】
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
class MyVSTProcessor : public AudioEffect { protected: ParamValue volume; // オシレータの音量 vector<float> pitchList; // 押されたキーの音程(周波数)を保存する可変長配列 ParamValue theta; // オシレータとして使用するSIN関数の角度θ public: // コンストラクタ MyVSTProcessor(); // クラスを初期化する関数(必須) tresult PLUGIN_API initialize(FUnknown* context); // バス構成を設定する関数。 tresult PLUGIN_API setBusArrangements(SpeakerArrangement* inputs, int32 numIns, SpeakerArrangement* outputs, int32 numOuts); // 音声信号を処理する関数(必須) tresult PLUGIN_API process(ProcessData& data); // 自作関数 // MIDIノートオンイベント、MIDIノートオフイベントを受け取った場合に処理する関数 virtual void onNoteOn (int channel, int note, float velocity); virtual void onNoteOff(int channel, int note, float velocity); // 自作VST Processorクラスのインスタンスを作成するための関数(必須) static FUnknown* createInstance(void*) { return (IAudioProcessor*)new MyVSTProcessor(); } }; |
pitchListは標準C++ライブラリの動的配列クラスであるvectorクラスを使用しています。vectorクラスの説明については割愛します。
また、MIDIノートオン/ノートオフイベントを受け取った際にそれぞれの処理を行うonNoteOn()関数とonNoteOff()関数を定義しております。
これらの関数については後述いたします。
入力イベントバスの追加
MIDIのノートオン/ノートオフメッセージを受け取るためには、入力イベントバスを利用します。
音声処理クラスのinitialize()関数でaddEventInput()関数を呼び出して、入力イベントバスを追加いたします。
また、メンバー変数も適当に初期化しております。
【processor.cpp】
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
tresult PLUGIN_API MyVSTProcessor::initialize(FUnknown* context) { // まず継承元クラスの初期化を実施 tresult result = AudioEffect::initialize(context); if (result == kResultTrue) { // 入力と出力を設定 addAudioOutput(STR16("AudioOutput"), SpeakerArr::kStereo); // 入力のEventBusを追加する addEventInput(STR16("Event Input"), 1); // 以下固有の初期化を実施。 volume = 0.0f; theta = 0.0f; pitchList.clear(); } // 初期化が成功すればkResultTrueを返す。 return result; } |
なお、このVSTでは入力のオーディオバスは必要ありませんので追加しておりません。
- addEventInput()関数
概要 | 音声処理クラスに入力イベントバスを追加します。 | ||
---|---|---|---|
戻り値 | 型 | 概要 | |
EventBus* | 追加したイベントバスへのポインタ | ||
引数 | 型 | 変数名 | 概要 |
const TChar* | name | イベントバスの名前。任意の名前でいい | |
int32 | channels | イベントバスのチャンネル数。省略可。省略した場合は16となる。 | |
BusType | busType | バスのタイプ。kMainもしくはkAuxを指定。省略可。省略した場合はkMainとなる。基本的にkMainでいいはず | |
int32 | flags | イベントバスのフラグ。kDefaultActiveのみ指定可能。省略可。省略した場合はkDefaultActiveとなる。 |
MIDIノートオン/ノートオフイベントの受け取り
MIDIノートオン/ノートオフイベントの受け取りはprocess()関数内で行います。
MIDIノートオン/ノートオフを含めいろいろなイベントがあった場合、引数のProcessData& dataに格納されて渡されます。
渡されたイベントは複数あるため、下記のように分離し、必要なメッセージのみを受け取ります。
(今回は必要なメッセージをMIDIノートオン/ノートオフイベントとします。)
【processor.cpp】
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
tresult PLUGIN_API MyVSTProcessor::process(ProcessData& data) { // パラメーター変更の処理 // 与えられたパラメーターがあるとき、dataのinputParameterChangesに // IParameterChangesクラスへのポインタのアドレスが入る if (data.inputParameterChanges != NULL) { // 与えられたパラメーターの数を取得 int32 paramChangeCount = data.inputParameterChanges->getParameterCount(); // 与えられたパラメーター分、処理を繰り返す。 for (int32 i = 0; i < paramChangeCount; i++) { // パラメーター変更のキューを取得 // (処理するサンプル内に複数のパラメーター変更情報がある可能性があるため、 // キューという形になっている。) IParamValueQueue* queue = data.inputParameterChanges->getParameterData(i); if (queue != NULL) { // どのパラメーターが変更されたか知るため、パラメーターtagを取得 int32 tag = queue->getParameterId(); // 変更された回数を取得 int32 valueChangeCount = queue->getPointCount(); ParamValue value; int32 sampleOffset; // 最後に変更された値を取得 if (queue->getPoint(valueChangeCount - 1, sampleOffset, value) == kResultTrue) { // tagに応じた処理を実施 switch (tag) { // 今回はパラメータがないので何もしない } } } } } // イベントの処理 // 与えられたイベントがあるときdataのinputEventsに // IEventListクラスへのポインタのアドレスが入る IEventList* eventList = data.inputEvents; if (eventList != NULL) { // イベントの数を取得する。 int32 numEvent = eventList->getEventCount(); for (int32 i = 0; i < numEvent; i++) { // i番目のイベントデータを取得する Event event; if (eventList->getEvent(i, event) == kResultOk) { int16 channel; int16 noteNo; float velocity; // イベントデータのタイプごとに振り分け switch (event.type) { case Event::kNoteOnEvent: // ノートオンイベントの場合 channel = event.noteOn.channel; noteNo = event.noteOn.pitch; velocity = event.noteOn.velocity; onNoteOn(channel, noteNo, velocity); break; case Event::kNoteOffEvent: // ノートオフイベントの場合 channel = event.noteOff.channel; noteNo = event.noteOff.pitch; velocity = event.noteOff.velocity; onNoteOff(channel, noteNo, velocity); break; } } } } |
MIDIノートオン/ノートオフイベントがあった場合の処理は下記の通りです。
【processor.cpp】
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
void MyVSTProcessor::onNoteOn(int channel, int note, float velocity) { // MIDIノートオンイベントの処理を行う // 簡単なサンプルなので、channelとvelocityは無視する // 押されたノートから、音程を計算 // ノートNo.69が440Hzになる。これを基準に計算する。 // 計算式の詳細説明については割愛 float pitch = (440.0f * powf(2.0f, (float)(note - (69)) / 12.0)); // pitchListの最後に追加する pitchList.push_back(pitch); // ボリュームが0.0fだと音が出ないのでを1.0fにしておく volume = 1.0f; } void MyVSTProcessor::onNoteOff(int channel, int note, float velocity) { // MIDIノートオフイベントの処理を行う // 簡単なサンプルなので、channelとvelocityは無視する // 押されたノートから、音程を計算 float pitch = (440.0f * powf(2.0f, (float)(note - (69)) / 12.0)); // pitchListを最初から検索し、pitchに合致するものを削除する for (int i = 0; i < (int)pitchList.size(); i++) { if (pitchList[i] == pitch) { // pitchと合致するものがあった場合、 // 該当するデータを取り除いて検索を終了する pitchList.erase(pitchList.begin() + i); break; } } // pitchListのサイズが0の場合、つまり押されたノートがない場合、 // ボリュームを0.0fにして音を消す if (pitchList.size() == 0) { volume = 0.0f; } } |
MIDIノートオンイベントを受け取った際には、ノートNoに対応する音程(周波数)を計算し、pitchList配列の最後に保存します。
そして、オシレータの音量を1.0にして音が出るようにします。
MIDIノートオフイベントを受け取った際には、ノートNoに対応する音程(周波数)を計算したあと、pitchList配列から該当する音程(周波数)のデータを取り除きます。
そして、pitchList配列のサイズを確認し、0の場合(つまり押されている鍵盤がない場合)音量を0にします。
音声出力処理
音声処理部分は簡単に下記のとおりとしています。
【processor.cpp】
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
// 入力・出力バッファのポインタをわかりやすい変数に格納 // inputs[]、outputs[]はAudioBusの数だけある(addAudioInput()、addAudioOutput()で追加した分だけ) // 今回はAudioBusは1つだけなので 0 のみとなる // channelBuffers32は32bit浮動小数点型のバッファで音声信号のチャンネル数分ある // モノラル(kMono)なら 0 のみで、ステレオ(kStereo)なら 0(Left) と 1(Right) となる Sample32* outL = data.outputs[0].channelBuffers32[0]; Sample32* outR = data.outputs[0].channelBuffers32[1]; // numSamplesで示されるサンプル分、音声を処理する for (int32 i = 0; i < data.numSamples; i++) { // オシレーターの音程を取得する。 float pitch = 0.0f; // 音程リストのサイズが0でない場合 if (pitchList.size() != 0) { // 最後に押されたMIDIノートの音程を取得する pitch = pitchList[pitchList.size() - 1]; } // 角度θに音程に応じた角速度を加える // (ここではサンプルレート44100Hz固定で計算しているので必要に応じて処理する) theta += (2.0f * 3.14159265f * pitch) / 44100.0f; // ボリュームとオシレータ(sin()関数)の値(振幅)を計算して出力する // (突然大きな音がでるとまずいのでとりあえず0.3fを掛けている) outL[i] = 0.3f * volume * sin(theta); outR[i] = 0.3f * volume * sin(theta); } |
MIDIノートが押されていない場合は、volumeが0となり無音状態となり、かつthetaが更新されない状態となります。
押されている場合は最後に押されたMIDIノートの音程(周波数)を取得し、それに合わせてsin()関数を出力いたします。
ホストに楽器(VSTi)として読み込ませるために
ホスト(DAWなど)に楽器(VSTi)として読み込ませるためには、factory.cppに記載されている生成関数の自作VSTカテゴリを変更いたします。
【factroy.cpp】
18 19 20 21 22 23 24 25 |
// 自作するVSTの名前。終端文字「\0」含めて64文字まで。 #define MYVST_VSTNAME "MyVST Name" // 自作するVSTのバージョン。終端文字「\0」含めて64文字まで。 #define MYVST_VERSION "0" // 自作するVSTのカテゴリ。終端文字「\0」含めて64文字まで。 #define MYVST_SUBCATEGORIES Vst::PlugType::kInstrument |
今まではVst::PlugType::kFxとしていたのでエフェクターとして読み込ませていましたが、
Vst::PlugType::kInstrumentとすることで楽器(VSTi)として読み込ませることが可能です。
他にもカテゴリはたくさんありますが、ここでは省略いたします。詳しくはこちらでご確認ください。
注意事項について
このサンプルでは、ノートオン/ノートオフのタイミングでvolumeを0.0→1.0や1.0→0.0のように振幅を急激に変化させているため、ノイズが入ります。
基本的には問題ないと思いますが大きな音量の場合、耳やスピーカーを痛める可能性があります。少し小さめの音で試していただくようお願いいたします。
最後に
以上でMIDIのノートオン/ノートオフを受け取ることができます。
なお本サンプルではMIDIイベントメッセージの厳密なタイミング処理を省略しております。
詳細については要望があれば記載いたしますので、掲示板かTwitterでいただければと思います。
次回はMIDIのコントロールチェンジを受信する方法を記載したいと思います。
このVST3プラグインのサンプルソースファイルはこちらからダウンロードできます → vst3dev_20210403.zip
コンパイル・ビルドの方法は簡単にこちらでご説明しております。ご参考までに。→サンプルソースファイルのビルド方法
VST3プラグイン作りの情報はこちらにもございます → はじめてのVST3プラグイン作り
ご指摘やご質問などがございましたら、コメント欄か掲示板、Twitterでご連絡いただければと思います。
■掲示板
■Twitterアカウント:@vstcpp URL:https://twitter.com/vstcpp