基本的なことほど結構難しいんですよね・・・
音声処理の計算については、意外とイメージしづらかったね
JUCEプログラミング、synthの項目の「Build a sine wave synthesiser」チュートリアルを進めていきます。前回に続いて、サイン波のシンプルなシンセサイザーアプリの実装内容を見ていきたいと思います。
公式のチュートリアルページはこちらになります。
こんな人の役に立つかも
・JUCEプログラミングを勉強している人
・JUCEのBuild a sine wave synthesiserチュートリアルをやっている人
・「SineSynthTutorial_01.h」の実装をしたい人
MainComponent.cppの実装
updateAngleDelta関数
まずは、updateAngleDelta関数を実装します。cppの一番下に追加しました。
//次の関数を追加しました。
void MainComponent::updateAngleDelta()
{
//以下の計算で、1サンプルに対するラジアンを求めます。
auto cyclesPerSample = frequencySlider.getValue() / currentSampleRate;
angleDelta = cyclesPerSample * 2.0 * juce::MathConstants<double>::pi;
}
まずは、ここでやっている計算式が直感的ではなかったので、少しひも解いてみたいと思います。
サイン波の時間的変化は、ラジアンという単位で表現します。2π(360°)で1周期となります。そして、サンプルレートで、1秒間に何サンプルかを定義しています。
まず基本的事項の整理です。
updateAngleDeltaで求めたい変数「angleDelta」は、1サンプルのラジアンの増加量となります。(↑の図でいうオレンジの1目盛の間隔)
それなので、サンプルレートと出力するサイン波の周期(周波数)が計算に必要な変数となります。
具体的に、5Hzのサイン波と、44100のサンプルレートで考えてみます。
上の図のように5Hzのサイン波ということは、1秒間に5周期のサイン波ということで、サンプル数も1秒間に44100サンプルありますので、1サンプルあたり何ラジアン進めればサイン波が5Hzとなるかを考えます。
上の図の①と②のように計算をしていき最後に簡単にした計算式が「angleDelta = Hz/sample * 2π」となりました。前半の部分が「Hz/sample」ということで、プログラムの[1]に該当し、その値と「2π」を掛け合わせる部分が[2]に該当します。
ちなみに、πの値は「juce::MathConstants<double>::pi」を利用できます。
getNextAudioBlock関数
getNextAudioBlock関数では、sin関数で音声バッファに音声データを作成します。
void MainComponent::getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill)
{
//[1]チャンネル毎に音声バッファを取得します。
auto level = 0.125f;
auto* leftBuffer = bufferToFill.buffer->getWritePointer(0, bufferToFill.startSample);
auto* rightBuffer = bufferToFill.buffer->getWritePointer(1, bufferToFill.startSample);
//[2]音声バッファへの音声処理ループです。
for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
{
//[2-1]サイン関数で現サンプルのゲイン値を取得します。
auto currentSample = (float)std::sin(currentAngle);
//[2-2]1サンプル分のラジアン値を増加させます。
currentAngle += angleDelta;
//[2-3]音声バッファに出力するデータを書き込みます。
leftBuffer[sample] = currentSample * level;
rightBuffer[sample] = currentSample * level;
}
}
[1]では、0.125という値で音量が大きくなりすぎないようにするスケーリングの定数を「level」という変数に格納しておきます。また、左チャンネルと右チャンネル(チャンネル1、2)のバッファのポインタを取得しておきます。
[2]では、いつもの音声バッファへの音声処理ループです。次の出力に必要なサンプル数分ループを行なっています。
[2-1]では、currentAngleのラジアン値をsin関数に与えることで、そのサンプルのゲインを計算しています。最初は初期値でcurrentAngleは0なので、sinも0です。[2-2]で、angleDelta分ラジアン値が増加しますので、2回目のループではsinへ少し進めたラジアンが入力されます。
ということは、高い周波数ほど1周期を表現するサンプル数が少ないということになるんですね。
currentAngleは増加し続けるので、2πになったら0に戻す、という処理が必要なのかな、と思っていましたが、チュートリアルでは特にそれをしなくても動作するのでそのままにしてあるようです。実際には、かなり長い間発音したらどこかでオーバーフローして0に戻るのだと思います。その時にノイズになるとかそういった問題は起きると思います。
意外と、言葉で表現しにくい部分です。
angleDeltaを更新するタイミング
updateAngleDelta関数で利用している変数「currentSampleRate」とスライダー値(周波数)の変更が起きた場合はupdateAngleDelta関数を再度実行してangleDelta変数を更新する必要があります。
コンストクタ
コンストラクタを以下のように書き換えます。また、コンストラクタでは、angleDelta変数を更新する重要なポイントになります。
MainComponent::MainComponent()
{
//[1]スライダーオブジェクトの可視化、初期化です。
addAndMakeVisible(frequencySlider);
frequencySlider.setRange(50.0, 5000.0);
frequencySlider.setSkewFactorFromMidPoint(500.0);
//[2]スライダーにハンドラを定義しておきます。
frequencySlider.onValueChange = [this]
{
if (currentSampleRate > 0.0)
updateAngleDelta();
};
setSize(600, 100);
setAudioChannels(0, 2);
}
[1]では、スライダーオブジェクト「frequencySlider」の可視化、周波数の上下限の設定(frequencySliderは周波数を単位として数値を与えることにしています。)しています。
「setSkewFactorFromMidPoint」関数は、スライダーの数値変化に影響しています。500という数値をスライダーの中心にして、スライダーの下半分と上半分で値の増加量を変更しています。スライダーの上半分(右半分)は500~5000を表現するので、分解能は落ちますが、周波数を表現するには、この方が音楽的に利用しやすいです。
[2]で、スライダーの値が変更されたときに実行されるハンドラを定義します。単純に、サンプルレートが0より大きいときというエラー処理の中に、updateAngleDelata関数を呼び出しているだけです。
prepareToPlay関数
ここでも重要な点としては、サンプルレートが変更されたタイミングで、angleDeltaを更新して正しい1サンプルのラジアン値を再計算することです。
サンプルレートをcurrentSampleRate変数に上書きして、updateAngleDelta関数を再度呼び出すことで、angleDelta変数を更新します。
void MainComponent::prepareToPlay (int samplesPerBlockExpected, double sampleRate)
{
//サンプルレート更新で、angleDelta変数を更新しておきます。
currentSampleRate = sampleRate;
updateAngleDelta();
}
resized関数
resized関数は、スライダーをGUIに配置するのみです。
void MainComponent::resized()
{
//以下のスライダーを配置しています。
frequencySlider.setBounds(10, 10, getWidth() - 20, 20);
}
GitHubのリポジトリ
以下のGitHubリポジトリに、作成したSineSynth01のプログラムを配置しましたので、もしよろしければご利用ください。