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

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



エンベロープと波形を変更できるように、いくつかのコントロールを追加しましょう。 取得したい結果を次に示します( ここからパフTIFFをダウンロードできます)。



次のファイルをダウンロードしてプロジェクトにアップロードします。
bg.png
knob.png (ファイル作成者-Bootsie
Waveform.png

いつものように、リンクとIDをresource.hに追加します。

// Unique IDs for each image resource. #define BG_ID 101 #define WHITE_KEY_ID 102 #define BLACK_KEY_ID 103 #define WAVEFORM_ID 104 #define KNOB_ID 105 // Image resource locations for this plug. #define BG_FN "resources/img/bg.png" #define WHITE_KEY_FN "resources/img/whitekey.png" #define BLACK_KEY_FN "resources/img/blackkey.png" #define WAVEFORM_FN "resources/img/waveform.png" #define KNOB_FN "resources/img/knob.png" 


そして、背景画像のサイズと一致するようにウィンドウの高さを変更します。

 #define GUI_HEIGHT 296 


Synthesis.rcヘッダーに変更を加えます。

 #include "resource.h" BG_ID PNG BG_FN WHITE_KEY_ID PNG WHITE_KEY_FN BLACK_KEY_ID PNG BLACK_KEY_FN WAVEFORM_ID PNG WAVEFORM_FN KNOB_ID PNG KNOB_FN 


次に、エンベロープジェネレーターの波形とステージのパラメーターを追加する必要があります。 EParams Synthesis.cppに EParamsEParams

 enum EParams { mWaveform = 0, mAttack, mDecay, mSustain, mRelease, kNumParams }; 


仮想キーボードを下に移動する必要があります。

 enum ELayout { kWidth = GUI_WIDTH, kHeight = GUI_HEIGHT, kKeybX = 1, kKeybY = 230 }; 


OscillatorModeで、 OscillatorModeにモードの総数を追加する必要があります。

 enum OscillatorMode { OSCILLATOR_MODE_SINE = 0, OSCILLATOR_MODE_SAW, OSCILLATOR_MODE_SQUARE, OSCILLATOR_MODE_TRIANGLE, kNumOscillatorModes }; 


初期化リストで、正弦をデフォルト波形として指定します。

 Oscillator() : mOscillatorMode(OSCILLATOR_MODE_SINE), // ... 


コンストラクターでGUIをビルドします。 AttachGraphics(pGraphics)直前にこれらの行を追加します。

 // Waveform switch GetParam(mWaveform)->InitEnum("Waveform", OSCILLATOR_MODE_SINE, kNumOscillatorModes); GetParam(mWaveform)->SetDisplayText(0, "Sine"); // Needed for VST3, thanks plunntic IBitmap waveformBitmap = pGraphics->LoadIBitmap(WAVEFORM_ID, WAVEFORM_FN, 4); pGraphics->AttachControl(new ISwitchControl(this, 24, 53, mWaveform, &waveformBitmap)); // Knob bitmap for ADSR IBitmap knobBitmap = pGraphics->LoadIBitmap(KNOB_ID, KNOB_FN, 64); // Attack knob: GetParam(mAttack)->InitDouble("Attack", 0.01, 0.01, 10.0, 0.001); GetParam(mAttack)->SetShape(3); pGraphics->AttachControl(new IKnobMultiControl(this, 95, 34, mAttack, &knobBitmap)); // Decay knob: GetParam(mDecay)->InitDouble("Decay", 0.5, 0.01, 15.0, 0.001); GetParam(mDecay)->SetShape(3); pGraphics->AttachControl(new IKnobMultiControl(this, 177, 34, mDecay, &knobBitmap)); // Sustain knob: GetParam(mSustain)->InitDouble("Sustain", 0.1, 0.001, 1.0, 0.001); GetParam(mSustain)->SetShape(2); pGraphics->AttachControl(new IKnobMultiControl(this, 259, 34, mSustain, &knobBitmap)); // Release knob: GetParam(mRelease)->InitDouble("Release", 1.0, 0.001, 15.0, 0.001); GetParam(mRelease)->SetShape(3); pGraphics->AttachControl(new IKnobMultiControl(this, 341, 34, mRelease, &knobBitmap)); 


まず、タイプEnum mWaveformパラメーターを作成します。 デフォルトでは、その値はOSCILLATOR_MODE_SINEであり、合計kNumOscillatorModes値を持つことができます。 次に、Waveform.pngをロードします 。 ここで、 4はフレーム数を示します。 kNumOscillatorModes使用することもできますが、これも4つになりました。 しかし、新しい波形を追加し、Waveform.pngを変更しないと、すべてがクリープします。 ただし、これは、イメージを更新する必要があることを思い出させるのに役立ちます。
次に、 ISwitchControlを作成し、座標を渡してmWaveformパラメーターにバインドします。
1つのknob.pngファイルをアップロードし、4つのIKnobMultiControlsすべてに使用します。
SetShape使用して、小さい値ではノブの感度をSetShape 、大きい値では粗くします。 デフォルト値は、コンストラクターEnvelopeGeneratorと同じです。 ただし、他の最小値と最大値を選択できます。

値の変更の処理



覚えているように、ユーザーがパラメーターを変更したときの反応は、メインの.cppプロジェクトファイルのOnParamChange関数にOnParamChangeれます。

 void Synthesis::OnParamChange(int paramIdx) { IMutexLock lock(this); switch(paramIdx) { case mWaveform: mOscillator.setMode(static_cast<OscillatorMode>(GetParam(mWaveform)->Int())); break; case mAttack: case mDecay: case mSustain: case mRelease: mEnvelopeGenerator.setStageValue(static_cast<EnvelopeGenerator::EnvelopeStage>(paramIdx), GetParam(paramIdx)->Value()); break; } } 


mWaveformが変更されるmWaveform intmWaveform値はOscillatorMode型に変換されます。
ご覧のとおり、すべてのエンベロープパラメーターには1行があります。 EParamsEnvelopeStage enumsを比較すると、 EParamsの値がAttack、Decay、SustainReleaseの各ステージに対応していることがEParamsます。 したがって、 static_cast<EnvelopeGenerator::EnvelopeStage>(paramIdx)EnvelopeStageエンベロープの可変ステージを提供し、 GetParam(paramIdx)->Value()は可変ステージの値を提供します。 したがって、これら2つの引数を指定してsetStageValueを呼び出すだけです。 この関数のみがまだ書かれていません。 publicクラスEnvelopeGenerator追加します。

 void setStageValue(EnvelopeStage stage, double value); 


この関数が単純なセッターになることをちょっと想像してみてください:

 // This won't be enough: void EnvelopeGenerator::setStageValue(EnvelopeStage stage, double value) { stageValue[stage] = value; } 


攻撃段階でstageValue[ENVELOPE_STAGE_ATTACK]を変更するとどうなりますか? このような実装は、 calculateMultiplierを呼び出さず、 nextStageSampleIndexcalculateMultiplierしません。 ジェネレーターは、この段階で次回に新しい値のみを使用します。 SUSTAINでも同じです。メモを保持し、同時に希望のレベルを検索できるようにしたいと思います。
そのような実装は不便であり、そのようなプラグインはまったくプロフェッショナルではないように見えます。
対応するノブが回転している場合、ジェネレーターは現在のステージのパラメーターをすぐに更新する必要があります。 したがって、新しい時間引数を使用してcalculateMultiplierを呼び出し、新しい値nextStageSampleIndexを計算する必要があります。

 void EnvelopeGenerator::setStageValue(EnvelopeStage stage, double value) { stageValue[stage] = value; if (stage == currentStage) { // Re-calculate the multiplier and nextStageSampleIndex if(currentStage == ENVELOPE_STAGE_ATTACK || currentStage == ENVELOPE_STAGE_DECAY || currentStage == ENVELOPE_STAGE_RELEASE) { double nextLevelValue; switch (currentStage) { case ENVELOPE_STAGE_ATTACK: nextLevelValue = 1.0; break; case ENVELOPE_STAGE_DECAY: nextLevelValue = fmax(stageValue[ENVELOPE_STAGE_SUSTAIN], minimumLevel); break; case ENVELOPE_STAGE_RELEASE: nextLevelValue = minimumLevel; break; default: break; } // How far the generator is into the current stage: double currentStageProcess = (currentSampleIndex + 0.0) / nextStageSampleIndex; // How much of the current stage is left: double remainingStageProcess = 1.0 - currentStageProcess; unsigned long long samplesUntilNextStage = remainingStageProcess * value * sampleRate; nextStageSampleIndex = currentSampleIndex + samplesUntilNextStage; calculateMultiplier(currentLevel, nextLevelValue, samplesUntilNextStage); } else if(currentStage == ENVELOPE_STAGE_SUSTAIN) { currentLevel = value; } } } 


ネストされたifは、ジェネレーターがnextStageSampleIndexパラメーター(ATTACK、DECAYまたはRELEASE)によって時間制限の段階にあるかどうifチェックします。 nextLevelValueは、エンベロープが求める次の段階の信号レベルです。 その値は、 enterStage関数と同じ方法で設定されます。 switch後の最も興味深いのは、現在のステージで、ジェネレーターがこのステージの残りの新しい値に従って動作することです。 このため、現在のステージは過去と残りの部分に分割されます。 最初に、ジェネレーターがステージ内にすでにある時間を計算します。 たとえば、 0.1は10%が合格したことを意味します。 RemainingStageProcess 、それぞれ、 RemainingStageProcessている量を反映しています。 ここで、 samplesUntilNextStageを計算し、 samplesUntilNextStageを更新する必要があります。 そして最も重要なことは、 calculateMultiplierを呼び出して、 samplesUntilNextStageサンプルのcurrentLevelからnextLevelValuesamplesUntilNextStageです。
C SUSTAINは簡単ですcurrentLevel更新しcurrentLevel

このような実装は、考えられるほとんどすべてのケースをカバーしてます。 ジェネレーターがDECAYになっていて、SUSTAINの値がいつ変化するかを把握することは残っています。 現在は、レベルが古い値に低下し、低下の段階が終了すると、レベルが新しい値にジャンプするように作成されています。 これを回避するには、 setStageValueを最後に追加しsetStageValue

 if (currentStage == ENVELOPE_STAGE_DECAY && stage == ENVELOPE_STAGE_SUSTAIN) { // We have to decay to a different sustain value than before. // Re-calculate multiplier: unsigned long long samplesUntilNextStage = nextStageSampleIndex - currentSampleIndex; calculateMultiplier(currentLevel, fmax(stageValue[ENVELOPE_STAGE_SUSTAIN], minimumLevel), samplesUntilNextStage); } 


これで、新しいレベルにスムーズに移行できます。 ここでは、 Sustainに依存しないnextStageSampleIndexnextStageSampleIndexは変更しません。
プラグインを起動し、波形をクリックしてノブを回します-すべての変更はすぐにサウンドに反映されます。

パフォーマンスの改善



ProcessDoubleReplacingこの部分を見てください。

 int velocity = mMIDIReceiver.getLastVelocity(); if (velocity > 0) { mOscillator.setFrequency(mMIDIReceiver.getLastFrequency()); mOscillator.setMuted(false); } else { mOscillator.setMuted(true); } 


MIDIレシーバーのmLastVelocityリセットしないことに決めたのを覚えていますか? つまり、最初の音の後、音が鳴らなくてもmOscillatorは波を生成します。 forループを次のように変更します。

 for (int i = 0; i < nFrames; ++i) { mMIDIReceiver.advance(); int velocity = mMIDIReceiver.getLastVelocity(); mOscillator.setFrequency(mMIDIReceiver.getLastFrequency()); leftOutput[i] = rightOutput[i] = mOscillator.nextSample() * mEnvelopeGenerator.nextSample() * velocity / 127.0; } 


mEnvelopeGenerator.currentStage ENVELOPE_STAGE_OFF等しくない場合、 mEnvelopeGenerator.currentStage波を生成するのは論理的です。 そのため、 mEnvelopeGenerator.enterStageどこかで無効化生成を有効にする必要があります。 前の投稿で説明した理由により、ここから直接呼び出すことはありませんが、ここでもシグナルとスロットを使用します。 EnvelopeGenerator.hでクラスを定義する前に、次の行を追加します。

 #include "GallantSignal.h" using Gallant::Signal0; 


次に、いくつかのシグナルをpublic追加します。

 Signal0<> beganEnvelopeCycle; Signal0<> finishedEnvelopeCycle; 


EnvelopeGenerator.cppenterStage最初に以下を追加します。

 if (currentStage == newStage) return; if (currentStage == ENVELOPE_STAGE_OFF) { beganEnvelopeCycle(); } if (newStage == ENVELOPE_STAGE_OFF) { finishedEnvelopeCycle(); } 


最初のif 、ジェネレーターが同じ段階でループしないようにすることです。 他の2つの意味は次のとおりです。


さて、 Signalへの反応を書きましょう。 以下のprivate関数をSynthesis.hに追加します。

 inline void onBeganEnvelopeCycle() { mOscillator.setMuted(false); } inline void onFinishedEnvelopeCycle() { mOscillator.setMuted(true); } 


エンベロープサイクルが始まると、オシレーターに波を生成させます。 終了したら、それをかき消します。
Synthesis.cppのコンストラクタの最後で、信号をスロットに接続します。

 mEnvelopeGenerator.beganEnvelopeCycle.Connect(this, &Synthesis::onBeganEnvelopeCycle); mEnvelopeGenerator.finishedEnvelopeCycle.Connect(this, &Synthesis::onFinishedEnvelopeCycle); 


以上です! 起動時に、すべてが機能するはずです。 REAPERでは、Cmd + Alt + P(Macの場合)またはCtrl + Alt + P(Windowsの場合)を押すと、パフォーマンスモニターが表示されます。



赤は、プロセッサ上のトラックの総負荷を示します。 音が鳴り始めると、この値は増加し、最終的に落ち着くと落ちます。これは、オシレーターが無駄なサンプルを計算しなくなるためです。

これで、完全に受け入れ可能なエンベロープジェネレーターができました。
ここからコードをダウンロードできます。

次回は、同じく重要なシンセサイザーコンポーネント、フィルターを作成します!

元の記事:
martin-finke.de/blog/articles/audio-plugins-012-envelopes-gui

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


All Articles