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

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



持っているコンポーネントからポリフォニックシンセサイザーの作成を始めましょう!

前回パラメータとユーザーインターフェースに取り組んだとき、今日はプラグインの基礎となるポリフォニックオーディオ処理の作業を開始します。 この例では、一度に最大64のノートを演奏できます。 これにはプラグインの構造を大きく変更する必要がありますが、すでに記述したクラスOscillatorEnvelopeGeneratorMIDIReceiverFilterを使用できます。

この投稿では、1つのサウンディングノートを表すVoiceクラスを作成します。 次に、 VoiceManagerクラスを作成し、すべてのノートが時間通りに鳴り、消音されるようにします。
次の投稿では、不要な古いパーツからコードを削除し、トーン変調を追加して、動作状態のインターフェイスにコントロールを追加します。 一見、仕事はいっぱいです。 しかし、第一に、私たちはすでに必要なコンポーネントのほぼすべてを持っています、そして第二に、最終的には、実際のポリフォニック減算減算パンケーキシンセサイザーがあります!



どこへ?



プラグインアーキテクチャのどの部分がグローバルで、個々のノートごとに別々に存在するかについて少し考えてみましょう。 想像してみてください。ここでは、キーについていくつかのノートを演奏しています。 キーを押すたびに、トーンがフェードし、場合によっては、特定のエンベロープに沿ってフィルターによって音色が変化します。 2番目のキーを押すと、最初のキーが鳴り、2番目のトーンが振幅とフィルターのエンベロープで表示されます。 2回押しても最初の音には影響せず、音自体が変化します 。 したがって、各音声は独立しており、独自の振幅とフィルターのエンベロープを持っています。
LFOはグローバルでユニークであり、機能するだけで、キーを押しても再起動しません。
フィルターに関しては、すべての音声がGUIの同じカットオフとレゾナンスノブを見るため、カットオフ周波数とレゾナンスがグローバルであることは明らかです。 しかし、フィルターのカットオフ周波数はエンベロープによって変調されるため、各瞬間において、各音声の計算されたカットオフ周波数は異なります。 Filter::cutoff - Filter::cutoffが呼び出されます。 そのため、音声ごとに独自のフィルターが必要です。
すべての音声に対して2つのオシレーターで対応できますか? 各Voiceは独自のノートを再生します。 独自の周波数を持つため、独立したOscillatorです。

つまり、構造は次のとおりです。



音声クラス



通常どおり、新しいクラスを作成し、 Voiceという名前を付けます。 そして、いつものように、すべてのXcodeターゲットとすべてのVSプロジェクトに追加することを忘れないでください。 Voice.hで以下を追加します。

 #include "Oscillator.h" #include "EnvelopeGenerator.h" #include "Filter.h" 


クラスの本文で、 privateセクションから始めます。

 private: Oscillator mOscillatorOne; Oscillator mOscillatorTwo; EnvelopeGenerator mVolumeEnvelope; EnvelopeGenerator mFilterEnvelope; Filter mFilter; 


ここで新しいことはありません。各ボイスには2つのオシレーター、フィルター、2つのエンベロープがあります。
各ボイスは、特定のMIDIノートとボリュームで始まります。 そこに追加:

  int mNoteNumber; int mVelocity; 


以下の各変数は、パラメーターの変調値を設定します。

  double mFilterEnvelopeAmount; double mOscillatorMix; double mFilterLFOAmount; double mOscillatorOnePitchAmount; double mOscillatorTwoPitchAmount; double mLFOValue; 


