メインコンテンツまでスキップ

チュートリアルサイン波シンセサイザーを作る

📚 Source Page

このチュートリアルでは、簡単なサイン波合成を紹介します。正弦波発振器の状態を管理し、オーディオ出力にデータを書き込む方法を紹介します。

レベル:中級

プラットフォーム:Windows, macOS, Linux

クラス: AudioAppComponent,Slider,MathConstants

スタート

注記

このチュートリアルはTutorial: Control audio levelsそれは最初に読んで理解すべきだった。

このチュートリアルのデモ・プロジェクトのダウンロードはこちらから:PIP|ZIP.プロジェクトを解凍し、最初のヘッダーファイルをProjucerで開く。

このステップにヘルプが必要な場合は、以下を参照してください。Tutorial: Projucer Part 1: Getting started with the Projucer.

デモ・プロジェクト

このデモ・プロジェクトは、Projucerのオーディオ・アプリケーション・テンプレートに基づいています。正弦波の周波数をコントロールするためのスライダーが1つ表示されます。

正弦波の生成

このチュートリアルでは、標準ライブラリ関数std::sin().これを使用するには、電流を保存して正弦波生成の状態を維持する必要があります。位相角と、出力サンプルごとに位相角がインクリメントされる必要がある量です。このサンプルごとの変化の大きさ(「デルタ」)は、出力のサンプル・レートと、生成したい正弦波の周波数に依存します。

警告

ほとんどの合成アプリケーションやプラグインではstd::sin()最も効率的な手法ではないだろう。一般的にwavetableを参照されたい。Tutorial: Wavetable synthesis.ウェーブテーブルは正弦波以外の波形も扱うことができる。

国家の維持

我々のMainContentComponentクラスには3つのdoubleメンバー[1]:

    double currentSampleRate = 0.0, currentAngle = 0.0, angleDelta = 0.0; // [1]

を更新する単純な関数がある。angleDeltaメンバーだ:

    void updateAngleDelta()
{
auto cyclesPerSample = frequencySlider.getValue() / currentSampleRate; // [2]
angleDelta = cyclesPerSample * 2.0 * juce::MathConstants::pi; // [3]
}
  • [2]まず、各出力サンプルに必要なサイクル数を計算します。
  • [3]そして、これに正弦波1周期の長さを掛けると、次のようになる。2piラジアン

この関数が正しく動作する前に、出力サンプルレートを知る必要がある。これは、サンプルの生成頻度を知る必要があるためで、1サンプルあたりに必要な変化量を知るためです。サンプルレートはAudioAppComponent::prepareToPlay()コールバック関数:

    void prepareToPlay (int, double sampleRate) override
{
currentSampleRate = sampleRate;
updateAngleDelta();
}

ここでは、サンプルレート値のコピーを保存しupdateAngleDelta()関数は当初は

スライダーの値を使う

アプリの実行中にスライダーが動いたらangleDeltaまたメンバーだ:

        frequencySlider.onValueChange = [this]
{
if (currentSampleRate > 0.0)
updateAngleDelta();
};

を呼び出す前に、サンプルレートが有効かどうかをチェックする。updateAngleDelta()関数を再び使用する。

オーディオの出力

期間中getNextAudioBlock()コールバックで実際のサイン波を生成し、それを出力に書き出す必要がある:

    void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
auto level = 0.125f;
auto* leftBuffer = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample);
auto* rightBuffer = bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample);

for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
{
auto currentSample = (float) std::sin (currentAngle);
currentAngle += angleDelta;
leftBuffer[sample] = currentSample * level;
rightBuffer[sample] = currentSample * level;
}
}

出力サンプルごとに、現在の角度に対するサイン関数を計算し、次のサンプルの角度をインクリメントする。レベルを0.125フルスケールの正弦波は非常に大きくなる!私たちは次のことができる(そしておそらくそうすべきだ)。ラップに達すると、現在の角度値をゼロに戻す。2pi.より大きな値はまだ有効な値を返すので、実際にはこの計算を避けることができる。次の画像のようなものが得られる:

位相角をラジアン単位で示すフルスケール±1.0の正弦波。
位相角をラジアン単位で示すフルスケール±1.0の正弦波。

スライダーの構成

