チュートリアル: AudioSampleBufferクラスを使ったオーディオのループ再生
このチュートリアルでは、AudioSampleBufferオブジェクトに格納されたオーディオを再生およびループする方法を示します。これは、録音されたオーディオデータを操作するサンプラーアプリケーションの有用な基礎となります。
レベル: 中級
プラットフォーム: Windows, macOS, Linux
クラス: AudioBuffer, AudioFormatReader, AudioAppComponent
はじめに
このチュートリアルは、すでにTutorial: Build a white noise generatorとTutorial: Build an audio playerを完了していることを前提としています。まだの場合は、まずこれらを確認してください。
このチュートリアルのデモプロジェクトをここからダウンロードしてください: PIP | ZIP。プロジェクトを解凍し、Projucerで最初のヘッダーファイルを開きます。
このステップに関するヘルプが必要な場合は、Tutorial: Projucer Part 1: Getting started with the Projucerを参照してください。
デモプロジェクト
このチュートリアルのデモプロジェクトでは、ユーザーがサウンドファイルを開き、ファイル全体をAudioSampleBufferオブジェクトに読み込み、ループで再生できます。Tutorial: Build an audio playerでは、AudioTransportSourceオブジェクトに接続されたAudioFormatReaderSourceオブジェクトを使用してサウンドファイルを再生しました。この方法でも、AudioFormatReaderSource::setLooping()関数を使用してAudioFormatReaderSourceオブジェクトのループフラグを有効にすることでループが可能です。
このチュートリアルの議論に関連するすべてのコードは、デモプロジェクトのMainContentComponentクラスにあります。
サンプルデータをメモリに読み込む
サウンドファイルの再生には、組み込みのクラスを使用する方が良い場合が多くあります。自分で行う必要がある場合もあり、このチュートリアルではいくつかの技術を紹介します。サンプラーアプリケーションは、特にサウンドが比較的短い場合、このようにサウンドファイルデータをメモリに読み込むのが一般的です(SamplerSoundクラスを例として参照)。サウンドの合成は、AudioSampleBufferオブジェクトにウェーブテーブルを格納し、必要な音楽ピッチを生成するために適切なレートでループすることでも実現できます。これはTutorial: Wavetable synthesisで探求されています。
このチュートリアルでは、ファイルへのアクセスとオーディオスレッドでのオーディオ処理を組み合わせる際に遭遇する可能性のあるマルチスレッドの問題についても強調しています。これらの問題のいくつかは表面的には単純に見えますが、クラッシュやオーディオのグリッチを避けるためには、慎重に適用された技術が必要になることがよくあります。これらの技術は、Tutorial: Looping audio using the AudioSampleBuffer class (advanced)でさらに探求されています。
なぜ長さの制限があるのか?
デモプロジェクトでは、読み込めるサウンドファイルの長さを2秒未満に制限しています。この制限はかなり恣意的ですが、大きく分けて2つの理由があります:
- ファイル全体が非常に大きい場合、コンピュータの物理メモリが不足する可能性があります。もちろん、実際のアプリケーションでは、はるかに高い制限を使用できます。44.1kHzのサンプルレートで2秒のステレオオーディオファイルをAudioSampleBufferオブジェクトに読み込む場合、705,600バイトのメモリしか占有しません。(注記を参照)
- かなり短いファイルであっても、読み込みには無視できない時間がかかります。
ポイント1について: コンピュータが持つ物理メモリの量を超えると、仮想メモリ(つまり、ハードドライブなどの二次ストレージ)の使用を開始する可能性があります。これは、そもそもデータをメモリに読み込む目的を完全に損ないます!もちろん、メモリが不足した場合、一部のデバイスでは操作が単に失敗する可能性があります。
ポイント2について: FileChooser::browseForFileToOpen()関数がユーザーが選択したファイルを返した後、オーディオデータを直接読み込むことで例をシンプルに保ちます。これは、すべてのオーディオがディスクからAudioSampleBufferオブジェクトに読み込まれるまで、_メッセージスレッド_がブロックされることを意味します。短いサウンドであっても、ユーザーインターフェースをユーザーにとって可能な限り応答性の高い状態に保つために、実際にはバックグラウンドスレッドでこれを行う必要があります。長いサウンドの場合、遅延と応答性の低下は非常に目立ちます。別の(バックグラウンド)スレッドを追加すると、この例の複雑さが増します。この方法でバックグラウンドスレッドでファイルを読み込む方法の例については、Tutorial: Looping audio using the AudioSampleBuffer class (advanced)を参照してください。
演習: シンプルに保つため、デモプロジェクトでは、より長いファイルを読み込もうとした場合にエラーを報告しません---単に失敗するだけです。このようなエラー報告の追加は、追加の演習として残されています。
サウンドファイルの読み取り
ユーザーが**Open...**ボタンをクリックすると、ファイルチューザーが表示されます。その後、ファイル全体がMainContentComponentクラスのfileBuffer AudioSampleBufferメンバーに読み込まれます。
void openButtonClicked()
{
shutdownAudio(); // [1]
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)); // [2]
if (reader.get() != nullptr)
{
auto duration = (float) reader->lengthInSamples / reader->sampleRate; // [3]
if (duration < 2)
{
fileBuffer.setSize ((int) reader->numChannels, (int) reader->lengthInSamples); // [4]
reader->read (&fileBuffer, // [5]
0, // [5.1]
(int) reader->lengthInSamples, // [5.2]
0, // [5.3]
true, // [5.4]
true); // [5.5]
position = 0; // [6]
setAudioChannels (0, (int) reader->numChannels); // [7]
}
else
{
// ファイルが2秒以上であるというエラーを処理..
}
}
});
}
- [1]: 新しいファイルを開くたびに、AudioAppComponentオブジェクトのオーディオシステムをシャットダウンすることに注意してください。これは、すでに示唆されているマルチスレッドの問題のいくつかを回避するためです。オーディオシステムがシャットダウンされると、Button::onClickラムダ関数(_メッセージスレッド_からこの
openButtonClicked()関数を呼び出す)の呼び出し内にいる間に、getNextAudioBlock()関数が_オーディオスレッド_で呼び出される危険性はありません。 - [2]: ここでは、AudioFormatManagerオブジェクトを使用してAudioFormatReaderオブジェクトを作成します。このオブジェクトを自分で管理する必要があるため、std::unique_ptrオブジェクトに格納していることに注意してください。(Tutorial: Build an audio playerでは、AudioFormatReaderオブジェクトをAudioFormatReaderSourceオブジェクトに渡して管理させました。)この操作はリーダーオブジェクトの作成に失敗する可能性があるため、次の行で
readerポインタがnullptr値でないことを確認する必要があります。 - [3]: ここでは、ファイルのサンプル単位の長さをそのサンプルレートで割ることで、サウンドファイルの長さを計算します。次の行で、これが2秒未満であることを確認します。
- [4]: ここでは、AudioFormatReaderオブジェクトからのチャンネル数と長さを使用して、AudioSampleBuffer::setSize()関数を呼び出してAudioSampleBufferオブジェクトのサイズを変更します。
- [5]: これは、AudioFormatReader::read()関数を使用して、AudioFormatReaderオブジェクトからオーディオデータをAudioSampleBuffer
fileBufferメンバーに読み込みます。引数は次のとおりです:- [5.1]: データの書き込みが開始されるAudioSampleBufferオブジェクト内の宛先開始サンプル。
- [5.2]: 読み取るサンプル数。
- [5.3]: 読み取りが開始されるAudioFormatReaderオブジェクト内の開始サンプル。
- [5.4]: ステレオ(またはその他の2チャンネル)ファイルの場合、このフラグは左チャンネルを読み取るかどうかを示します。
- [5.5]: ステレオファイルの場合、このフラグは右チャンネルを読み取るかどうかを示します。
- [6]: 再生中にバッファ内の最新の読み取り位置を保存する必要があります。これにより、
positionメンバーがゼロにリセットされます。 - [7]: これにより、オーディオシステムが再び起動します。ここでは、サウンドファイルのチャンネル数を使用して、同じチャンネル数でオーディオデバイスを構成しようとする機会があります。
オーディオの処理
getNextAudioBlock()関数では、fileBuffer AudioSampleBufferメンバーから適切な数のサンプルが読み取られ、AudioSourceChannelInfo構造体内のAudioSampleBufferオブジェクトに書き出されます。
ファイルからオーディオデータを読み取る間、positionメンバーを使用して現在の読み取り位置を追跡します(指定されたサンプルブロックのすべてのチャンネルのオーディオが処理された後、注意深く更新します):
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
auto numInputChannels = fileBuffer.getNumChannels();
auto numOutputChannels = bufferToFill.buffer->getNumChannels();
auto outputSamplesRemaining = bufferToFill.numSamples; // [8]
auto outputSamplesOffset = bufferToFill.startSample; // [9]
while (outputSamplesRemaining > 0)
{
auto bufferSamplesRemaining = fileBuffer.getNumSamples() - position; // [10]
auto samplesThisTime = juce::jmin (outputSamplesRemaining, bufferSamplesRemaining); // [11]
for (auto channel = 0; channel < numOutputChannels; ++channel)
{
bufferToFill.buffer->copyFrom (channel, // [12]
outputSamplesOffset, // [12.1]
fileBuffer, // [12.2]
channel % numInputChannels, // [12.3]
position, // [12.4]
samplesThisTime); // [12.5]
}
outputSamplesRemaining -= samplesThisTime; // [13]
outputSamplesOffset += samplesThisTime; // [14]
position += samplesThisTime; // [15]
if (position == fileBuffer.getNumSamples())
position = 0; // [16]
}
}
- [8]:
outputSamplesRemaining変数は、getNextAudioBlock()関数が出力する必要がある合計サンプル数を格納し、AudioSourceChannelInfo構造体からコピーを取ります。これを使用して、次の行で始まるwhile()ループを終了する必要があるかどうかを確認します。 - [9]: また、宛先バッファ内のオフセットとして使用するために、AudioSourceChannelInfo::startSample値のコピーも取ります。
- [10]: ここでは、読み取り元のバッファに残っているサンプル数を計算します。
- [11]: この
while()ループのパスでは、getNextAudioBlock()関数へのこの呼び出しの残りのサンプルと、バッファ内の残りのサンプルのうち小さい方を出力する必要があります---jmin()関数を使用します。これがgetNextAudioBlock()関数へのこの呼び出しの合計サンプル数よりも少ない場合、終了する前にwhile()ループがもう1回パスされます。 - [12]: 各出力チャンネルに対して、AudioSampleBuffer::copyFrom()関数を使用して、あるバッファの1つのチャンネルから別のバッファのチャンネルにデータのセクションをコピーします。ここでは、宛先チャンネルインデックスを指定します。
- [12.1]: これは、宛先バッファ内のサンプルオフセットです。
- [12.2]: これは、コピー元のソースAudioSampleBufferオブジェクトです。
- [12.3]: これは、ソースバッファのチャンネルインデックスです。ソースバッファが宛先バッファよりも少ないチャンネルを持つ場合に備えて、このモジュロ計算を使用します。例えば、モノラルソースバッファの場合、これは常にゼロになり、同じデータが各出力チャンネルにコピーされます。
- [12.4]: これは、ソースバッファで読み取りを開始する位置です。
- [12.5]: 先ほど計算した読み取るサンプル数です。
- [13]: 処理したばかりのサンプル数を
outputSamplesRemaining変数から差し引きます。 - [14]:
while()ループの別のパスがある場合に備えて、outputSamplesOffsetを同じ量だけインクリメントします。 - [15]:
positionメンバーも同じ量だけオフセットします。 - [16]: 最後に、
positionメンバーがfileBufferAudioSampleBufferオブジェクトの終わりに達したかどうかを確認し、必要に応じてループを形成するためにゼロにリセットします。
演習: オーディオファイルのオーディオ再生レベルを制御するレベルスライダーを追加します(Tutorial: Control audio levelsを参照)。AudioSampleBuffer::applyGain()またはAudioSampleBuffer::applyGainRamp()関数を使用して、AudioSampleBufferオブジェクト内のデータにゲインを適用できます。
マルチスレッドの問題
前述のように、このチュートリアルでは、ユーザーが**Open...**ボタンをクリックするたびにオーディオをシャットダウンして再起動することで、マルチスレッドの問題を回避しています。しかし、これを行わなかった場合、何が起こり得るでしょうか?多くのことがうまくいかない可能性があり、そのすべてはgetNextAudioBlock()関数とopenButtonClicked()関数の両方が異なるスレッドで同時に実行される可能性があるという事実に関係しています。以下にいくつかの例を示します:
- アプリケーションがすでにオーディオファイルを再生していて、ユーザーが**Open...**ボタンをクリックして新しいファイルを選択したとします。オーディオスレッドが[4]と[5]の間でこの関数を中断したとします。バッファはリサイズされましたが、データはバッファに書き込まれていません。バッファには前のファイルのオーディオデータがまだ含まれている可能性がありますが、これはリサイズ時にバッファのメモリを移動する必要があったかどうかによります。いずれにせよ、おそらくグリッチが発生します。
getNextAudioBlock()関数がopenButtonClicked()関数のコードによって中断される可能性があります。これが[11]の直後に発生し、openButtonClicked()関数が[4]に到達したばかりだとします。バッファは以前より短くリサイズされる可能性がありますが、数行前にすでに開始点を計算しています。これはメモリアクセスエラーにつながり、アプリケーションがクラッシュする可能性があります。getNextAudioBlock()関数がAudioSampleBuffer::copyFrom()関数を呼び出している間に中断される可能性があります。繰り返しになりますが、この実装によっては、アクセスすべきでないメモリにアクセスする可能性があります。
他にもうまくいかない可能性のあることがいくつかあります。スレッド間でメモリアクセスを同期するためにクリティカルセクションを使用することに慣れているかもしれません。これは可能な解決策の1つに過ぎませんが、オーディオコードでクリティカルセクションを使用すると、オーディオドロップアウトを引き起こす可能性のある優先度逆転につながる可能性があるため、注意が必要です。Tutorial: Looping audio using the AudioSampleBuffer class (advanced)では、クリティカルセクションを回避する解決策を見ていきます。
注記
44.1kHzでの2秒のステレオオーディオは、AudioSampleBufferオブジェクトで705,600バイトを使用します。これは、次の要素があるためです:
- 2チャンネル
- 2秒
- 44,100サンプル
- 4バイト/サンプル(
float型を使用)
これらを掛け合わせると、結果は次のようになります: 2 x 2 x 44100 x 4 = 705600
まとめ
このチュートリアルでは、次のことを紹介しました:
- サウンドファイルから直接オーディオデータを読み取る方法。
- 再生用にデータをバッファにコピーする方法。
- ウェーブテーブルバッファを使用したシンプルなサンプラーアプリケーションとシンセサイザーの基礎。
- オーディオアプリケーションに存在する潜在的なマルチスレッドの問題のいくつか。