mLFOValueを除くそれらすべては、インターフェイスハンドルの値に関連付けられています。 実際、これらの値はすべてのボイスで同じですが、グローバルにせず、プラグインクラスにドロップしません。 各音声はサンプルごとにこれらのパラメーターにアクセスする必要があり、Voiceクラスはプラグインクラスの存在すら知りません( #include "SpaceBass.h" )。 このようなアクセスの設定は、時間のかかるタスクです。
そしてもう1つのパラメーターがあります。 OscillatorクラスにisMutedフラグを追加したことを覚えていますか? それをVoice移動して、声が静かなときにオシレーター、エンベロープ、フィルターの値が計算されないようにします。

  bool isActive; 


private publicを追加します。 コンストラクターから始めましょう。

 public: Voice() : mNoteNumber(-1), mVelocity(0), mFilterEnvelopeAmount(0.0), mFilterLFOAmount(0.0), mOscillatorOnePitchAmount(0.0), mOscillatorTwoPitchAmount(0.0), mOscillatorMix(0.5), mLFOValue(0.0), isActive(false) { // Set myself free everytime my volume envelope has fully faded out of RELEASE stage: mVolumeEnvelope.finishedEnvelopeCycle.Connect(this, &Voice::setFree); }; 


これらの行は、適切な値で変数を初期化します。 デフォルトでは、 VoiceアクティブでVoiceません。 また、信号とEnvelopeGeneratorスロットを使用して、振幅エンベロープがリリースステージを離れるとすぐに音声を「リリース」します。
セッターをpublic追加します。

  inline void setFilterEnvelopeAmount(double amount) { mFilterEnvelopeAmount = amount; } inline void setFilterLFOAmount(double amount) { mFilterLFOAmount = amount; } inline void setOscillatorOnePitchAmount(double amount) { mOscillatorOnePitchAmount = amount; } inline void setOscillatorTwoPitchAmount(double amount) { mOscillatorTwoPitchAmount = amount; } inline void setOscillatorMix(double mix) { mOscillatorMix = mix; } inline void setLFOValue(double value) { mLFOValue = value; } inline void setNoteNumber(int noteNumber) { mNoteNumber = noteNumber; double frequency = 440.0 * pow(2.0, (mNoteNumber - 69.0) / 12.0); mOscillatorOne.setFrequency(frequency); mOscillatorTwo.setFrequency(frequency); } 


ここで唯一興味深い点はsetNoteNumberです。 既知の式を使用して特定のノートの周波数を計算し、両方のオシレーターに渡します。 追加後:

  double nextSample(); void setFree(); 


Oscillator::nextSampleOscillator::nextSampleの出力を提供するので、 Voice::nextSampleは振幅とフィルターのエンベロープの後の音声の結果値を提供します。 Voice.cppで実装を書きましょう:

 double Voice::nextSample() { if (!isActive) return 0.0; double oscillatorOneOutput = mOscillatorOne.nextSample(); double oscillatorTwoOutput = mOscillatorTwo.nextSample(); double oscillatorSum = ((1 - mOscillatorMix) * oscillatorOneOutput) + (mOscillatorMix * oscillatorTwoOutput); double volumeEnvelopeValue = mVolumeEnvelope.nextSample(); double filterEnvelopeValue = mFilterEnvelope.nextSample(); mFilter.setCutoffMod(filterEnvelopeValue * mFilterEnvelopeAmount + mLFOValue * mFilterLFOAmount); return mFilter.process(oscillatorSum * volumeEnvelopeValue * mVelocity / 127.0); } 


最初の行は、音声が非アクティブのときに何も計算されず、ゼロが返されるようにします。 次の3行は、両方のオシレーターのnextSampleを計算し、 nextSampleに従ってそれらをミックスします。 mOscillatorMixがゼロの場合、 oscillatorOneOutputのみが聞こえます。 0.5両方のオシレーターの振幅が等しくなります。
次に、両方のエンベロープの次のサンプルが計算されます。 filterEnvelopeValueをフィルターカットオフ周波数に適用し、LFO値を考慮します。 全体的なカットモジュレーションは、フィルターエンベロープとLFOの合計です。
両方のオシレーターのトーン変調は、単にLFO出力に変調値を掛けたものです。 すぐに書きます。
最後の行は興味深いです。 まず、括弧の内容:2つのオシレーターの合計を取り、ボリュームエンベロープとノートのボリューム値を適用します。 次に、結果をmFilter.process渡します。その結果、フィルター処理された出力を取得し、それを返します。

setFreeの実装setFree非常に簡単です。

 void Voice::setFree() { isActive = false; } 


既に述べたように、この関数はmVolumeEnvelope完全にフェードするたびに呼び出されます。

ボイスマネージャー



音声制御用のクラスを作成します。 VoiceManagerというクラスを作成します。 ヘッダーで、次の行から始めます。

 #include "Voice.h" class VoiceManager { }; 


そして、クラスのprivateメンバーで続行します。

 static const int NumberOfVoices = 64; Voice voices[NumberOfVoices]; Oscillator mLFO; Voice* findFreeVoice(); 


NumberOfVoicesは、同時にNumberOfVoicesできるボイスの最大数を示します。 次の行は投票の配列を作成します。 この構造は64票の場所を使用するため、メモリの動的割り当てを検討することをお勧めします 。 ただし、プラグインクラスはnew PLUG_CLASS_NAME動的に分散されているため( new PLUG_CLASS_NAMEで「 new PLUG_CLASS_NAME 」を探してください )、プラグインクラスのすべてのメンバーもヒープ上にあります

mLFOは、プラグインのグローバルLFOです。 再起動することはなく、独立して振動するだけです。 プラグインクラス内にあるべきだと主張することができます( VoiceManagerはLFOについて知る必要はありません)。 ただし、これにより、 VoiceとLFOの音声に別のレイヤーが追加されます 。つまり、より多くの接着コードが必要になります
findFreeVoiceは、現在findFreeVoiceれていない声を見つけるためのヘルパー関数です。 VoiceManager.cppに実装を追加します。

 Voice* VoiceManager::findFreeVoice() { Voice* freeVoice = NULL; for (int i = 0; i < NumberOfVoices; i++) { if (!voices[i].isActive) { freeVoice = &(voices[i]); break; } } return freeVoice; } 


彼女は単にすべての声を繰り返して、最初の声を黙らせます。 この場合、リンクとは異なり、 NULLを返すことができるため、( &リンクの代わりに)ポインターを返しNULL 。 これは、すべての声が聞こえることを意味します。

次に、次の関数ヘッダーをpublic追加します。

 void onNoteOn(int noteNumber, int velocity); void onNoteOff(int noteNumber, int velocity); double nextSample(); 


名前がonNoteOnonNoteOnはMIDI Note Onメッセージを受信したときに呼び出されます。 onNoteOff 、したがって、Note Offメッセージが受信されると呼び出されます。 これらの関数のコードを.cppクラスファイルに記述します。

 void VoiceManager::onNoteOn(int noteNumber, int velocity) { Voice* voice = findFreeVoice(); if (!voice) { return; } voice->reset(); voice->setNoteNumber(noteNumber); voice->mVelocity = velocity; voice->isActive = true; voice->mVolumeEnvelope.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_ATTACK); voice->mFilterEnvelope.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_ATTACK); } 


