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

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



SpaceBassのサウンドをさらに良くするには、エイリアシングの少ないオシレーターを作成する必要があります。 これはオプションの改善です。 これがないと、シンセサイザーは以前と同じように動作しますが、それを使用すると、上位オクターブの音がはるかに良くなります。

スペクトル分析



非常に優れた無料のプラグイン、 Voxengo SPANを紹介したいと思います。 トラックに掛けることができ、通過する信号のスペクトルを表示します。 この段階では、 FFTを使用して独自のテスト手順を作成するのは時期尚早であるため、SPANは発振器のさまざまなアルゴリズムの結果を比較するための不可欠なツールになります。 ダウンロードしてインストールします。 REAPERでSpaceBass起動し、次を実行します。



これらの設定では、最初のオシレーターの生の波形が聞こえます。 ここで、シンセサイザーの後に同じトラックにSPANを掛けます。 高い音(6オクターブを使用)を保持し、SPANのスペクトログラムを見ると、次のようになります。



それを読むことは非常に簡単です:x軸はヘルツで周波数を示します;このプラグインでは、66 Hzから20 kHzの周波数が表示されます。 スケールは対数 、つまり オクターブ間の距離は常に同じです-最初のオクターブと2番目から2番目の間は、7番目と8番目の間です。 隣接するオクターブの周波数の比率は2対1です。 一方、信号の高調波は、基本周波数に異なる整数を乗算 (または除算)したものです。 これは、高調波がx軸に沿って不均一に分布していることを意味します。
y軸はデシベル単位の振幅です。 したがって、特定の瞬間にどの周波数がどの振幅を持つかを判断するのは非常に簡単です。
設定によっては、あなたのスペクトログラムと私のスペクトログラムでいくつかの点が異なる場合がありますが、1つ確かなことがあります。 蛇行のスペクトログラム、つまり周波数が増加するにつれて振幅が減少する一連のピークが表示され、ピーク間には何も表示されないことが予想されました。 そして確かに、基本周波数(グラフの左側)より下のスペクトル成分が表示されることは期待していませんでした。 覚えているように、そのようなナンセンスはエイリアシングです。

それをどうしますか? ソリューションにはさまざまなアプローチがあります。 最も正確な解決策は、フーリエ級数のメンバーを合成して、蛇行を近似することです。 実際、これは、基本周波数から始まり、高調波ごとに1つの正弦である、適切に選択された振幅を持つ正弦のオーバーラップです。 しかし、 ナイキスト周波数に達すると 、高調波の合成を停止する必要があります。 このアプローチにより、理想的な帯域制限波形が得られ、そのすべてのスペクトル成分は厳密に基本周波数とナイキスト周波数内にあります。
当然、1つの問題があります。 この方法は、基本(基本)周波数が非常に高く、ナイキスト周波数の前に高調波がほとんど残っていない最高のオクターブに適しています。 しかし、低いオクターブには、控えめに言っても多くの高調波があります:44.1 kHzの周波数では、100 Hzの基本周波数を持つ蛇行は、ナイキストまで219の高調波を持ちます。つまり、合計で各サンプルを220回計算する必要があります。 ポリフォニックモデルでは、この数値に再生されるノートの数が乗算されます。 一方では、各ノートにサインを1回だけ追加する必要があります。 しかし、これは、音の変調がなければそうです。 そこに到達すると、頻度はすべてのサンプルを変更する可能性があるため、多くの作業を行う必要があります。

BLIPとBLEP



合成には他のアプローチもあります。 最も注目すべき:


最後の2つのアプローチは、波形の突然の変化によってのみエイリアシングが発生するという事実に基づいています。 私たちが合成する波形では、唯一の問題はこれらの突然の変化です。 紙やすりで木片のように挽くことができますか? 単純な丸めは、ライトローパスフィルター処理と同等ですが、それは必要なものではありません。 ナイキスト周波数に対して何もフィルタリングする必要はなく、その上に何もフィルターをかける必要はありません。まるで正弦波が重ね合わされているかのようです。
サインからの蛇行の合成は次のようになります。



青色は副鼻腔が互いに重なり合っていることを示し、赤色は結果として生じる帯域制限された蛇行を示します。 ご覧のとおり、これらは単なる角丸ではありません。 この波形には、特徴的な振動「リップル」があります。
簡略化するために、BLEPメソッドは、以前行ったように 、最初に波形を生成し、次にこのリップルを課します。 これにより、エイリアシングが排除されます(または大幅に抑制されます)。

