パート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) { // 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.hの
public
セクションで、以下を追加します。
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
関数を呼び出して音声を初期状態にリセットします。 この関数は、音声オシレーター、そのエンベロープジェネレーター、およびフィルターをリセットします。
静的か非静的か?
すべての投票のメンバー変数は同じです:
-
Oscillator
:mOscillatorMode
-
Filter
:cutoff
、resonance
、mode
-
EnvelopeGenerator
: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を動作状態にします。
コードはここからダウンロードできます 。
元の投稿 。