スライダーの値が非線形に変化することにお気づきかもしれない(そうでなければ、今すぐ試してみるべきだ)。実はこの変化は対数的なものなのです。これにより、小さい値では分解能が高くなり、大きい値では分解能が低くなります。周波数値をコントロールする場合、この方法が適切なことがよくあります(音楽的には、リニアに等しく変化するよりも、周波数間の比率で等しく変化するように聞こえるからです)。この設定はSlider::setSkewFactorFromMidPoint()機能[4].私たちのスライダーの範囲は次のように設定されている。50..5000したがって、スライダートラックの中心を500ということは、スライダーの最小値と中央、中央と最大値の間に等しい音楽的間隔があることを意味する:

    MainContentComponent()
{
addAndMakeVisible (frequencySlider);
frequencySlider.setRange (50.0, 5000.0);
frequencySlider.setSkewFactorFromMidPoint (500.0); // [4]

についてスキュー係数を使って直接設定できます。Slider::setSkewFactor()関数は、中間点でどのような値が欲しいかを考える方が簡単な場合が多い。

エクササイズ

アプリケーションに別のスライダーを追加して、サイン波のレベルをコントロールする。レベルを1.0- の最大値である。0.25でいいはずだ。

周波数の変化を滑らかにする

スライダを動かすと、特に高い周波数で、耳に聞こえる、おそらく不要なアーチファクトが発生することにお気づきでしょう。これは、スライダが実際には離散的なステップで変化しており、スライダを素早く動かすとこのステップがかなり大きくなるためです。これに加えて、スライダーの周波数はオーディオブロックごとにしか更新されないため、これらの変化の正確な効果はハードウェアのブロックサイズに依存します。

スムージング用ステート・メンバー

ひとつは、合成に使われている現在の周波数を保存するためのもので、もうひとつは、合成に使われている現在の周波数を保存するためのものだ。ターゲットユーザーがスライダーを動かして要求した頻度。その後、これらの値の間をよりゆっくりとランプして、アーチファクトを除去することができる:

    double currentFrequency = 500.0, targetFrequency = 500.0; // [5]

これらの値を同時に初期化する[5].スライダーを同じ値に初期化することもできます。[6]:

    MainContentComponent()
{
addAndMakeVisible (frequencySlider);
frequencySlider.setRange (50.0, 5000.0);
frequencySlider.setSkewFactorFromMidPoint (500.0);
frequencySlider.setValue (currentFrequency, juce::dontSendNotification); // [6]

合成コードの更新

このアルゴリズムが機能する鍵は、現在値と目標値が同じか異なるかをチェックすることである。もし同じなら、単純に元のコードをangleDeltaメンバーは変更する必要はない。もし現在値と目標値が異なるなら、その時点でangleDelta各サンプルのメンバーは、現在の値を徐々に目標値に近づけていく。

警告

この例では、出力バッファのサンプル数をランプの長さとして使用します。つまり、バッファサイズが非常に小さい場合、アーティファクトが聞こえる可能性があります。

    void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
auto level = 0.125f;
auto* leftBuffer = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample);
auto* rightBuffer = bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample);

auto localTargetFrequency = targetFrequency;

if (! juce::approximatelyEqual (localTargetFrequency, currentFrequency)) // [7]
{
auto frequencyIncrement = (localTargetFrequency - currentFrequency) / bufferToFill.numSamples; // [8]

for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
{
auto currentSample = (float) std::sin (currentAngle);
currentFrequency += frequencyIncrement; // [9]
updateAngleDelta(); // [10]
currentAngle += angleDelta;
leftBuffer[sample] = currentSample * level;
rightBuffer[sample] = currentSample * level;
}

currentFrequency = localTargetFrequency;
}
else // [11]
{
for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
{
auto currentSample = (float) std::sin (currentAngle);
currentAngle += angleDelta;
leftBuffer[sample] = currentSample * level;
rightBuffer[sample] = currentSample * level;
}
}
}
  • [7]ターゲットが現在の値と異なるかどうかをチェックする。この関数が実行されている間にスライダーがメッセージスレッド上で値を変更した場合に備えて、ターゲット値のローカルコピーを取っていることに注意してください。
  • [8]1サンプルに必要なインクリメントを計算します。
  • [9]現在の周波数をインクリメントする。
  • [10]を更新する。deltaAngleこの新しい周波数に基づくメンバー。
  • [11]そうでない場合は、オリジナルのコードを使用してください。
注記

このコードの書式は、以下の典型的なパターンを使用しています。DSPコードを使用します。内部では条件文は使わない。for()ループを使用する。その代わりに、条件をループの外でテストさせ、パラメータが変化しているかどうかによって、2つの異なる、しかしよく似たループを使う。

最後にSlider::onValueChangeヘルパー・オブジェクトはターゲット値を更新するだけである:

        frequencySlider.onValueChange = [this] { targetFrequency = frequencySlider.getValue(); };

これで終わりです!スライダーの動きによるアーチファクトが取り除かれているはずです。

エクササイズ

以前の練習で追加したレベル・スライダ・コントロールにスムージングを追加します。

備考

  • 位相角を巻き込まない2piは、すべての状況において理想的であるとは限りません。もしfloatよりもdouble変数を使用すると、現在の角度値が非常に大きくなったときに、計算が不正確になる可能性がある。で位相をラップしないことで2piを使用している。std::sin()関数は、単純なウェーブテーブル技法に比べ、それなりにうまく機能する。参照Tutorial: Wavetable synthesisを参照されたい。

概要

このチュートリアルでは、サイン波の合成と制御の基本的な方法をいくつか紹介しました。ここでは

  • 正弦波発振器の状態を維持するために必要不可欠な変数。
  • これらの変数をどのように設定すれば、望ましい結果が得られるか。
  • オーディオ・アーティファクトを避けるためにパラメーターの変化を滑らかにする方法。

こちらも参照