メインコンテンツまでスキップ

チュートリアルAudioSampleBufferクラスを使ったオーディオのループ(上級者向け)

📚 Source Page

このチュートリアルでは、オーディオファイルを再生してループさせる方法を説明します。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オブジェクトはバックグラウンドのスレッドでのみ削除される。

バックグラウンド・スレッドで音声を読む

私たちのアプリケーションは、依然としてメッセージスレッドでオーディオデータを読み込んでいます。メッセージスレッドがブロックされ、大きなファイルはロードに時間がかかる可能性があるため、これは理想的ではありません。実際、バックグラウンドスレッドを使ってこのタスクを実行することもできます。

バックグラウンド・スレッドにファイル・パスを渡す

まず、以下のメンバーをMainContentComponentクラスである:

    juce::CriticalSection pathMutex;
juce::String chosenPath;

を変更する。openButtonClicked()関数をスワップファイルのフル・パスをこのメンバに渡す。文字列の入れ替えは技術的にスレッドセーフではないので、他のスレッドがファイルを変更しようとしないようにロックをかける必要がある。chosenPathこのスレッドが使っている間は。

    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;

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 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マルチスレッド・アプリケーションで。
  • 不要になったオブジェクトの削除や、ファイルの読み取り操作などのタスクを実行するバックグラウンド・スレッドを実装する。

こちらも参照