オーディオプラグインの作成、パート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に EParams



EParams







 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



int



mWaveform



値はOscillatorMode



型に変換されます。

ご覧のとおり、すべてのエンベロープパラメーターには1行があります。 EParams



EnvelopeStage 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



を呼び出さず、 nextStageSampleIndex



calculateMultiplier



しません。 ジェネレーターは、この段階で次回に新しい値のみを使用します。 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



からnextLevelValue



samplesUntilNextStage



です。

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に依存しないnextStageSampleIndex



nextStageSampleIndex



は変更しません。

プラグインを起動し、波形をクリックしてノブを回します-すべての変更はすぐにサウンドに反映されます。



パフォーマンスの改善





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



All Articles