オーディオプラグインの作成、パート7

シリーズのすべての投稿:
パート1.紹介とセットアップ
パート2.コードの学習
パート3. VSTおよびAU
パート4.デジタル歪み
パート5.プリセットとGUI
パート6.信号合成
パート7. MIDIメッセージの受信
パート8.仮想キーボード
パート9.封筒
パート10. GUIの改善
パート11.フィルター
パート12.低周波発振器
パート13.再設計
パート14.ポリフォニー1
パート15.ポリフォニー2
パート16.アンチエイリアス



これまでのところ、特定の周波数で鳴るだけの一定の音波のみを生成しました。 受信したノートに応じて、MIDIメッセージに応答し、目的の周波数で波の生成をオンまたはオフにする方法を見てみましょう。

MIDIメッセージの受信



MIDI処理の基本



プラグインがホストにロードされると、リンク先のトラックからすべてのMIDIメッセージを受信します。 メモが開始および終了すると、 ProcessMidiMsg関数がプラグインで呼び出されます。 ノートに加えて、MIDIメッセージはポルタメント情報(ピッチベンド)およびコントロールコマンド( コントロール変更 、CCと略記)を送信できます。これらはプラグインパラメーターの自動化に使用できます。 IMidiMsgメッセージがProcessMidiMsg関数に送信されます。この関数は、形式に依存しない形式でMIDIイベントを記述します。 この説明には、オシレーターのピッチに関する情報を含むNoteNumberおよびVelocityパラメーターがあります。

MIDIメッセージが到着するたびに、システムは以前に満たされたオーディオバッファを既に再生しています。 MIDIメッセージを受信したときに新しいオーディオを正確に詰め込む方法はありません。 これらのイベントは、次にProcessDoubleReplacing関数を呼び出す前に記憶する必要があります。 また、メッセージを受信した時間を覚えておく必要があります。そうすれば、次のバッファ充填のためにこの情報をそのまま残すことができます。

これらのタスクを実行するツールはIMidiQueueになります。

MIDI受信者



合成プロジェクトを使用します。 バージョン管理システムを使用している場合は、プロジェクトをコミットします。 新しいMIDIReceiverクラスを作成し、各ターゲットで.cppがコンパイルされることを確認します。 MIDIReceiver.hで、 #define#endif間にインターフェイスを挿入します。

 #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wextra-tokens" #include "IPlug_include_in_plug_hdr.h" #pragma clang diagnostic pop #include "IMidiQueue.h" class MIDIReceiver { private: IMidiQueue mMidiQueue; static const int keyCount = 128; int mNumKeys; // how many keys are being played at the moment (via midi) bool mKeyStatus[keyCount]; // array of on/off for each key (index is note number) int mLastNoteNumber; double mLastFrequency; int mLastVelocity; int mOffset; inline double noteNumberToFrequency(int noteNumber) { return 440.0 * pow(2.0, (noteNumber - 69.0) / 12.0); } public: MIDIReceiver() : mNumKeys(0), mLastNoteNumber(-1), mLastFrequency(-1.0), mLastVelocity(0), mOffset(0) { for (int i = 0; i < keyCount; i++) { mKeyStatus[i] = false; } }; // Returns true if the key with a given index is currently pressed inline bool getKeyStatus(int keyIndex) const { return mKeyStatus[keyIndex]; } // Returns the number of keys currently pressed inline int getNumKeys() const { return mNumKeys; } // Returns the last pressed note number inline int getLastNoteNumber() const { return mLastNoteNumber; } inline double getLastFrequency() const { return mLastFrequency; } inline int getLastVelocity() const { return mLastVelocity; } void advance(); void onMessageReceived(IMidiMsg* midiMessage); inline void Flush(int nFrames) { mMidiQueue.Flush(nFrames); mOffset = 0; } inline void Resize(int blockSize) { mMidiQueue.Resize(blockSize); } }; 


ここでIPlug_include_in_plug_hdr.hを有効にする必要があります。そうしないと、 IMidiQueue.hがエラーを作成します。
ご覧のとおり、MIDIメッセージキューを格納するためのprivate IMidiQueueオブジェクトがあります。 また、現在再生されているノートと、再生されているノートの数に関する情報も保存します。 プラグインはモノフォニックになるmLast... 、3つのmLast...パラメーターが必要mLast...次の各ノートは前のノートを消し去ります(いわゆる最後のノートの優先度 )。 noteNumberToFrequency関数は、MIDIノートの数をヘルツnoteNumberToFrequency周波数に変換します。 Oscillatorクラスはノートナンバーではなく周波数で動作するため、これを使用します。
publicセクションには多数のinlineゲッターが含まれ、 FlushおよびResizemMidiQueue
Flushの本体ではFlush mOffsetをゼロに設定します。 mMidiQueue.Flush(nFrames)呼び出しは、 advance関数の前の呼び出しでこの部分のイベントをすでに処理したため、キューの先頭からサイズnFrames部分を削除することを意味します。 mOffsetをゼロにすることにより、次にmOffsetを実行するときに、キューの開始も処理することが保証されます。 括弧の後に現れるconstは、関数がそのクラスの不変のメンバーを変更しないことを意味します。