上記のリンクをたどった場合、PolyBLEPメソッドが最も簡単であるとすでに推測しています。 使用します!

PolyBLEPOscillatorクラス



PolyBLEPOscillatorOscillatorなので、後者を公に継承します。

プロジェクトで新しいPolyBLEPOscillatorクラスを作成します。 前の記事を読んでいない場合は、 完成したプロジェクトをダウンロードして、その瞬間から始めてください。

クラス定義は次のようになります。

 #include "Oscillator.h" class PolyBLEPOscillator: public Oscillator { public: PolyBLEPOscillator() : lastOutput(0.0) { updateIncrement(); }; double nextSample(); private: double poly_blep(double t); double lastOutput; }; 


Oscillatorから宣伝をOscillatorます。 合成方法を変更するには、新しいメンバー関数nextSampleを定義します。 また、新しいprivate関数poly_blep追加します。これは、蛇行エッジで振動を生成します。 lastOutputは、最後に生成された値を保存します(これは三角波形の場合にのみ必要です)。
poly_blep実装を追加します

 // PolyBLEP by Tale // (slightly modified) // http://www.kvraudio.com/forum/viewtopic.php?t=375517 double PolyBLEPOscillator::poly_blep(double t) { double dt = mPhaseIncrement / twoPI; // 0 <= t < 1 if (t < dt) { t /= dt; return t+t - t*t - 1.0; } // -1 < t < 0 else if (t > 1.0 - dt) { t = (t - 1.0) / dt; return t*t + t+t + 1.0; } // 0 otherwise else return 0.0; } 


これは少しだまされているように見えるかもしれませんが、実際には、差に近い場合を除き、関数はほとんど常に0.0を返します。 最初のif 、期間の初めにあるelse ifであり、 else if場合は、ほとんど最後にある場合です。 これは、2つの期間の違いが1つしかないため、のこぎりの動作です。

nextSampleを実装する前に、発振器クラスの何かを変更する必要があります。 Oscillator.hnextSample関数を仮想化します。

 virtual double nextSample(); 


これは、サブクラスのnextSample関数の動作を変更できることを意味します。 重要なランタイムでvirtualコードを使用することは最適なソリューションではありません。 テンプレートを使用できます(そしてコードの重複を避けます)が、説明は単純なレベルのままにしておき、合成のトピックから注意をそらさないようにします。
private:protected: private:に変更しprotected: これにより、 mPhaseメンバー関数からmPhaseなどのパラメーターにアクセスできます。
poly_blepたように、 Oscillatorクラスのエイリアスを使用して波形を使用し、 poly_blepします。 現時点では、 nextSampleは波形を計算し、位相増分を実装します。 これらの無関係なものを分離する必要があります。
次のprotectedメンバー関数を追加します。

 double naiveWaveformForMode(OscillatorMode mode); 