まず、 findFreeVoice無料の音声をfindFreeVoiceます。 何も見つからない場合、何も返しません。 これは、すべての音声が聞こえたときに、別のキーを押しても結果が得られないことを意味します。 音声盗難アプローチの実装は、次の投稿のトピックの1つになります。 無料の音声がある場合は、その音声を初期状態にresetする必要があります( reset 、すぐに行います)。 その後、 setNoteNumbermVelocity正しい値を設定します。 音声をアクティブとしてマークし、両方のエンベロープを攻撃ステージに転送します。
今すぐアセンブリを開始すると、外部からprivate Voiceメンバーにアクセスしようとしていることを示すエラーがポップアップ表示されます。 私の意見では、この状況での最良の解決策はキーワードfriendを使用することです。 Voice.hの public前に適切な行を追加します。

 friend class VoiceManager; 


この行により、 VoiceManagerprivateメンバーにVoiceManagerアクセスできます。 私はこのアプローチの広範な使用のファンではありませんが、 FooクラスとFooManagerクラスがある場合、これは多くのセッターを書くことを避ける良い方法です。

onNoteOffは次のようになります。

 void VoiceManager::onNoteOff(int noteNumber, int velocity) { // Find the voice(s) with the given noteNumber: for (int i = 0; i < NumberOfVoices; i++) { Voice& voice = voices[i]; if (voice.isActive && voice.mNoteNumber == noteNumber) { voice.mVolumeEnvelope.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_RELEASE); voice.mFilterEnvelope.enterStage(EnvelopeGenerator::ENVELOPE_STAGE_RELEASE); } } } 


リリースされたノートの番号を持つすべてのボイスを見つけ、そのエンベロープをリリースステージに転送します。 なぜではなく声ですか? 振幅のエンベロープに非常に長い減衰段階があると想像してください。 キーを押して離すと、ノートのテールが鳴っている間に、そのキーをすばやくすばやく押します。 当然、前の発音音を切り落としたくありません。 それは非常にいでしょう。 前の音が鳴り、新しい音が並行して鳴り始めることが必要です。 したがって、ノートごとに複数のボイスが必要になります。 キーを非常にすばやく叩くと、多くの票が必要になります。
たとえば、3番目のオクターブまで5つのアクティブなボイスがあり、このキーを離すとどうなりますか? onNoteOffonNoteOff 、5つのボイスすべてのエンベロープがリリースステージにonNoteOffます。 それらの4つはすでにこの段階にあるため、 EnvelopeGenerator::enterStage最初の行を見てみましょう。

 if (currentStage == newStage) return; 


ご覧のとおり、これらの4つの音符には何も起こりません。ここにひっかかりはありません。

nextSamplenextSampleメンバーnextSampleを作成しましょう。 すべてのアクティブな投票の合計値を表示する必要があります。

 double VoiceManager::nextSample() { double output = 0.0; double lfoValue = mLFO.nextSample(); for (int i = 0; i < NumberOfVoices; i++) { Voice& voice = voices[i]; voice.setLFOValue(lfoValue); output += voice.nextSample(); } return output; } 


無音(0.0)から開始し、すべての音声を反復処理し、現在のLFO値を設定して、音声出力を合計出力に追加します。 覚えているように、音声がアクティブでない場合、そのVoice::nextSample関数は何も計算せず、すぐに終了します。

再利用可能なコンポーネント



これまで、 OscillatorおよびFilterオブジェクトを作成し、プラグインが機能する間ずっと使用していました。 ただし、VoiceManagerは無料の音声を再利用するため、音声を元の状態に完全に戻す方法を理解する必要があります。 まず、 public Voiceヘッダーに関数を追加します。

 void reset(); 


関数の本体を.cppに記述します。

 void Voice::reset() { mNoteNumber = -1; mVelocity = 0; mOscillatorOne.reset(); mOscillatorTwo.reset(); mVolumeEnvelope.reset(); mFilterEnvelope.reset(); mFilter.reset(); } 


ご覧のとおり、 mNoteNumbermVelocitymVelocityでリセットさmVelocity 、その後オシレーター、エンベロープ、フィルターがリセットされます。 書きましょう!

Oscillator.hpublicセクションで、以下を追加します。

 void reset() { mPhase = 0.0; } 


これにより、音声が鳴り始めるたびに最初に波形を開始できます。

同時に、私たちがそこにいる間に、 privateセクションからisMutedフラグを削除します。 コンストラクターの初期化リストからも削除し、 setMutedメンバーsetMutedを削除することを忘れないでください。 Voiceレベルでアクティビティレベルを監視するようになったため、オシレーターはそれを必要としなくなりました。 Oscillator::nextSampleからこの行を削除しOscillator::nextSample

 // remove this line: if(isMuted) return value; 


EnvelopeGeneratorreset関数EnvelopeGeneratorもう少し長くなります。 EnvelopeGeneratorヘッダーのpublicセクションで、次のように記述します。

 void reset() { currentStage = ENVELOPE_STAGE_OFF; currentLevel = minimumLevel; multiplier = 1.0; currentSampleIndex = 0; nextStageSampleIndex = 0; } 


ここでは、より多くの値をリセットするだけで、すべてが線形です。 Filterクラスのresetを追加することは(これもpublic ):

 void reset() { buf0 = buf1 = buf2 = buf3 = 0.0; } 


おそらく覚えているように、これらのバッファーには以前の出力フィルターサンプルが含まれています。 音声を再利用するとき、これらのバッファは空でなければなりません。

要約すると、 VoiceManagerVoice使用するたびに、 reset関数を呼び出して音声を初期状態にリセットします。 この関数は、音声オシレーター、そのエンベロープジェネレーター、およびフィルターをリセットします。

静的か非静的か?



すべての投票のメンバー変数は同じです:



最初は、そのような冗長性は悪であり、これらはすべて静的メンバーであると考えました。 mOscillatorModeが静的であると想像してみましょう。 この場合、LFOは他のオシレーターと同じ波形になりますが、これは望ましくありません。 さらに、 EnvelopeGeneratorエンベロープジェネレーターのstageValue値が静的である場合、振幅エンベロープとフィルターエンベロープは同じになります。

これは、継承によって修正できますFilterEnvelopeクラスを継承するFilterEnvelopeクラスとFilterEnvelopeクラスを作成することによって。 stageValueパラメーターは静的であり、 VolumeEnvelopeおよびFilterEnvelopeはそれを変更できます。 これにより、エンベロープが明確に分離され、すべての音声が静的メンバーにアクセスできます。 ただし、この場合、大量のメモリについては説明していません。 作成した構造で行う必要があるのは、振幅のエンベロープとすべての音声のフィルター間でこれらの変数を同期することだけです。

ただし、静的なものの1つはsampleRateです。 シンセサイザーのコンポーネントが異なるサンプリング周波数で動作することは意味がありません。 Oscillator.hでこれを微調整しましょう。

 static double mSampleRate; 


そのため、初期化リストを介してこの変数を初期化しないでください。 mSampleRate(44100.0)削除します。 #include追加後のOscillator.cppで

 double Oscillator::mSampleRate = 44100.0; 


サンプリングレートは静的になり、すべてのオシレーターはその値のいずれかを使用します。
EnvelopeGeneratorについても同じことをしましょう。 sampleRate静的にし、コンストラクターを初期化リストから削除し、 EnvelopeGenerator.cppを追加します。

 double EnvelopeGenerator::sampleRate = 44100.0; 


EnvelopeGenerator.hで 、セッターを静的にします。

 static void setSampleRate(double newSampleRate); 


たくさんの新しいものを追加しました! 次回は余分な部分を取り除き、GUIを動作状態にします。

コードはここからダウンロードできます
元の投稿

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


All Articles