オーディオプラグインの作成、パート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のノートを演奏できます。 これにはプラグインの構造を大きく変更する必要がありますが、すでに記述したクラスOscillator



EnvelopeGenerator



MIDIReceiver



Filter



を使用できます。



この投稿では、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::nextSample



Oscillator::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();
      
      







名前がonNoteOn



onNoteOn



は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



、すぐに行います)。 その後、 setNoteNumber



mVelocity



正しい値を設定します。 音声をアクティブとしてマークし、両方のエンベロープを攻撃ステージに転送します。

今すぐアセンブリを開始すると、外部からprivate



Voice



メンバーにアクセスしようとしていることを示すエラーがポップアップ表示されます。 私の意見では、この状況での最良の解決策はキーワードfriendを使用することです。 Voice.hの public



前に適切な行を追加します。



 friend class VoiceManager;
      
      







この行により、 VoiceManager



private



メンバーに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つのアクティブなボイスがあり、このキーを離すとどうなりますか? onNoteOff



onNoteOff



、5つのボイスすべてのエンベロープがリリースステージにonNoteOff



ます。 それらの4つはすでにこの段階にあるため、 EnvelopeGenerator::enterStage



最初の行を見てみましょう。



 if (currentStage == newStage) return;
      
      







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



nextSample



nextSample



メンバー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(); }
      
      







ご覧のとおり、 mNoteNumber



mVelocity



mVelocity



でリセットさmVelocity



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



Oscillator.hpublic



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



 void reset() { mPhase = 0.0; }
      
      







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



同時に、私たちがそこにいる間に、 private



セクションからisMuted



フラグを削除します。 コンストラクターの初期化リストからも削除し、 setMuted



メンバーsetMuted



を削除することを忘れないでください。 Voice



レベルでアクティビティレベルを監視するようになったため、オシレーターはそれを必要としなくなりました。 Oscillator::nextSample



からこの行を削除しOscillator::nextSample







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







EnvelopeGenerator



reset



関数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; }
      
      







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



要約すると、 VoiceManager



Voice



使用するたびに、 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を動作状態にします。



コードはここからダウンロードできます

元の投稿



All Articles