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

チュートリアルAudioSampleBufferクラスを使ったオーディオのループ再生

📚 Source Page

このチュートリアルでは、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を使ってサウンドファイルを再生した。AudioFormatReaderSourceオブジェクトに接続されたAudioTransportSourceオブジェクトでサウンドを再生します。を有効にすれば、この方法でループ再生が可能です。AudioFormatReaderSourceオブジェクトのループフラグAudioFormatReaderSource::setLooping()関数である。

このチュートリアルの議論に関連するコードはすべて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)この方法でバックグラウンド・スレッドにファイルをロードする方法の例を参照されたい。

エクササイズ

シンプルにするために、デモ・プロジェクトでは長いファイルをロードしようとしてもエラーは報告されない。このようなエラー報告を追加することは、追加の練習としてあなたに残されています。

サウンドファイルの読み込み

ユーザーが**オープン...**ボタンをクリックすると、ファイル・セレクタが表示される。ファイル全体がAudioSampleBufferメンバーfileBuffer私たちのMainContentComponentクラスである。

    void openButtonClicked()
{
shutdownAudio(); // [1]

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)); // [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
{
// handle the error that the file is 2 seconds or longer..
}
}
});
}
  • [1]オーディオシステムをシャットダウンしていることに注目してください。AudioAppComponentオブジェクトを生成する。これは、すでにほのめかしたマルチスレッドの問題を避けるためだ。オーディオシステムがシャットダウンされれば、私たちのgetNextAudioBlock()関数はオーディオスレッドへのコール内にいる間は、まだButton::onClickラムダ関数(これは、このopenButtonClicked()関数からメッセージスレッド).

  • [2]ここではAudioFormatReaderオブジェクトを使用する。AudioFormatManagerオブジェクトに格納している。このオブジェクトを自分たちで管理する必要があるため、std::unique_ptrオブジェクトに格納していることに注意してほしい。(つまりTutorial: Build an audio playerを渡す。AudioFormatReaderオブジェクトをAudioFormatReaderSourceオブジェクトを生成して管理してくれる)。この操作はリーダー・オブジェクトの生成に失敗するかもしれない。readerポインタはnullptrの値を次の行に書く。

  • [3]ここでは、ファイルの長さ(サンプル数)をサンプル・レートで割って、サウンド・ファイルの長さを計算します。これが2秒以下であることを次の行でチェックする。

  • [4]ここではAudioSampleBufferオブジェクトを呼び出す。AudioSampleBuffer::setSize()関数のチャンネル数と長さを使用します。AudioFormatReaderオブジェクトがある。

  • [5]オーディオ・データをAudioFormatReaderオブジェクトをAudioSampleBuffer fileBufferを使用している。AudioFormatReader::read()関数を使用します。引数は以下の通り:

  • [5.1]のデスティネーション開始サンプル。AudioSampleBufferオブジェクトにデータが書き込まれる。

  • [5.2]読み込むサンプル数。

  • [5.3]の開始サンプル。AudioFormatReaderオブジェクトで、読み取りが開始される。

  • [5.4]ステレオ(または他の2チャンネル)ファイルの場合、このフラグは左チャンネルを読むかどうかを示す。

  • [5.5]ステレオ・ファイルの場合、右チャンネルを読み込むかどうかを示す。

  • [6]バッファ内の最新の読み取り位置を、再生しながら保存する必要がある。これによりpositionメンバーをゼロにする。

  • [7]これでオーディオシステムが再び起動します。ここで、サウンドファイルのチャンネル数を使って、オーディオデバイスを同じチャンネル数で構成してみる機会があります。

オーディオの処理

の中でgetNextAudioBlock()関数から適切なサンプル数が読み込まれる。fileBuffer AudioSampleBufferを書き出す。AudioSampleBufferオブジェクトをAudioSourceChannelInfo構造体

