ウェーブテーブルってよく聞くけど、実際どんなことをしているのかはよくわかっていませんでした。
シンセの種類としてよく聞くよね~
JUCEチュートリアルの、Synthの項目、「Wavetable synthesis」を進めていきます。
今回は、「Sine Wave Oscillator」の項目で、ウェーブテーブルシンセ方式を利用しない基本的なサイン波のオシレータを作成していきます。
チュートリアル概要
サンプルとしてダウンロードできるプログラムは「01」~「04」があります。
①WavetableSynthTutorial_01.h
サイン波の発音処理をクラス化して、「SineOscillator」クラスというものを作成します。
チュートリアルの「Sine Wave Oscillator」の項目に該当します。
②WavetableSynthTutorial_02.h
ウェーブテーブルの概念でサイン波オシレータを実装します。
チュートリアルの「Wavetable Oscillator」の項目に該当します。
③WavetableSynthTutorial_03.h
WavetableSynthTutorial_02.hを改良するようです。
チュートリアルの「Wrapping the Wavetable」の項目に該当します。
④WavetableSynthTutorial_04.h
サイン波に倍音を付加するような内容です。
チュートリアルの「Selecting the Hamonics」の項目に該当します。
最初のデモアプリ
最初のアプリは、サンプルとしてダウンロードできる「WavetableSynthTutorial_01.h」に基づいています。まずは、このアプリを実装していきたいと思います。
200個のサイン波をランダムなピッチで発音させ、CPU負荷をGUIに表示するようなアプリを作成していきます。
それでは、実装をしていきたいと思います。
MainComponent.h
SineOscillatorクラス
ヘッダファイルに、次のようなSineOscillatorクラスを作成します。
サイン波をプログラムで生成するときには、サイン波の位相(ラジアン)をsin関数に与えることでゲインが得られます。このあたりのサイン波の生成の仕組みについては、以前の記事で詳細に勉強しましたので、そちらもご参照ください。
class SineOscillator
{
public:
SineOscillator() {}
//[2]サイン波1サンプル分の増加ラジアン値を計算します。
void setFrequency(float frequency, float sampleRate)
{
auto cyclesPerSample = frequency / sampleRate;
angleDelta = cyclesPerSample * juce::MathConstants<float>::twoPi;
}
//[3]サンプル毎にサイン波の位相(currentAngle)を進める処理です。
forcedinline void updateAngle() noexcept
{
currentAngle += angleDelta;
if (currentAngle >= juce::MathConstants<float>::twoPi)
currentAngle -= juce::MathConstants<float>::twoPi;
}
//[4]サンプル値を計算する関数です。
forcedinline float getNextSample() noexcept
{
auto currentSample = std::sin(currentAngle);
updateAngle();
return currentSample;
}
private:
//[1]サイン波のサンプルを求めるために必要なデータです。
float currentAngle = 0.0f, angleDelta = 0.0f;
};
[1]では、サイン波を計算するために必要な位相に関する変数をprivateなメンバとして準備しています。
[2]のsetFrequency関数は、サイン波の周波数とシステムのサンプルレートから、1サンプル当たりのラジアン値を求める準備関数です。prepareToPlayで呼び出します。angleDeltaをここで計算して、1サンプル当たりの増加ラジアン値を求めます。
[3]では、currentAngle(求めるsinのラジアン値)が2πより大きい場合は、2πを引くことで、currentAngle値が増加し続けるのを防ぎます。
[4]のgetNextSample関数で、サンプル値(そのサンプルのゲイン値)を求めて返します。
updateAngleとgetNextSample関数にはforcedinlineというものが付いています。これは、JUCEフレームワークのもので、プラットフォームに合わせたインライン関数としてくれるようなものです。
MainComponentクラス
次に、MainComponentクラスにタイマーを追加します。
タイマーは、[1]のように継承して、[2]のようにコールバック関数を準備しておきます。今回、タイマーはCPU利用率の表示のために使います。
class MainComponent : public juce::AudioAppComponent,
public juce::Timer//[1]タイマーを追加します。
{
public:
//==============================================================================
MainComponent();
~MainComponent() override;
void timerCallback() override;//[2]タイマーコールバックを追加します。
//...略...
private:
//[3]GUIで利用するラベルです。
juce::Label cpuUsageLabel;
juce::Label cpuUsageText;
//[4]音量のための変数と、SineOscillatorを複数作成して管理する配列を準備します。
float level = 0.0f;
juce::OwnedArray<SineOscillator> oscillators;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};
[3]と[4]では、GUIのラベルと、音量や、オシレータクラスを格納するための配列オブジェクト(OwnedArrayクラスで定義)を定義しています。OenwdArrayクラスは、<>で指定したオブジェクトを配列のように利用できるようにするものです。
MainConponent.cpp
次に、cppファイルの実装です。
コンストラクタ
まずは、コンストラクタでGUIのラベルをセットします。
MainComponent::MainComponent()
{
//[1]GUIのCPU表示ラベルをセットします。
cpuUsageLabel.setText("CPU Usage", juce::dontSendNotification);
cpuUsageText.setJustificationType(juce::Justification::right);
addAndMakeVisible(cpuUsageLabel);
addAndMakeVisible(cpuUsageText);
//[2]システム関連の初期化を行います。
setSize(400, 200);
setAudioChannels(0, 2);
startTimer(50);
}
[1]では、今回、CPUの使用%を表示するためのラベルと、「CPU Usage」という文字列を表示するためのラベルを初期化、可視化します。
[2]では、ウィンドウサイズを400×200として、アプリの音声入力を0、出力をステレオの2チャンネルとしています。最後に、タイマーコールバック関数を50msec毎に動作するようにしてタイマーをスタートさせます。
タイマーコールバック関数
タイマーのコールバック関数を次のように追加します。cppファイルの一番下に追加しました。(どこでも良いです。)
void MainComponent::timerCallback()
{
auto cpu = deviceManager.getCpuUsage() * 100;
cpuUsageText.setText(juce::String(cpu, 6) + " %", juce::dontSendNotification);
}
deviceManagerは、AudioDeviceManagerクラスのオブジェクトで、「getCpuUsage」関数でCPU使用率を取得することができます。最後に100をかけて%の単位に直します。そして、ラベルにフォーマットを成型して出力するようにしています。この処理は、50msec毎に実行されます。
PrepareToPlay関数
音声処理開始前に一回実行されるprepareToPlay関数です。[1]のように、ここで、発音のオシレータを200個準備します。
void MainComponent::prepareToPlay (int samplesPerBlockExpected, double sampleRate)
{
//[1]生成するSineOscillatorの数です。
auto numberOfOscillators = 200;
//[2]オシレータの初期化を先の数分繰り返します。
for (auto i = 0; i < numberOfOscillators; ++i)
{
auto* oscillator = new SineOscillator();
//[2-1]MIDIのノートナンバーをランダムに取得します。
auto midiNote = juce::Random::getSystemRandom().nextDouble() * 36.0 + 48.0;
//[2-2]MIDIノートナンバーを周波数に変換します。
auto frequency = 440.0 * pow(2.0, (midiNote - 69.0) / 12.0);
//[2-3]オシレータに周波数を設定して配列に追加します。
oscillator->setFrequency((float)frequency, (float)sampleRate);
oscillators.add(oscillator);
}
//[3]レベルを調整します。
level = 0.25f / (float)numberOfOscillators;
}
[2]では、オシレータを設定しています。[2-1]は、「nextRandom」関数は0~1.0の小数をランダムに取得できる関数です。midiNoteはdouble型の小数として、48.0~84.0までの数値をとることになります。[2-2]では、先ほど生成したMIDIノートナンバーを周波数に変換します。これは、数式を利用して実現しています。69が440Hzなので、取得したMIDIノートナンバーから引くと、A440からどれくらい半音で離れているかの数値になります。この半音を12で割った値を2のべき乗して440Hzに掛け合わせることで、ノートナンバーを周波数に変換できるようです。
解説は、こちらのサイトが参考になります。
[2-3]では、setOscillator関数に周波数とサンプルレートを渡しています。これで、オシレータはラジアンが計算できるようになります。最後に、配列にオシレータを追加します。
[3]の「level」変数は、一つのオシレータに設定する音量を表します。全体として0.25fというゲインとしたいので、それをオシレータすうで割ったゲインがオシレータ一つ当たりのゲインとしています。
getNextAudioBlock関数
今回のgetNextAudioBlockは、チャンネル毎にループさせるのではなく、2チャンネルと決めているので、チャンネル数のループ処理をしていません。その代わりにLeftとRightの処理をそのまま順番に行っています。
void MainComponent::getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill)
{
auto* leftBuffer = bufferToFill.buffer->getWritePointer(0, bufferToFill.startSample);
auto* rightBuffer = bufferToFill.buffer->getWritePointer(1, bufferToFill.startSample);
bufferToFill.clearActiveBufferRegion();
//[1]オシレータ配列の要素数分繰り返します。
for (auto oscillatorIndex = 0; oscillatorIndex < oscillators.size(); ++oscillatorIndex)
{
//[2]オシレータのオブジェクトを取得します。
auto* oscillator = oscillators.getUnchecked(oscillatorIndex);
for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
{
//[3]サンプルのゲインを計算します。
auto levelSample = oscillator->getNextSample() * level;
//[4]ゲイン値をそれぞれのチャンネルに足しこみます。
leftBuffer[sample] += levelSample;
rightBuffer[sample] += levelSample;
}
}
}
[1]では、OwnedArrayクラスの配列「oscillators」に登録されているオシレータオブジェクトの数だけ繰り返しを行うようなループ処理となっています。
[2]で、「oscillators」配列から「getUnchecked」関数をつかってoscillatorIndexの番号のSineOscillatorクラスのオブジェクトを取得しています。
[3]で、SineOscillatorクラスに定義したgetNextSample関数で、サンプルのゲイン値を計算します。計算したゲイン値はlevel変数でスケーリングして音量を調整します。
[4]に、左右のチャンネルの出力音声バッファにサンプルの計算値を足しこむことでゲイン値とします。今回は200のオシレータがあるので、200回それぞれのサイン波のサンプル値が足しこまれたゲインがその音声サンプルの値となります。
resized関数
最後に、GUIの設定を行います。resized関数を次のように書き換えます。CPUの利用率のラベルを配置しています。
void MainComponent::resized()
{
cpuUsageLabel.setBounds(10, 10, getWidth() - 20, 20);
cpuUsageText.setBounds(10, 10, getWidth() - 20, 20);
}
まずは、以前やったサイン波の生成をシンプルに実装しました。また、たくさんのサイン波オシレータがあるとき、最後にサンプル値を足しこむとゲインとなります。