ウェーブテーブルの実装について非常に気になります!
シンセではよく使う技術っぽいから、押さえておきたいね~
JUCEチュートリアル、Synthの項目、「Wavetable synthesis」を進めていきます。
今回は、「Wavetable Oscillator」の項目です。前回のサイン波オシレータを、ウェーブテーブルの仕組みを導入したオシレータに作り変えるような実装を行います。
公式ドキュメントからダウンロードできる該当サンプルは、「WavetableSynthTutorial_02.h」となります。
前回のチュートリアルで作成したプログラムの改良となります。
こんな人の役に立つかも
・JUCEでプログラミングの勉強をしている人
・JUCEチュートリアル「Wavetable synthesis」を勉強している人
・JUCEでウェーブテーブルの仕組みを実装したい人
ウェーブテーブルの仕組みに変更
前回のサイン波オシレータクラスを変更して、ウェーブテーブルの仕組みにより、サイン波を出力する「WavetableOscillator」クラスというものを実装していきます。アプリの挙動としては、前回と同じものとなりますが、ウェーブテーブル方式にすることで、CPUの負荷などが少なくなる模様です。
MainComponent.h
MainComponentクラス
MainComponentクラスにはいくつかの変更を加えます。
[0]で、タイマークラスを継承しておきます。
class MainComponent : public juce::AudioAppComponent,
public juce::Timer//[0]タイマーを継承します。
{
public:
//...略...
void createWavetable();//[1]ウェーブテーブルのサンプル作成関数です。
private:
juce::Label cpuUsageLabel;
juce::Label cpuUsageText;
const unsigned int tableSize = 1 << 7;//[2]テーブルサイズです。
float level = 0.0f;
juce::AudioSampleBuffer sineTable;//[3]ウェーブテーブルです。
//juce::OwnedArray<SineOscillator> oscillators;
juce::OwnedArray<WavetableOscillator> oscillators;//[4]これから実装するオシレータクラスです。
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};
[1]で、後程cppファイルに中身を実装する「createWaveTable」関数を定義しておきます。createWaveTable関数では、サイン波のデータを作成する関数です。
[2]、[3]はウェーブテーブルで利用するための変数です。[2]は、シフト演算で1という整数を左に7回シフトして、128のビットを1とすることで整数の128になりますので、128という整数となります。ウェーブテーブルのサンプルサイズを表現します。
ここでシフト演算を利用した意味はあまりよくわかりません。
[4]は、以前SineOscillatorクラスとして定義したサイン波オシレータを今回は「WavetableOscillator」という名称で再度作成しなおすので、配列の対象となるクラスをWavetableOscillatorに変更しています。
WavetableOscillatorクラス
SinewaveOscillatorクラスと構成は似ていますが、計算方式が違うWavetableOscillatorクラスというものを新たに作成しました。ヘッダーファイルに追加します。MainComponentクラスの前に作成しました。
class WavetableOscillator
{
public:
WavetableOscillator(const juce::AudioSampleBuffer& wavetableToUse)
: wavetable(wavetableToUse)
{
jassert(wavetable.getNumChannels() == 1);
}
//[2]周波数とサンプルレートでtableDeltaを求める関数です。
void setFrequency(float frequency, float sampleRate)
{
auto tableSizeOverSampleRate = (float)wavetable.getNumSamples() / sampleRate;
tableDelta = frequency * tableSizeOverSampleRate;
}
//[3]実際に音声のゲインを計算する処理です。
forcedinline float getNextSample() noexcept
{
//[3-1]ウェーブテーブルの音声のサンプル数を取得します。
auto tableSize = (unsigned int)wavetable.getNumSamples();
//[3-2]currentIndexで、前の処理のインデックス番号を引き継ぎます。
auto index0 = (unsigned int)currentIndex;
//[3-3]index1として、index0の次のサンプル番号を求めます。
auto index1 = index0 == (tableSize - 1) ? (unsigned int)0 : index0 + 1;
auto frac = currentIndex - (float)index0;
//[3-4]ウェーブテーブルのサンプル値(ゲイン)を取得します。
auto* table = wavetable.getReadPointer(0);
auto value0 = table[index0];
auto value1 = table[index1];
//[3-5]現在のサンプル値を計算します。
auto currentSample = value0 + frac * (value1 - value0);
//[3-6]次のcurrentIndexを設定します。
if ((currentIndex += tableDelta) > (float)tableSize)
currentIndex -= (float)tableSize;
return currentSample;
}
private:
//[1]privateなメンバを定義しています。
const juce::AudioSampleBuffer& wavetable;
float currentIndex = 0.0f, tableDelta = 0.0f;
};
[1]で、ウェーブテーブル方式のオシレータに利用するメンバ変数を定義します。AudioSampleBufferクラスは、32bit、floatのマルチチャンネルの音声バッファクラスです。AudioBufferクラスのfloat定義のものと同じになります。また、currentIndexは、現在のテーブルのインデックス値、tableDeltaはテーブルのサンプルの変化分とします。
[2]の「setFrequency」では、以前angleDeltaという変数を求めていたものをtableDeltaを求めるという目的に変更しています。今回は、すでに決まっているオーディオバッファサイズをサンプルレートで割ることで、準備したテーブルに対してどれだけ1サンプルで時間が進むか、という点を計算します。なので、求めるものがラジアンから、テーブルに定義したサンプルのサンプル数に変化しています。
[3]は、親コンポーネントのgetNextAudioBlockから呼び出されるオシレータの音声処理になります。
[3-1]では、ウェーブテーブルとして登録している音声のサンプル数を取得します。
[3-2]では、現在処理のウェーブテーブルの処理インデックス(サンプル番号)をIndex0という変数に取得します。この時、整数に値をキャストして取得します。currentIndexは、前回の処理インデックスの次のインデックスとしてfloatの小数値で保持されていますがここでは、ウェーブテーブルのサンプル数を取得したいので、値を整数にキャストしてIndex0とします。
[3-3]では、現在のインデックス(サンプル番号)の次のインデックスをIndex1という変数に取得します。そして、「frac」という変数として、小数のcurrentIndex変数から、それを整数にキャストしたIndex0を引いています。これで、丸められた小数値を求めることができます。
[3-4]では、ウェーブテーブルを保存しているAudioBufferクラスのポインタを取得して、実際のウェーブテーブルのインデックス(サンプル)の値のIndex0番目とIndex1番目の値を取得します。
[3-5]では、今回のサンプルの値(ゲインの値)を求めています。「value0-value1」は、ウェーブテーブルに保持されているゲインの変化量になりますが、今回は、この間のfracという変化量なので、value0〜value1の変化量のfracの割合だけゲインを変化させることになります。これを、value0に足しこむことで、ウェーブテーブルの荒い保持サンプルの間のゲイン値を補完しています。
[3-6]では、次の処理に向けてcurrentIndexを更新します。deltaTableをたして次のcurrentIndexとしたいのですが、もしウェーブテーブルのサンプル数を上回った場合は、ウェーブテーブルの全体のサンプル値(整数)を引くことで波形の頭に戻って次のサンプル数を指し示します。
WavetableOscillatorクラス、言葉だけではわかりずらいですね。できれば図式化したいです。