音声処理を理解するのが結構大変でした。
スレッドも使い始めて、難易度が上がったね~
JUCEプログラミング、Looping audio using the AudioSampleBuffer class Advanceのチュートリアルの続きをやっていきます。前回までで、ヘッダーに「ReferenceCountedBuffer」クラスというものを定義しましたので、今回はcppのプログラミングに入っていきます。
こんな人の役に立つかも
・JUCEフレームワークに入門したい人
・JUCEで音声プログラミングを学びたい人
・JUCEのAudioチュートリアルを進めている人
MainComponent.cpp
コンストラクタ
MainComponent::MainComponent()
: Thread("Background Thread")//[1]スレッドを初期化します。
{
addAndMakeVisible(openButton);
openButton.setButtonText("Open...");
openButton.onClick = [this] { openButtonClicked(); };
addAndMakeVisible(clearButton);
clearButton.setButtonText("Clear");
clearButton.onClick = [this] { clearButtonClicked(); };
setSize(300, 200);
formatManager.registerBasicFormats();
setAudioChannels(0, 2);
startThread();//[2]スレッドをスタートさせます。
}
[1]では、スレッドをメンバイニシャライザで初期化しています。そして、[2]でスレッドをスタートさせます。
デストラクタ
デストラクタでは、スレッドを止める処理を入れておきます。stopThread関数を入れておきます。引数の4000は、スレッドを強制終了するまでの時間(ミリ秒)を入れます。
MainComponent::~MainComponent()
{
stopThread(4000);
shutdownAudio();
}
getNextAudioBlock関数
void MainComponent::getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill)
{
//[1]定義したPtrで「retainedCurrentBuffer」を作成します。
ReferenceCountedBuffer::Ptr retainedCurrentBuffer(currentBuffer);
if (retainedCurrentBuffer == nullptr)//[2]
{
bufferToFill.clearActiveBufferRegion();
return;
}
auto* currentAudioSampleBuffer = retainedCurrentBuffer->getAudioSampleBuffer(); //[3]
auto position = retainedCurrentBuffer->position;//[4]retainedCurrentBufferに変更です
auto numInputChannels = currentAudioSampleBuffer->getNumChannels();
auto numOutputChannels = bufferToFill.buffer->getNumChannels();
auto outputSamplesRemaining = bufferToFill.numSamples;
auto outputSamplesOffset = 0;
while (outputSamplesRemaining > 0)
{
auto bufferSamplesRemaining = currentAudioSampleBuffer->getNumSamples() - position;
//[5]
auto samplesThisTime = juce::jmin(outputSamplesRemaining, bufferSamplesRemaining);
for (auto channel = 0; channel < numOutputChannels; ++channel)
{
bufferToFill.buffer->copyFrom(channel,
bufferToFill.startSample + outputSamplesOffset,
*currentAudioSampleBuffer,
//[6]
channel % numInputChannels,
position,
samplesThisTime);
}
outputSamplesRemaining -= samplesThisTime;
outputSamplesOffset += samplesThisTime;
position += samplesThisTime;
if (position == currentAudioSampleBuffer->getNumSamples())
//[7]
position = 0;
}
retainedCurrentBuffer->position = position;
}
以前のチュートリアルで音声ファイルを格納していた「fileBuffer」という変数を「currentAudioSampleBuffer」という変数に変更しています。
[1]では、currentBuffer(openButtonClick関数で設定)変数からヘッダーの入れ子クラス「referenceCountedBuffer」に定義した「ReferenceCountedBuffer::Ptr」の型で「retainedCurrentBuffer」というポインタを作成します。[2]で、このポインタがnullptrでないことを確認します。
[3]では、retainedCurrentBufferの「getAudioSampleBuffer」(ヘッダーで定義しました)関数を使い、「currentAudioSampleBuffer」として、AudioBufferクラスのポインタを取得します。[4]は、音声の再生位置であるpositionをretainedCurrentBufferクラスから取得します。
[5]~[7]では、準備したcurrentAudioSampleBufferへと以前fileBufferであったものを変更しています。
少しだけ見えた今回の目的
今までの問題としては、openButtonClikedがfileBufferを用意して、それをオーディオスレッドがそのまま使っていたことです。
メッセージスレッドは、openButtonClikedのようにボタンが押されたタイミング(ユーザーのタイミング)で発生してfileBufferを作成します。このタイミングはいつ来るかはわかりません。
一方で、getNextAudioBlock関数は、音声出力デバイスからの要求で処理が動作します。この2つのスレッドは、非同期、といっていいのか(正しい言い回しではないと思いますので、イメージで^^;)そのため、fileBufferの状態に関係なくgetNextAudioBlock関数は単純に連動していないfileBufferで作られた音声データを見に行くのみです。
openButtonCliked関数でshutDownAudioが呼び出されているとは言え、この改善策があるということは、おそらく、停止するトリガーとなるだけで、実行中のgetNextAudioBlockは処理は最後まで続いたりしていそうです。この辺りは深く調べきれていないので、イメージなのですが・・・^^;
そんなこんなで、上の図の赤い矢印のように、getNextAudioBlockがfileBufferを見に行くと、違うものが入っている可能性があります。
今回のチュートリアルの改善点は、バックグラウンドスレッドを一定間隔で動かして、参照される予定がなくなったバッファを削除するような形になっています。
また、メッセージスレッド(openButtonClick関数)のcurrentBufferがそのままオーディオスレッド(getNextAudioBlock関数)使われることはなく、オーディオスレッドの最初で、retainedCurrentBufferという形でcurrentBufferを設定することで、バックグラウンドスレッドの音声の配列に登録して、その後はこのreferenceCountedBufferを参照することで、オーディオスレッドで利用する音声へのポインタがオーディオスレッド実行中に変更されることがないようになっているようです。
ちょっと複雑ですが、何となく意味が理解できてきました。