チュートリアル: AudioSampleBufferクラスを使ったオーディオのループ再生(上級編)
このチュートリアルでは、スレッドセーフな技術を使用して、AudioSampleBufferオブジェクトに格納された音声を再生およびループする方法を説明します。また、バックグラウンドスレッドで音声データを読み込む技術も紹介します。
レベル: 上級
プラットフォーム: Windows, macOS, Linux
クラス: ReferenceCountedObject, ReferenceCountedArray, Thread, AudioBuffer
はじめに
このチュートリアルは、Tutorial: Looping audio using the AudioSampleBuffer classから続きます。まだ読んでいない場合は、先にそのチュートリアルをお読みください。
このチュートリアルのデモプロジェクトをダウンロードしてください: PIP | ZIP。プロジェクトを解凍し、Projucerで最初のヘッダーファイルを開いてください。
この手順でヘルプが必要な場合は、Tutorial: Projucer Part 1: Getting started with the Projucerを参照してください。
デモプロジェクト
このデモプロジェクトは、Tutorial: Looping audio using the AudioSampleBuffer classのデモプロジェクトと同様の動作を実装しています。ユーザーは音声ファイルを開くことができ、そのファイルはバッファに読み込まれてループ再生されます。このチュートリアルの大きな違いの1つは、ファイルを参照するたびにオーディオシステムをシャットダウンするのではなく、オーディオシステムを実行し続けることです。これは、スレッド間でスレッドセーフな方法で通信するための便利なクラスを使用することで実現されています。
スレッドセーフな技術
Tutorial: Looping audio using the AudioSampleBuffer classで、オーディオスレッドとメッセージスレッドが不完全または破損したデータにアクセスする可能性がある問題をどのように解決したかを思い出してください。ファイルを参照する直前にオーディオシステムをシャットダウンしました。その後、ファイルが選択されると、ファイルを開いてオーディオシステムを再起動しました。これは明らかに実際のアプリケーションでは非実用的で面倒な方法です!
参照カウントオブジェクト
ReferenceCountedObjectクラスは、スレッド間でメッセージやデータを渡すための便利なツールです。ここでは、AudioSampleBufferオブジェクトと再生位置をReferenceCountedObjectクラスに格納します。デバッグを支援し、クラスの動作を説明するために、nameメンバーも含めています(ただし、これはクラスが機能するために厳密には必要ありません):
class ReferenceCountedBuffer : public juce::ReferenceCountedObject
{
public:
typedef juce::ReferenceCountedObjectPtr<ReferenceCountedBuffer> Ptr;
ReferenceCountedBuffer (const juce::String& nameToUse,
int numChannels,
int numSamples)
: name (nameToUse),
buffer (numChannels, numSamples)
{
DBG (juce::String ("Buffer named '") + name + "' constructed. numChannels = " + juce::String (numChannels) + ", numSamples = " + juce::String (numSamples));
}
~ReferenceCountedBuffer()
{
DBG (juce::String ("Buffer named '") + name + "' destroyed");
}
juce::AudioSampleBuffer* getAudioSampleBuffer()
{
return &buffer;
}
int position = 0;
private:
juce::String name;
juce::AudioSampleBuffer buffer;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ReferenceCountedBuffer)
};
クラスの先頭にあるtypedefは、ReferenceCountedObjectサブクラスを実装する上で重要な部分です。ReferenceCountedBufferオブジェクトを生のポインタに格納するのではなく、ReferenceCountedBuffer::Ptr型に格納します。これがオブジェクトの参照カウント(必要に応じてインクリメントおよびデクリメント)とその寿命(参照カウントがゼロに達したときにオブジェクトを削除する)を管理します。ReferenceCountedArrayクラスを使用して、ReferenceCountedBufferオブジェクトの配列を格納することもできます。
MainContentComponentクラスでは、配列と単一のインスタンスの両方を格納します:
juce::SpinLock mutex;
juce::ReferenceCountedArray<ReferenceCountedBuffer> buffers;
ReferenceCountedBuffer::Ptr currentBuffer;
buffersメンバーは、オーディオスレッドがもう必要としないことが確実になるまで、配列内のバッファを保持します。currentBufferメンバーは、現在選択されているバッファを保持します。
バックグラウンドスレッドの実装
MainContentComponentクラスはThreadクラスを継承しています:
class MainContentComponent : public juce::AudioAppComponent,
private juce::Thread
{
public:
これは、バックグラウンドスレッドを実装するために使用されます。オーバーライドしたThread::run()関数は次のとおりです:
void run() override
{
while (!threadShouldExit())
{
checkForBuffersToFree();
wait (500);
}
}
ここでは、解放すべきバッファがあるかどうかをチェックし、その後スレッドは500msまたは起こされるまで(Thread::notify()関数を使用して)待機します。基本的に、これは少なくとも500msごとにチェックが行われることを意味します。checkForBuffersToFree()関数は、buffers配列を検索して、解放できるバッファがあるかどうかを確認します:
void checkForBuffersToFree()
{
for (auto i = buffers.size(); --i >= 0;) // [1]
{
ReferenceCountedBuffer::Ptr buffer (buffers.getUnchecked (i)); // [2]
if (buffer->getReferenceCount() == 2) // [3]
buffers.remove (i);
}
}
- [1]: これらの状況では、配列を逆順に反復処理することを覚えておくと便利です。配列を反復処理しながらアイテムを削除する場合、配列インデックスアクセスの破損を避けやすくなります。
- [2]: これは、指定されたインデックスのバッファのコピーを保持します。
- [3]: この時点で参照カウントが2に等しい場合、オーディオスレッドがバッファを使用していないことがわかり、配列から削除できます。これら2つの参照のうち1つは
buffersにあり、もう1つはローカルのbuffer変数にあります。削除されたバッファは、buffer変数がスコープを外れると(これが残っている最後の参照になるため)自動的に削除されます。
もちろん、アプリケーションの起動時にスレッドを開始する必要があり、これはMainContentComponentコンストラクタで行います:
startThread();
}
ファイルを開く
openButtonClicked()関数は、Tutorial: Looping audio using the AudioSampleBuffer classのopenButtonClicked()関数と似ていますが、いくつかの小さな違いがあります:
void openButtonClicked()
{
chooser = std::make_unique<juce::FileChooser> ("Select a Wave file shorter than 2 seconds to play...",
juce::File {},
"*.wav");
auto chooserFlags = juce::FileBrowserComponent::openMode
| juce::FileBrowserComponent::canSelectFiles;
chooser->launchAsync (chooserFlags, [this] (const juce::FileChooser& fc) {
auto file = fc.getResult();
if (file == juce::File {})
return;
std::unique_ptr<juce::AudioFormatReader> reader (formatManager.createReaderFor (file));
if (reader != nullptr)
{
auto duration = (float) reader->lengthInSamples / reader->sampleRate;
if (duration < 2)
{
ReferenceCountedBuffer::Ptr newBuffer = new ReferenceCountedBuffer (file.getFileName(),
(int) reader->numChannels,
(int) reader->lengthInSamples);
reader->read (newBuffer->getAudioSampleBuffer(), 0, (int) reader->lengthInSamples, 0, true, true);
{
const juce::SpinLock::ScopedLockType lock (mutex);
currentBuffer = newBuffer;
}
buffers.add (newBuffer);
}
else
{
// handle the error that the file is 2 seconds or longer..
}
}
});
}
getNextAudioBlock()関数の実装
オーディオコールバックでは、currentBufferメンバーのコピーを保持する必要があります。これはスレッドセーフな方法で実行できます。この方法では、音声出力中にcurrentBufferメンバーが変更されても問題ありません。完全なgetNextAudioBlock()関数は次のとおりです:
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
ReferenceCountedBuffer::Ptr retainedCurrentBuffer (currentBuffer); // [4]
if (retainedCurrentBuffer == nullptr) // [5]
{
bufferToFill.clearActiveBufferRegion();
return;
}
auto* currentAudioSampleBuffer = retainedCurrentBuffer->getAudioSampleBuffer(); // [6]
auto position = retainedCurrentBuffer->position; // [7]
auto numInputChannels = currentAudioSampleBuffer->getNumChannels();
auto numOutputChannels = bufferToFill.buffer->getNumChannels();
auto outputSamplesRemaining = bufferToFill.numSamples;
auto outputSamplesOffset = 0;
while (outputSamplesRemaining > 0)
{
auto bufferSamplesRemaining = currentAudioSampleBuffer->getNumSamples() - position;
auto samplesThisTime = juce::jmin (outputSamplesRemaining, bufferSamplesRemaining);
for (auto channel = 0; channel < numOutputChannels; ++channel)
{
bufferToFill.buffer->copyFrom (channel,
bufferToFill.startSample + outputSamplesOffset,
*currentAudioSampleBuffer,
channel % numInputChannels,
position,
samplesThisTime);
}
outputSamplesRemaining -= samplesThisTime;
outputSamplesOffset += samplesThisTime;
position += samplesThisTime;
if (position == currentAudioSampleBuffer->getNumSamples())
position = 0;
}
retainedCurrentBuffer->position = position; // [8]
}
重要な変更点は次のとおりです:
- [4]:
currentBufferメンバーのコピーを保持します。この関数のこの時点以降、別のスレッドでcurrentBufferメンバーが変更されても問題ありません。ここではトライロックを使用しているため、別のスレッドが現在変更している場合に、オーディオスレッドがcurrentBufferへのアクセスを待機してブロックされることはありません。 - [5]: コピーを取得したときに
currentBufferメンバーがnullだった場合、無音を出力します。 - [6]:
ReferenceCountedBufferオブジェクト内のAudioSampleBufferオブジェクトにアクセスします。 - [7]: バッファの現在の再生位置を取得します。
- [8]: 現在の再生位置を変更した後、
ReferenceCountedBufferオブジェクトに保存し直します。
このアルゴリズムにより、ReferenceCountedBufferオブジェクトがオーディオスレッド上で削除されないことが保証されます。オーディオスレッド上でメモリを割り当てたり解放したりすることは良い考えではありません。ReferenceCountedBufferオブジェクトは、バックグラウンドスレッド上でのみ削除されます。
バックグラウンドスレッドでの音声読み込み
このアプリケーションは、まだメッセージスレッドで音声データを読み込んでいます。これはメッセージスレッドをブロックし、大きなファイルの読み込みに時間がかかる可能性があるため、理想的ではありません。実際には、バックグラウンドスレッドを使用してこのタスクを実行することもできます。
ファイルパスをバックグラウンドスレッドに渡す
まず、MainContentComponentクラスに次のメンバーを追加します:
juce::CriticalSection pathMutex;
juce::String chosenPath;
次に、openButtonClicked()関数を変更して、ファイルのフルパスをこのメンバーにスワップします。文字列のスワップは厳密にはスレッドセーフではないため、このスレッドが使用している間に他のスレッドがchosenPathを変更しようとしないようにロックを取得する必要があります。
void openButtonClicked()
{
chooser = std::make_unique<juce::FileChooser> ("Select a Wave file shorter than 2 seconds to play...",
juce::File {},
"*.wav");
auto chooserFlags = juce::FileBrowserComponent::openMode
| juce::FileBrowserComponent::canSelectFiles;
chooser->launchAsync (chooserFlags, [this] (const juce::FileChooser& fc) {
auto file = fc.getResult();
if (file == juce::File {})
return;
auto path = file.getFullPathName();
{
const juce::ScopedLock lock (pathMutex);
chosenPath.swapWith (path);
}
notify();
});
}
ここでは、バックグラウンドスレッドで関数を呼び出してファイルを開く予定があるため、バックグラウンドスレッドを起こします。
バックグラウンドスレッドからパスにアクセスする
run()関数は次のように更新する必要があります:
void run() override
{
while (!threadShouldExit())
{
checkForPathToOpen();
checkForBuffersToFree();
wait (500);
}
}
checkForPathToOpen()関数は、chosenPathメンバーをローカル変数にスワップしてチェックします。繰り返しになりますが、スワップはスレッドセーフではないため、chosenPathにアクセスする前にロックを取得する必要があります。
void checkForPathToOpen()
{
juce::String pathToOpen;
{
const juce::ScopedLock lock (pathMutex);
pathToOpen.swapWith (chosenPath);
}
if (pathToOpen.isNotEmpty())
{
juce::File file (pathToOpen);
std::unique_ptr<juce::AudioFormatReader> reader (formatManager.createReaderFor (file));
if (reader.get() != nullptr)
{
auto duration = (float) reader->lengthInSamples / reader->sampleRate;
if (duration < 2)
{
ReferenceCountedBuffer::Ptr newBuffer = new ReferenceCountedBuffer (file.getFileName(),
(int) reader->numChannels,
(int) reader->lengthInSamples);
reader->read (newBuffer->getAudioSampleBuffer(), 0, (int) reader->lengthInSamples, 0, true, true);
{
const juce::SpinLock::ScopedLockType lock (mutex);
currentBuffer = newBuffer;
}
buffers.add (newBuffer);
}
else
{
// handle the error that the file is 2 seconds or longer..
}
}
}
}
pathToOpen変数が空の文字列の場合、開くべき新しいファイルがないことがわかります。この関数の残りのコードは見慣れたものでしょう。
アプリケーションを再度実行すると、正しく機能するはずです。
このセクションの最終コードは、デモプロジェクトのLoopingAudioSampleBufferAdvancedTutorial_02.hファイルにあります。
まとめ
このチュートリアルでは、特にオーディオアプリケーションにおいて、スレッド間でデータを渡すための便利な技術をいくつか紹介しました。このチュートリアルを読んだ後、次のことができるようになります:
- ReferenceCountedObjectクラスのサブクラスを実装する。
- マルチスレッドアプリケーションでReferenceCountedObjectの寿命を管理する。
- 不要になったオブジェクトの削除やファイル読み込み操作などのタスクを実行するバックグラウンドスレッドを実装する。