この関数は、エイリアスを使用して波形を計算します。 ここでナイーブとは、波形が単純で誤った方法で生成されることを意味します。 Oscillator.cppで書きましょう( Oscillator::nextSampleとほとんど同じなので、コピーするだけOscillator::nextSample

 double Oscillator::naiveWaveformForMode(OscillatorMode mode) { double value; switch (mode) { case OSCILLATOR_MODE_SINE: value = sin(mPhase); break; case OSCILLATOR_MODE_SAW: value = (2.0 * mPhase / twoPI) - 1.0; break; case OSCILLATOR_MODE_SQUARE: if (mPhase < mPI) { value = 1.0; } else { value = -1.0; } break; case OSCILLATOR_MODE_TRIANGLE: value = -1.0 + (2.0 * mPhase / twoPI); value = 2.0 * (fabs(value) - 0.5); break; default: break; } return value; } 


以下のOscillator::nextSample違い:


なぜなら この関数には、 Oscillator::nextSampleからのすべてのコードが含まれており、 nextSample本体をこれに置き換えます。

 double Oscillator::nextSample() { double value = naiveWaveformForMode(mOscillatorMode); mPhase += mPhaseIncrement; while (mPhase >= twoPI) { mPhase -= twoPI; } return value; } 


ここでは、単純に波形を計算するためにnaiveWaveformForModeが呼び出され、 mPhase

PolyBLEP生成



PolyBLEPOscillator.cppに戻ってnextSampleを書きましょう。 このように始めましょう:

 double PolyBLEPOscillator::nextSample() { double value = 0.0; double t = mPhase / twoPI; if (mOscillatorMode == OSCILLATOR_MODE_SINE) { value = naiveWaveformForMode(OSCILLATOR_MODE_SINE); } else if (mOscillatorMode == OSCILLATOR_MODE_SAW) { value = naiveWaveformForMode(OSCILLATOR_MODE_SAW); value -= poly_blep(t); } 


変数tpoly_blep関数が機能するために必要です。 これは、現在の位相値をtwoPIで除算したtwoPIであるため、常に01間です。 波形を分離する最初のif 。 正弦波にはアンチエイリアシングは必要ありません。これは、1次高調波(メイン周波数自体)しか持たないためです。 のこぎりについては、まず発振器から単純な波形を取得し、次にpily_blepを配置します-それだけです!
このような三角形を作成しましょう。最初に蛇行し、それを統合します。 離散値を扱うため、統合とは単に値を加算することを意味します。 推定する場合、蛇行はソリッドユニットで始まるため、それらの合計は線形増分になります。 半サイクル後、マイナス1が連続しているため、それらの統合は直線的に低下します。 三角形はまさにこれです:線形成長と線形減少。
これを念頭に置いて、蛇行と三角形のコードをすぐに記述します。

  else { value = naiveWaveformForMode(OSCILLATOR_MODE_SQUARE); value += poly_blep(t); value -= poly_blep(fmod(t + 0.5, 1.0)); 


繰り返しますが、エイリアシングを伴う単純な波から始めます。 しかし、今回は2つの PolyBLEPを課しています。 1つは期間の開始のため、もう1つは0.5期間シフトされます。 蛇行には2つのドロップがあります。 のこぎりの違いは1つだけです。
不足している唯一のものは三角形です。 elseブロックの最後に追加します。

  if (mOscillatorMode == OSCILLATOR_MODE_TRIANGLE) { // Leaky integrator: y[n] = A * x[n] + (1 - A) * y[n-1] value = mPhaseIncrement * value + (1 - mPhaseIncrement) * lastOutput; lastOutput = value; } 


前に、蛇行を統合することを書きました。 これは完全に正確ではありません。 単純に統合すると、出力値が膨大になります。つまり、膨大なオーバーロードが発生します。 代わりに、漏れやすい積分器を使用する必要があります。 新しい値と古い値を合計しますが、1よりわずかに小さい値を乗算します。 したがって、値はスケール外ではありません。
位相増分を追加しましょう(すべてが以前と同じです):

  } mPhase += mPhaseIncrement; while (mPhase >= twoPI) { mPhase -= twoPI; } return value; } 


PolyBLEPOscillatorを作成するのはとても簡単PolyBLEPOscillator

新しいオシレーターを使用する



光沢のある新しいPolyBLEPOscillatorを使用するには、 PolyBLEPOscillatorを変更するだけです。 #include "Oscillator.h"#include "PolyBLEPOscillator.h"置き換えます。
privateセクションで、 mOscillatorOnemOscillatorTwoPolyBLEPOscillatorクラスのオブジェクトにmOscillatorTwoします。

 PolyBLEPOscillator mOscillatorOne; PolyBLEPOscillator mOscillatorTwo; 


以上です! プラグインを起動して、スペクトルを見てみましょう。 ご覧のとおり、エイリアシングの影響は非常に顕著に除去されています。 比較の前後のスクリーンショット:

見た:
画像
画像

蛇行:
画像
画像

三角形:
画像
画像

しかし、LFOはどうですか?



私たちはまだLFOに古いOscillatorを使用しています。 PolyBLEPOscillatorに切り替える必要がありますか? 実際、LFOでは鋭い境界線が非常に望ましいため、興味深い効果を得ることができます。 そしてエイリアシングは私たちを本当に気にしません。 通常、基本周波数は低く、30 Hz未満です。 次の各高調波の振幅は前の高調波よりも低いため、ナイキストより上の周波数の振幅は非常に小さくなります。

まとめ



蛇行を生成し、PolyBLEPが適用されたエイリアス波形を使用して、エイリアスなしで見ました。 三角形は、エイリアシングのない準積分器と蛇行器を使用して生成されました。 これで、最高のオクターブでシンセを楽しく演奏でき、不調和な周波数が発生することを恐れることはありません!

コードはここからダウンロードできます

読んでくれてありがとう! :)

元の投稿

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


All Articles