パート1.紹介とセットアップ
パート2.コードの学習
パート3. VSTおよびAU
パート4.デジタル歪み
パート5.プリセットとGUI
パート6.信号合成
パート7. MIDIメッセージの受信
パート8.仮想キーボード
パート9.封筒
パート10. GUIの改善
パート11.フィルター
パート12.低周波発振器
パート13.再設計
パート14.ポリフォニー1
パート15.ポリフォニー2
パート16.アンチエイリアス
エンベロープと波形を変更できるように、いくつかのコントロールを追加しましょう。 取得したい結果を次に示します( ここからパフTIFFをダウンロードできます)。
![](https://habrastorage.org/getpro/habr/post_images/b38/9d5/72c/b389d572c31ccc1bde54fb246ff28a16.png)
次のファイルをダウンロードしてプロジェクトにアップロードします。
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、Sustain 、 Releaseの各ステージに対応していることが
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.cppの
enterStage
最初に以下を追加します。
if (currentStage == newStage) return; if (currentStage == ENVELOPE_STAGE_OFF) { beganEnvelopeCycle(); } if (newStage == ENVELOPE_STAGE_OFF) { finishedEnvelopeCycle(); }
最初の
if
、ジェネレーターが同じ段階でループしないようにすることです。 他の2つの意味は次のとおりです。
- OFFステージを終了すると、新しいサイクルが開始されます
- OFFと入力すると、サイクルが終了します
さて、
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