まずは動きましたので、次にプログラムの中身を見ていきたいと思います。
今回のは結構頭つかったよね~
前回動作を確認したファイルを読み込んでひたすらループを行うアプリケーションのプログラムを見ていきたいと思います。
公式のチュートリアルページはこちらです。
こんな人の役に立つかも
・JUCEフレームワークに入門したい人
・JUCEで音声プログラミングを学びたい人
・JUCEのAudioチュートリアルを進めている人
ファイルの読み込み部に関して
今回作成したオーディオループ再生のアプリには、読み込むことができる音声ファイルの長さを2秒より短いという制限を設けています。
制限の理由としては、
①音声ファイルが長い場合、物理メモリを占有してしまうため
②ファイルのロードに時間がかからないため
という点で、チュートリアルでは2秒より短いという制限を設けているようです。
②の理由については、FileChooser::browserForFileToOpen関数が音声ファイルを返して音声ファイルをロードするという順次のプログラムで、チュートリアルとしてシンプルな処理になるようにしているだけで、実用的ではないようです。そのため、音声が読み込まれるまでGUI描画などが停止します。(今回の場合は、2秒という短い音声の制限があるためあまり気にならないかもしれません。)
次のチュートリアルで、この点をバックグラウンドスレッドで行うように改善するようです。
プログラムの詳細
ファイルの読み込みの部分は、「openButtonClicked」関数に実装されています。
void openButtonClicked()
{
shutdownAudio();//[1]
juce::FileChooser chooser ("Select a Wave file shorter than 2 seconds to play...",
{},
"*.wav");
if (chooser.browseForFileToOpen())
{
auto file = chooser.getResult();
//[2]AudioFormatReaderクラスのポインタのreaderを作成します。
std::unique_ptr<juce::AudioFormatReader> reader (formatManager.createReaderFor (file));
if (reader.get() != nullptr)
{
//[3]音声ファイルの長さを取得するための計算を行います。
auto duration = (float) reader->lengthInSamples / reader->sampleRate;
if (duration < 2)
{
//[4]音声バッファのチャンネル数とサイズを設定します。
fileBuffer.setSize ((int) reader->numChannels, (int) reader->lengthInSamples);
//[5]fileBufferに音声を読み込みます。
reader->read (&fileBuffer,
0,//[5.1]AudioBufferSampleの開始場所
(int) reader->lengthInSamples,//[5.2]読み取りサンプル数
0,//[5.3]AudioFormatReaderの開始サンプル数
true,//[5.4]ステレオの場合、Lチャンネルを読み込むかどうかのフラグ
true);//[5.5]ステレオの場合、Rチャンネルを読み込むかどうかのフラグ
position = 0;// [6]
setAudioChannels (0, (int) reader->numChannels);// [7]
}
else
{
//選択された音声が2秒以上の場合の処理です。
}
}
}
}
[1]では、一度オーディオスレッドを停止するようです。shutDownAudio関数を呼び出すことで、getNextAudioBlockの処理が停止されました。
[2]では、AudioFormatReaderクラスを利用して、音声ファイルが選択されているかを確認します。この次の条件でreaderが選択されているかの条件で利用します。ここまでの処理は、以前AudioPlayerアプリで作成したものと同様の処理となっています。
[3]では、ロードする音声ファイルの長さをサンプルレートとサンプル数から計算します。サンプルレートは「reader->sampleRate」で取得できるので、音声ファイル全体のサンプル数を1秒当たりのサンプル数(サンプルレート)で割ると秒数が計算できます。次の行の条件で、2秒より短いかどうかの判定で利用します。
[4]では、AudioSampleBufferクラスの変数、「fileBuffer」に、「setSize」関数を使い、チャンネル数とバッファのサイズを設定しています。オーディオストリームのチャンネル数をreaderの「numChannels」から、音声ファイルのサンプル数をreaderの「lengthInSamples」から取得します。
[5]で、音声ファイルをreaderを利用してfileBufferに読み込みます。[5.1]~[5.5]のパラメータを設定します。
[6]のposition変数は、バッファの再生位置をアプリが制御する変数なので、0に初期化しておきます。
[7]では、setAudioChannelで再度オーディオスレッドを開始しています。音声ファイルの出力チャンネル数となる設定にしています。
音声処理プログラム
次に、音声のループ再生処理を行うgetNextAudioBlock関数です。
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill)
{
auto numInputChannels = fileBuffer.getNumChannels();
auto numOutputChannels = bufferToFill.buffer->getNumChannels();
//[8]出力バッファのサンプル数です。
auto outputSamplesRemaining = bufferToFill.numSamples;
//[9]音声の書き込みスタート位置です。
auto outputSamplesOffset = bufferToFill.startSample;
//[10]出力バッファのサンプルでループさせます。
while (outputSamplesRemaining > 0)
{
//再生させたい音声のサンプル数です。
auto bufferSamplesRemaining = fileBuffer.getNumSamples() - position;
//再生させたい音声のサンプルと出力バッファのサイズの小さいほうの数値を取得します。
auto samplesThisTime = juce::jmin (outputSamplesRemaining, bufferSamplesRemaining);
//チャンネル数分ループします。
for (auto channel = 0; channel < numOutputChannels; ++channel)
{
//[11]bufferToFillにfileBufferから音声をコピーしてきます。
bufferToFill.buffer->copyFrom (channel,
outputSamplesOffset,
fileBuffer,
channel % numInputChannels,
position,
samplesThisTime);
}
//出力バッファの残りを減じます。
outputSamplesRemaining -= samplesThisTime;
outputSamplesOffset += samplesThisTime;
//音声の書き込み開始位置も進めます。
position += samplesThisTime;//音声バッファの位置を進めます。
//position変数が読み込み音声サンプル数となったら最初に戻します。これでループになります。
if (position == fileBuffer.getNumSamples())
position = 0;
}
}
[8]のoutputSampleRemainingは、getNextAudioBlock関数が出力に必要なサンプル数になります。bufferToFillの「numSamples」の値を入れます。次に出力しなければならないサンプル数となるので、このサンプル数分の音声データを関数内で作成する必要があります。[9]は、bufferToFillのサンプルのスタート位置になります。
[10]では、outputSamplesRemainingの変数が0をこえるまでループを行います。ちなみに、whileループですが、音声ファイルがまだ続く場合は基本的にoutputSamplesRemainingは0となるので、基本的に音声ファイルの最後だけがループとなります。
このループ内では、[11]のbufferToFill(出力バッファ)へfileBuffer(読み込み音声)の書き込みが行われます。その前後で、出力バッファへ音声を書き込みを制御するプログラムになっています。[11]のAudioBufferクラスのcopyFrom関数に指定するパラメータを中心に前後のパラメータを制御している、と考えると少しだけすっきりしました。
図でイメージしてみました
図のように、getNextAudioBlock関数が呼ばれるたびに音声の読み込む位置がずれていきます。最後の一回は、bufferToFillのサンプルすうより音声ファイルのサンプルが少なくなるので、(一致するばあいもありますが^^;)whileループでもう一度バッファを埋めます。
最後の出力バッファより取得できる音声サンプルが少なくなると、「outputSamplesRemaining 」は0にならず、もう一度再生サンプルを埋めるためにループします。この時、position変数はfileBufferのサンプル数と同じ数値となるので、positionが0に戻り、whileループでもう一度バッファをコピーする際には、音声の最初のサンプルから再度読み込まれることになります。
ちょっとややこしいですが、よく考えられてますね~
マルチスレッドの問題点
今回のプログラムでは、いくつかの問題点があるようです。openButtonClicked関数のなかでshutdownAudioが呼び出されています。一方で、getNextAudioBlockは別のスレッドで動作しているので、音声出力に応答するためひたすら動作しています。それぞれのスレッドが特に連携なく音声の制御を行っているので、いろいろなタイミングで不具合が起きる可能性をはらんでいます。
次のチュートリアルでは、このようなオーディオスレッドでの致命的なエラーの回避を検討するようですね。