ファイルからオーディオデータを読み込んでいる間、現在の読み込み位置は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()関数をもう1回パスすることになる。while()ループを終了する。

  • [12]各出力チャンネルにはAudioSampleBuffer::copyFrom()関数を使用して、あるバッファのあるチャンネルから別のバッファのあるチャンネルにデータのセクションをコピーすることができます。ここでは、コピー先のチャネルインデックスを指定します。

  • [12.1]デスティネーションバッファ内のサンプルオフセット。

  • [12.2]ソースはこちらAudioSampleBufferオブジェクトをコピーする。

  • [12.3]これはソースバッファのチャンネルインデックスです。ソースバッファのチャンネル数がデスティネーションバッファより少ない場合、このモジュロ計算を使用します。例えば、モノラル・ソース・バッファの場合、この結果は常にゼロとなり、同じデータが各出力チャンネルにコピーされます。

  • [12.4]ソース・バッファの読み込み開始位置。

  • [12.5]先ほど計算した読み込むサンプル数。

  • [13]から、今処理したサンプル数を差し引きます。outputSamplesRemaining変数である。

  • [14]をインクリメントする。outputSamplesOffsetをもう1回パスする場合は、同額を上乗せする。while()走る。

  • [15]オフセットpositionメンバーも同額。

  • [16]最後にpositionが終了した。fileBuffer AudioSampleBufferオブジェクトをゼロにリセットし、必要に応じてループを形成する。

エクササイズ

レベルスライダーを追加して、オーディオファイルの再生レベルをコントロールする (Tutorial: Control audio levels).を使うことができる。AudioSampleBuffer::applyGain()またはAudioSampleBuffer::applyGainRamp()のデータにゲインを適用する関数である。AudioSampleBufferオブジェクトがある。

マルチスレッドの問題

前述したように、このチュートリアルでは、ユーザーが**オープン...**ボタンだ。しかし、もしそうしなかったらどうなるだろうか?うまくいかないことはたくさんある。getNextAudioBlock()そしてopenButtonClicked()関数は異なるスレッドで同時に実行される可能性がある。以下はその例である:

  • アプリケーションがすでにオーディオファイルを再生していて、ユーザーが**オープン...**ボタンをクリックし、新しいファイルを選択する。オーディオ・スレッドがこの関数に割り込むとする。[4]そして[5].バッファのサイズが変更されたが、データがバッファに書き込まれていない。バッファには前のファイルのオーディオデータが残っているかもしれませんが、バッファのサイズが変更されたときに、バッファのメモリを移動する必要があったかどうかによります。いずれにしても、おそらく不具合が発生するでしょう。
  • 可能性はある。getNextAudioBlock()のコードによって中断される可能性がある。openButtonClicked()関数を使用します。これが[11]そしてopenButtonClicked()に達したところである。[4].バッファは以前より短くリサイズされるかもしれないが、すでに数行前に開始点を計算している。これはメモリ・アクセス・エラーにつながり、アプリケーションがクラッシュする可能性がある。
  • についてgetNextAudioBlock()関数を呼び出している間に中断される可能性がある。AudioSampleBuffer::copyFrom()関数を使用している。これも実装次第で、アクセスすべきでないメモリにアクセスしてしまう可能性がある。
警告

他にもうまくいかないことはいくつもある。あなたはcritical sectionを使ってスレッド間のメモリ・アクセスを同期させることができる。これはひとつの解決策に過ぎないが、スレッド間のメモリ・アクセスを同期させるためにcritical sectionを引き起こす可能性がある。priority inversionこれはオーディオのドロップアウトを引き起こす可能性がある。の重要なセクションを回避するソリューションを見てみましょう。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

概要

このチュートリアルでは、次のことを紹介した:

  • サウンドファイルからオーディオデータを直接読み込む方法。
  • データを再生用のバッファにコピーする方法。
  • シンプルなサンプラー・アプリケーションや、ウェーブテーブル・バッファを使ったシンセサイザーの基礎となる。
  • オーディオアプリケーションに存在する潜在的なマルチスレッド問題のいくつか。

こちらも参照