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.バッファにロードされたオーディオファイルを開き、ループで再生できます。このチュ ートリアルで大きく異なる点は、ファイルを参照するたびにシャットダウンするのではなく、オーディオシステムを実行し続けることです。これは、スレッドセーフな方法でスレッド間の通信を行うためのいくつかの便利なクラスを使用することで実現されています。
スレッドセーフ・テクニック
を思い出してほしい。Tutorial: Looping audio using the AudioSampleBuffer classオーディオスレッドとメッセージスレッドが不完全または破損したデータにアクセスする可能性があるという潜在的な問題をどのように解決したか。ファイルをブラウズする直前に、オーディオシステムをシャットダウンした。そして、ファイルを選択したら、ファイルを開き、オーディオシステムを再起動した。これは、実際のアプリケーションでは明らかに非現実的で面倒な方法である!
参照カウントされるオブジェクト
についてReferenceCountedObjectク ラスは、スレッド間でメッセージやデータを受け渡すのに便利なツールだ。ここではAudioSampleBufferオブジェクトの再生位置とReferenceCountedObjectクラスを作成しました。デバッグの助けとなるように、また、このクラスがどのように動作するかを説明するのに役立つように、次のようなものもあります。name
メンバー(ただし、これはクラスが機能するために厳密には必要ではない):
class ReferenceCountedBuffer : public juce::ReferenceCountedObject
{
public:
typedef juce::ReferenceCountedObjectPtr 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
型がある。これは、オブジェクトの参照カウント(必要に応じてインクリメントとデクリメントを行う)とライフタイム(参照カウントがゼロになったときにオブジェクトを削除する)を管理するものである。の配列を格納することもできる。ReferenceCountedBuffer
オブジェクトを使用する。ReferenceCountedArrayクラスである。
我々のMainContentComponent
クラスでは、配列と単一のインスタンスの両方を格納します:
juce::SpinLock mutex;
juce::ReferenceCountedArray 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);
}
}
ここでは、解放すべきバッファがあるかどうかをチェックし、スレッドは500ミリ秒待機するか、ウェイクアップされるのを待つ。Thread::notify()関数)。基本的に、これは少なくとも500ミリ秒ごとにチェックが行われることを意味する。つまり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
もう一方はローカルbuffer
変数として削除されます。削除されたバッファはbuffer
変数がスコープ外になる(これが最後に残った参照となるため)。
もちろん、アプリケーションの起動と同時にスレッドを開始する必要がある。MainContentComponent
ビルダー
startThread();
}
ファイルを開く
私たちのopenButtonClicked()
関数はopenButtonClicked()
関数Tutorial: Looping audio using the AudioSampleBuffer class若干の違いはあるが:
void openButtonClicked()
{
chooser = std::make_unique ("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 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..
}
}
});
}
以下はその違いである:
- の新しいインスタンスを割り当てる。
ReferenceCountedBuffer
クラスである。 - オーディオデータをAudioSampleBufferオブジェクトを含む。
- 現在のバッファにする。
- これをバッファの配列に追加する。
現在のバッファをクリアするには、その値をnullptr
:
void clearButtonClicked()
{
const juce::SpinLock::ScopedLockType lock (mutex);
currentBuffer = nullptr;
}
バッファーの再生
私たちのgetNextAudioBlock()
関数はgetNextAudioBlock()
関数Tutorial: Looping audio using the AudioSampleBuffer classただし、現在のReferenceCountedBuffer
オブジェクトとAudioSampleBufferオブジェクトを含む。
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
auto retainedCurrentBuffer = [&]() -> ReferenceCountedBuffer::Ptr // [4]
{
const juce::SpinLock::ScopedTryLockType lock (mutex);
if (lock.isLocked())
return currentBuffer;
return nullptr;
}();
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
メンバーが別のスレッドで変更された場合、ローカル・コピーを取っているので、このようなことは起こりません。へのアクセス待ちでオーディオ・スレッドが立ち往生しないように、ここではtryロックを使用していることに注意してください。currentBuffer
他のスレッドが現在修正中である場合。 - [5]の場合は無音を出力する。
currentBuffer
コピーを取ったとき、メンバーはNULLだった。 - [6]にアクセスする。AudioSampleBufferオブジェクトを
ReferenceCountedBuffer
オブジェクトがある。 - [7]バッファの現在の再生位置を得る。
- [8]現在の再生位置を変更した後、その位置を
ReferenceCountedBuffer
オブジェクトがある。
このアルゴリズムはReferenceCountedBuffer
オブジェクトはオーディオスレッド上では削除されません。オーディオ・スレッドでメモリを確保したり解放したりするのは良い考えとは言えません。そのためReferenceCountedBuffer
オブジェクトはバックグラウンドのスレッドでのみ削除される。
バックグラウンド・スレッドで音声を読む
私たちのアプリケーションは、依然としてメッセージスレッドでオーディオデータを読み込んでいます。メッセージスレッドがブロックされ、大きなファイルはロードに時間がかかる可能性があるため、これは理想的ではありません。実際、バックグラウンドスレッドを使ってこのタスクを実行することもできます。