シリーズのすべての投稿:
パート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です。
つまり、構造は次のとおりです。
- プラグインには1つの
MIDIReceiverと1つのVoiceManager VoiceManagerは1つのLFOと多くのVoiceボイスがありますVoiceは、2つのOscillator 、2つのEnvelopeGenerators (振幅とフィルター用)、および1つのFilter
音声クラス
通常どおり、新しいクラスを作成し、
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) {
これらの行は、適切な値で変数を初期化します。 デフォルトでは、
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) {
リリースされたノートの番号を持つすべてのボイスを見つけ、そのエンベロープをリリースステージに転送します。 なぜ
声ではなく声ですか? 振幅のエンベロープに非常に長い減衰段階があると想像してください。 キーを押して離すと、ノートのテールが鳴っている間に、そのキーをすばやくすばやく押します。 当然、前の発音音を切り落としたくありません。 それは非常にいでしょう。 前の音が鳴り、新しい音が並行して鳴り始めることが必要です。 したがって、ノートごとに複数のボイスが必要になります。 キーを非常にすばやく叩くと、多くの票が必要になります。
たとえば、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.hの
publicセクションで
、以下を追加します。
void reset() { mPhase = 0.0; }
これにより、音声が鳴り始めるたびに最初に波形を開始できます。
同時に、私たちがそこにいる間に、
privateセクションから
isMutedフラグを削除します。 コンストラクターの初期化リストからも削除し、
setMutedメンバー
setMutedを削除することを忘れないでください。
Voiceレベルでアクティビティレベルを監視するようになったため、オシレーターはそれを必要としなくなりました。
Oscillator::nextSampleからこの行を削除し
Oscillator::nextSample :
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関数を呼び出して音声を初期状態にリセットします。 この関数は、音声オシレーター、そのエンベロープジェネレーター、およびフィルターをリセットします。
静的か非静的か?
すべての投票のメンバー変数は同じです:
Oscillator : mOscillatorModeFilter : cutoff 、 resonance 、 modeEnvelopeGenerator : stageValue
最初は、そのような冗長性は悪であり、これらはすべて静的メンバーであると考えました。
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を動作状態にします。
コードは
ここからダウンロードでき
ます 。
元の投稿 。