チュートリアルウェーブテーブルシンセシス
シンセサイザーのオシレーターを最適化するためにウェーブテーブルを組み込みます。ウェーブテーブルを使ってサイン波オシレーターの状態を管理し、オーディオ出力にデータを書き込もう。
レベル:中級
プラットフォーム:Windows, macOS, Linux
クラス: AudioBuffer,AudioAppComponent,Random,MathConstants
スタート
このチュートリア ルのデモ・プロジェクトのダウンロードはこちらから:PIP|ZIP.プロジェクトを解凍し、最初のヘッダーファイルをProjucerで開く。
このステップにヘルプが必要な場合は、以下を参照してください。Tutorial: Projucer Part 1: Getting started with the Projucer.
デモ・プロジェクト
このデモ・プロジェクトは、ランダムなサイン波高調波のスタックを生成し、ステレオ出力で出力するだけです。ユーザー・インターフェースでは、従来のオシレーターの実装とウェーブテーブルを利用した実装を比較することで、CPU使用率をモニターすることができる。
異なる実装のCPU使用率を適切に評価・比較するために、アプリケーションをリリース通常のデバッグテストや開発時に使用されるコンフィギュレーション。プロジェクトをリリースモードでは、コンパイラーはコードからアサーションやコメントを削除したり、関数をインライン化したりすることで、可能な限りコードを最適化することができる。
Xcode でビルド構成を変更するには、まず、インターフェースの左上隅にあるデプロイメントターゲットをクリックし、次の場所に移動します。**編集スキーム...**以下の通りである:
ポップアップ・ウィンドウでリリースでのビルド構成コンボ・ボックスはスクリーン・ショットのとおりである:
あなたのアプリケーションは、コンパイラによる最適化後に実行され、CPU使用率は大幅に減少するはずです。
ウェーブテーブル
ウェーブテーブル・シンセシスとは、あらかじめ周期波形を埋め込んだルックアップテーブルを使用し、計算されたサンプルごとに同じ波形を生成することなくオシレーターを生成するシンセシス手法です。ウェーブテーブルは任意の周期波形で初期化され、これらの波形の分解能を指定することができます。出力する正しいサンプル値を取得する際、テーブル内のサンプル数とオーディオ・バッファ・ブロック内のサンプル数、および対応する要求周波数が一致しない場合は、2つのウェーブテーブル・サンプル間を補間して値を見つけます。
例として、ウェーブテーブルからサイン波を検索したいとします。まず、例えば128サンプル・ポイントの分解能で、サイン波の1サイクル分のウェーブテーブルを作成します。 バッファ・ブロックの各サンプルについて、サンプル・レート、再生要求周波数、ウェーブテーブルの解像度、波形の現在の位相または角度の組み合わせを使って、正しい補間サンプルを計算することで、正弦波のサンプル値を要求することができます。
ウェーブテーブルに入る前に、簡単なサイン波オシレーターの実装から始めよう。
正弦波発振器
このセクションの詳細はTutorial: Build a sine wave synthesiserもしこれらのステップで助けが必要な場合は、まずそのチュートリアルを参照してください。
の中でSineOscillator
クラスでは、波形サイクルの現在の角度または位相と、周波数とサンプル・レートに応じて各サイクル間で増分する角度デルタを格納する2つのメンバ変数を追跡します:
class SineOscillator
{
public:
SineOscillator() {}
//...
private:
float currentAngle = 0.0f, angleDelta = 0.0f;
};
についてsetFrequency()
関数を使えば、まず周波数をサンプルレートで割り、その結果に2pi(ラジアン単位の1サイクルの長さ)を掛けることで、角度デルタを計算することができる:
void setFrequency (float frequency, float sampleRate)
{
auto cyclesPerSample = frequency / sampleRate;
angleDelta = cyclesPerSample * juce::MathConstants::twoPi;
}
についてgetNextSample()
関数が呼び出される。getNextAudioBlock()
の機能である。AudioSourceを使用してサンプル値を計算します。ここではstd::sin()
関数に現在の角度を引数として渡し、ヘルパー関数を呼び出して現在の角度を更新する。updateAngle()
の後に定義されている:
forcedinline float getNextSample() noexcept
{
auto currentSample = std::sin (currentAngle);
updateAngle();
return currentSample;
}
角度は、周波数を設定する際に以前に計算された角度デルタで現在の角度をインクリメントし、角度が2piを超えたときに値を折り返すことによって更新される:
forcedinline void updateAngle() noexcept
{
currentAngle += angleDelta;
if (currentAngle >= juce::MathConstants::twoPi)
currentAngle -= juce::MathConstants::twoPi;
}
では、次の実装に切り替えよう。MainContentComponent
クラスである。
ここに示すように、出力全体のレベルとオシレーターの配列をプライベート・メンバー変数として管理している:
class MainContentComponent : public juce::AudioAppComponent,
public juce::Timer
{
//...
private:
//...
float level = 0.0f;
juce::OwnedArray oscillators;
//...
};
の中でprepareToPlay()
関数では、オシレーターを初期化し、サンプルレートに基づいて再生する周波数を次のように設定する必要があります:
void prepareToPlay (int, double sampleRate) override
{
auto numberOfOscillators = 200; // [1]
for (auto i = 0; i < numberOfOscillators; ++i)
{
auto* oscillator = new SineOscillator(); // [2]
auto midiNote = juce::Random::getSystemRandom().nextDouble() * 36.0 + 48.0; // [3]
auto frequency = 440.0 * pow (2.0, (midiNote - 69.0) / 12.0); // [4]
oscillator->setFrequency ((float) frequency, (float) sampleRate); // [5]
oscillators.add (oscillator);
}
level = 0.25f / (float) numberOfOscillators; // [6]
}
- [1]まず、多数の発振器を定義し、そのCPU負荷を評価します。
- [2]各オシレーターに対して、新しい
SineOscillator
正 弦波の単一音声を生成するオブジェクト。 - [3]を使用してランダムなMIDIノートを選択することもできます。Randomクラスの最低音を4オクターブずらし、その最低音を起点に3オクターブの音域を定義する。
- [4]そのミディ・ノートの周波数を計算するには、簡単な数式を使ってA440の周波数に乗じるスカラーを求めます。A440のミディ・ノート番号は69であることがわかっているので、ミディ・ノート番号から69を引くと、A440からの半音の距離が求まり、それを以下の式に当てはめることができる:440 * 2 ^ (d / 12)
- [5]次に、発振器の周波数を設定する。
setFrequency()
関数を使います。また、そのオシレーターをオシレーターの配列に追加する。 - [6]最後に、このような多数のオシレーター・サンプルの合計による信号のクリッピングを防ぐために、静かなゲイン・レベルをオシレーターの数で割って出力レベルを定義します。
の中でgetNextAudioBlock()
関数では、単純にすべてのオシレーターサンプルを合計し、以下のように結果を出力バッファに書き込みます:
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
auto* leftBuffer = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample); // [7]
auto* rightBuffer = bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample);
bufferToFill.clearActiveBufferRegion();
for (auto oscillatorIndex = 0; oscillatorIndex < oscillators.size(); ++oscillatorIndex)
{
auto* oscillator = oscillators.getUnchecked (oscillatorIndex); // [8]
for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
{
auto levelSample = oscillator->getNextSample() * level; // [9]
leftBuffer[sample] += levelSample; // [10]
rightBuffer[sample] += levelSample;
}
}
}
- [7]まず、出力バッファに書き込むための左右のチャンネルポインタを取得する。
- [8]配列内の各オシレーターについて、オシレーターのインスタンスへのポインターを取得します。
- [9]次に、オーディオ・サンプル・バッファの各サンプルについて、サイン波のサンプルを取得し、レベル変数でゲインをトリミングする。
- [10]最後に、そのサンプル値を左右のチャンネルのサンプルに足して、他のオシレーターと合計することができる。
今、アプリケーションを実行すると、正弦波を積み重ねたランダムなノイズが聞こえるはずだ。
ランダムなMIDIノートを生成する代わりに、あるコードのMIDIノートを見つけ、そのコードからランダムなノートを生成する。
ウェーブテーブル・オシレーター
オシレーターの実装をウェーブテーブル・シンセシス方式に変えてみよう。
の中でMainContentComponent
クラスのメンバ変数としてAudioSampleBufferを追加し、正弦波1サイクルのウェーブテーブル値を保持します。[1].また、ビットシフト演算子を用いて、ウェーブテーブルの分解能を128サンプルの定数として定義します。[2]:
private:
juce::Label cpuUsageLabel;
juce::Label cpuUsageText;
const unsigned int tableSize = 1 << 7; // [2]
float level = 0.0f;
juce::AudioSampleBuffer sineTable; // [1]
juce::OwnedArray oscillators;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};
という新しい関数を定義する。createWavetable()
で呼び出される。MainContentComponent
コンストラクタでオーディオ処理を開始する前に
void createWavetable()
{
sineTable.setSize (1, (int) tableSize);
auto* samples = sineTable.getWritePointer (0); // [3]
auto angleDelta = juce::MathConstants::twoPi / (double) (tableSize - 1); // [4]
auto currentAngle = 0.0;
for (unsigned int i = 0; i < tableSize; ++i)
{
auto sample = std::sin (currentAngle); // [5]
samples[i] = (float) sample;
currentAngle += angleDelta;
}
}
- [3]この関数では、AudioSampleBufferを初期化します。
setSize()
メソッドに、必要なのは1つのチャンネルだけで、サンプル数はテーブル・サイズと同じであることを指定する。そして、その単一チャンネル・バッファの書き込みポインタを取り出す。 - [4]次に、前節と同様に角度デルタを計算するが、今回はテーブルサイズを使用するため、全2pi周期を127で割る。
- [5]今度は、ウェーブテーブルの各ポイントについて、正弦波の値を
std::sin()
関数を呼び出すと、その値がバッファサンプルに代入され、現在の角度がデルタ値分インクリメントされる。
この関数呼び出しをMainContentComponent
コンストラクタを次のように使用する:
MainContentComponent()
{
cpuUsageLabel.setText ("CPU Usage", juce::dontSendNotification);
cpuUsageText.setJustificationType (juce::Justification::right);
addAndMakeVisible (cpuUsageLabel);
addAndMakeVisible (cpuUsageText);
createWavetable();
setSize (400, 200);
setAudioChannels (0, 2); // no inputs, two outputs
startTimer (50);
}
これでウェーブテーブルは、正弦波の全サイクルの128サンプルを含むようになります。
のfor()ループではprepareToPlay()
関数をインスタンス化するために、以下の行を変更する。WavetableOscillator
オブジェクトの代わりにSineOscillator
オブジェクトがある:
for (auto i = 0; i < numberOfOscillators; ++i)
{
auto* oscillator = new WavetableOscillator (sineTable);
このコンストラクタは、サウンド生成に使用するウェーブテーブルを引数にとり、対応する新しいWavetableOscillator
クラスは以下のようになる:
class WavetableOscillator
{
public:
WavetableOscillator (const juce::AudioSampleBuffer& wavetableToUse)
: wavetable (wavetableToUse)
{
jassert (wavetable.getNumChannels() == 1);
}
private:
const juce::AudioSampleBuffer& wavetable;
float currentIndex = 0.0f, tableDelta = 0.0f;
};
現在の角度と波形サイクルの角度デルタを記録する代わりに、現在のウェーブテーブル・インデックスとウェーブテーブルの角度デルタを格納する2つのメンバ変数を定義する。また、AudioSampleBuffer変数を定義して、使用するウェーブテーブルへの参照を保持する。
についてsetFrequency()
の機能である。WavetableOscillator
クラスは、2piのラジアン単位の全サイクルの代わりにウェーブテーブルのサイズを使って角度デルタを計算する点を除けば、以前に実装されたものとかなり似ている:
void setFrequency (float frequency, float sampleRate)
{
auto tableSizeOverSampleRate = (float) wavetable.getNumSamples() / sampleRate;
tableDelta = frequency * tableSizeOverSampleRate;
}