onMessageReceived実装をMIDIReceiver.cppに追加しましょう。

 void MIDIReceiver::onMessageReceived(IMidiMsg* midiMessage) { IMidiMsg::EStatusMsg status = midiMessage->StatusMsg(); // We're only interested in Note On/Off messages (not CC, pitch, etc.) if(status == IMidiMsg::kNoteOn || status == IMidiMsg::kNoteOff) { mMidiQueue.Add(midiMessage); } } 


この関数は、MIDIメッセージを受信するたびに呼び出されます。 現在、 ノートオンメッセージとノートオフメッセージ(ノートの再生の開始/停止)のみに関心があり、それらをmMidiQueue追加しmMidiQueue
次の興味深い機能はadvanceです:

 void MIDIReceiver::advance() { while (!mMidiQueue.Empty()) { IMidiMsg* midiMessage = mMidiQueue.Peek(); if (midiMessage->mOffset > mOffset) break; IMidiMsg::EStatusMsg status = midiMessage->StatusMsg(); int noteNumber = midiMessage->NoteNumber(); int velocity = midiMessage->Velocity(); // There are only note on/off messages in the queue, see ::OnMessageReceived if (status == IMidiMsg::kNoteOn && velocity) { if(mKeyStatus[noteNumber] == false) { mKeyStatus[noteNumber] = true; mNumKeys += 1; } // A key pressed later overrides any previously pressed key: if (noteNumber != mLastNoteNumber) { mLastNoteNumber = noteNumber; mLastFrequency = noteNumberToFrequency(mLastNoteNumber); mLastVelocity = velocity; } } else { if(mKeyStatus[noteNumber] == true) { mKeyStatus[noteNumber] = false; mNumKeys -= 1; } // If the last note was released, nothing should play: if (noteNumber == mLastNoteNumber) { mLastNoteNumber = -1; mLastFrequency = -1; mLastVelocity = 0; } } mMidiQueue.Remove(); } mOffset++; } 


この関数は、オーディオバッファがいっぱいになっている間、 サンプルごとに呼び出されます 。 キューにメッセージがある限り、メッセージを処理して最初から削除します( PeekおよびRemoveを使用)。 ただし、これは、オフセット( mOffset )がバッファーオフセットより大きくないMIDIメッセージに対してのみ行います。 つまり、対応するサンプルの各メッセージを処理し、相対的な時間シフトをそのまま残します。
noteNumberVelocity値を読み取った後、条件付きifノートオンノートオフのメッセージを分離ます(ベロシティ値がないとノートオフと解釈されます )。 どちらの場合も、どのノートが再生され、現在再生されているノートの数を追跡します。 mLast...値も更新され、最後のノートに優先順位が付けられます。 さらに、音の周波数を更新する必要があるのはここであるということは論理的です。 最後に、 mOffset更新され、受信者に、このメッセージが現在バッファ内にどの程度あるかが通知されます。 別の方法で受信者にこれを伝えることもできます-引数としてオフセットを渡すことによって。
したがって、すべての着信MIDIノートのオン/オフメッセージを受信するクラスがあります。 現在再生されているノート、最後のノート、およびその頻度を追跡します。 それを使用しましょう。

MIDI受信者を使用する



開始するには、 resource.hにこれらの変更を慎重に加えます。

 // #define PLUG_CHANNEL_IO "1-1 2-2" #if (defined(AAX_API) || defined(RTAS_API)) #define PLUG_CHANNEL_IO "1-1 2-2" #else // no audio input. mono or stereo output #define PLUG_CHANNEL_IO "0-1 0-2" #endif // ... #define PLUG_IS_INST 1 // ... #define EFFECT_TYPE_VST3 "Instrument|Synth" // ... #define PLUG_DOES_MIDI 1 


これらの行は、プラグインが「MIDI対応」であることをホストに伝えます。 0-1および0-2は、プラグインにオーディオ入力がなく、出力が1つあることを示します。 モノ( 0-1 )、またはオーディオ入力がなく、ステレオ出力( 0-2 )があります。
次に、 Oscillator.h後に#include "MIDIReceiver.h"Synthesis.hに追加します。 同じ場所のpublicセクションで、メンバー関数の宣言を追加します。

 // to receive MIDI messages: void ProcessMidiMsg(IMidiMsg* pMsg); 


privateセクションにMIDIReceiverオブジェクトを追加します。

 private: // ... MIDIReceiver mMIDIReceiver; 


Synthesis.cppで、次の単純な関数を記述します。

 void Synthesis::ProcessMidiMsg(IMidiMsg* pMsg) { mMIDIReceiver.onMessageReceived(pMsg); } 


MIDIメッセージが受信されるたびに呼び出され、受信者にメッセージを送信します。
少し整理しましょう。 上部の両方のenumsを編集します。

 enum EParams { kNumParams }; enum ELayout { kWidth = GUI_WIDTH, kHeight = GUI_HEIGHT }; 


そして、デフォルトのプリセットを1つだけ作成します。

 void Synthesis::CreatePresets() { MakeDefaultPreset((char *) "-", kNumPrograms); } 


プラグインのパラメーターを変更する場合、何もする必要はありません:

 void Synthesis::OnParamChange(int paramIdx) { IMutexLock lock(this); } 


インターフェイスのハンドルは、もはや役に立ちません。 コンストラクタを必要最小限に減らしましょう:

 Synthesis::Synthesis(IPlugInstanceInfo instanceInfo) : IPLUG_CTOR(kNumParams, kNumPrograms, instanceInfo) { TRACE; IGraphics* pGraphics = MakeGraphics(this, kWidth, kHeight); pGraphics->AttachPanelBackground(&COLOR_RED); AttachGraphics(pGraphics); CreatePresets(); } 


システムのサウンド設定が変更されると、オシレーターに新しいサンプリング周波数を伝える必要があります。

 void Synthesis::Reset() { TRACE; IMutexLock lock(this); mOscillator.setSampleRate(GetSampleRate()); } 


ProcessDoubleReplacing関数はまだあります。 考えてみてください: mMIDIReceiver.advance()は各サンプルを実行する必要があります。 その後、MIDIレシーバーのgetLastVelocitygetLastFrequencyを使用して、周波数と音量をgetLastVelocityます。 次に、 mOscillator.setFrequency()およびmOscillator.generate()を呼び出して、オーディオバッファーを目的の周波数のサウンドで満たします。
generate関数は、バッファ全体を処理するために作成されました。 MIDIレシーバーは、個別のサンプルのレベルで動作します。メッセージは、バッファー内で任意のオフセットを持つことができます。つまり、 mLastFrequencyは任意のサンプルで変更できます。 サンプルレベルでも機能するように、 Oscillatorクラスを改良する必要があります。

最初に、 generateからtwoPItwoPIし、 privateセクションOscillator.hに移動します。 ここにいる間に、リンデン変数boolをすぐに追加して、オシレーターがミュートされている(つまり、ノートが再生されていない)かどうかを示します。

 const double twoPI; bool isMuted; 


初期化リストにコンストラクタを追加して、それらを初期化します。 これは次のようになります。

 Oscillator() : mOscillatorMode(OSCILLATOR_MODE_SINE), mPI(2*acos(0.0)), twoPI(2 * mPI), // This line is new isMuted(true), // And this line mFrequency(440.0), mPhase(0.0), mSampleRate(44100.0) { updateIncrement(); }; 


インラインセッターをパブリックセクションに追加します。

 inline void setMuted(bool muted) { isMuted = muted; } 


そして、このすぐ下に次の行を挿入します。

 double nextSample(); 


この関数をサンプルごとに呼び出し、オシレーターからオーディオデータを受け取ります。
Oscilator.cppに次のコードを追加します。

 double Oscillator::nextSample() { double value = 0.0; if(isMuted) return value; switch (mOscillatorMode) { case OSCILLATOR_MODE_SINE: value = sin(mPhase); break; case OSCILLATOR_MODE_SAW: value = 1.0 - (2.0 * mPhase / twoPI); break; case OSCILLATOR_MODE_SQUARE: if (mPhase <= mPI) { value = 1.0; } else { value = -1.0; } break; case OSCILLATOR_MODE_TRIANGLE: value = -1.0 + (2.0 * mPhase / twoPI); value = 2.0 * (fabs(value) - 0.5); break; } mPhase += mPhaseIncrement; while (mPhase >= twoPI) { mPhase -= twoPI; } return value; } 


ご覧のとおり、 twoPI使用されていtwoPI 。 サンプルごとにこの値を計算することは冗長なので、クラスに定数として2つのpiを追加しました。
発振器が何も生成しない場合、ゼロを返します。 switch構造はすでにおなじみですが、ここではforループを使用していません。 ここでは、バッファ全体を埋めるのではなく、バッファの単一の値を生成します。 また、同様の構造により、位相の増分を終了し、繰り返しを避けることができます。
これは、コードの柔軟性が不十分なために発生するリファクタリングの好例です。 もちろん、「バッファ」アプローチでgenerate関数を書き始める前に、1、2時間考えることができます。 しかし、この実装には1時間もかかりませんでした。 単純なアプリケーション(このような)では、アプローチを実装し、実際にコードがタスクを処理する方法を確認する方が効率的である場合があります。 ほとんどの場合、先ほど見たように、アイデア全体が正しいことがわかりました(異なる音波を計算する原理)が、問題のいくつかの側面が見落とされていました。 一方、パブリックAPIを開発している場合、後で何かを変更して、軽度の変更を加えるのは不便なので、事前に考え直す必要があります。 一般的に、状況によって異なります。

setFrequency関数もすべてのサンプルで呼び出されます。 これは、 updateIncrementも頻繁に呼び出されることを意味します。 しかし、まだ最適化されていません。

 void Oscillator::updateIncrement() { mPhaseIncrement = mFrequency * 2 * mPI / mSampleRate; } 


2 * mPI * mSampleRateは、サンプリングレートが変更された場合にのみ変更されます。 したがって、この計算の結果は、 Oscillator::setSampleRate内でのみ覚えて再カウントする方が適切Oscillator::setSampleRate 。 超越的な最適化はコードを読みにくく、さらに見苦しくすることもあることを覚えておく価値があります。 特定のケースでは、基本的なモノフォニック構文を作成しているため、パフォーマンスの問題は発生しません。 ポリフォニーに到達するとき、それは別の問題になり、その後、確実に最適化されます。
これで、 Synthesis.cppの ProcessDoubleReplacingを書き換えることができます。

 void Synthesis::ProcessDoubleReplacing( double** inputs, double** outputs, int nFrames) { // Mutex is already locked for us. double *leftOutput = outputs[0]; double *rightOutput = outputs[1]; for (int i = 0; i < nFrames; ++i) { mMIDIReceiver.advance(); int velocity = mMIDIReceiver.getLastVelocity(); if (velocity > 0) { mOscillator.setFrequency(mMIDIReceiver.getLastFrequency()); mOscillator.setMuted(false); } else { mOscillator.setMuted(true); } leftOutput[i] = rightOutput[i] = mOscillator.nextSample() * velocity / 127.0; } mMIDIReceiver.Flush(nFrames); } 


forループでは、MIDIレシーバーが最初に値を更新します( advanceが呼び出されます)。 音が鳴る( velocity > 0 )場合、オシレーターの周波数を更新して鳴らします。 それ以外の場合は、スタブします( nextSampleはゼロを返します)。
次に、単にnextSampleを呼び出して値を取得し、ボリュームを変更し( velocity127整数)、出力バッファーに結果を書き込みます。 最後に、キューの先頭を削除するためにFlushが呼び出されます。

テスト



VSTまたはAUを実行します。 AUがホストに表示されない場合、 resource.hPLUG_UNIQUE_IDを変更する必要がある場合があります。 2つのプラグインのIDが同じ場合、ホストは1つを除くすべてを無視します。
プラグインは、いくつかのMIDIデータを入力に送信する必要があります。 最も簡単な方法は、REAPERの仮想キーボードを使用することです( [表示]→[仮想MIDIキーボード ]メニュー)。 左側にプラグインがあるトラックには、丸い赤いボタンがあります。 MIDI構成を右クリックしてMIDI構成に移動し、仮想キーボードからメッセージを受信することを選択します。



同じメニューで、 モニター入力を有効にします。 これで仮想キーボードウィンドウフォーカスが置かれたので 、通常のキーボードでシンセサイザーを演奏できます。 パスワードマネージャーからユーザー名またはパスワードを入力し、どのように聞こえるかを聞きます。
MIDIキーボードを使用している場合、それを接続することにより、スタンドアロンアプリケーションをテストできます。 主なことは、正しいMIDI入力を選択することです。 音が聞こえない場合は、 〜/ Library / Application Support / Synthesis / settings.iniを削除してみてください。

この段階のプロジェクト全体は、ここからダウンロードできます

次回、インターフェースに素敵なキーボードを追加します:)

元の記事:
martin-finke.de/blog/articles/audio-plugins-009-receive-midi

Source: https://habr.com/ru/post/J226573/


All Articles