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

チュートリアル: オーディオプレーヤーの構築

📚 Source Page

このチュートリアルでは、サウンドファイルを開いて再生する方法について説明します。JUCEでサウンドファイルを扱うための重要なクラスをいくつか紹介します。

レベル: 中級
プラットフォーム: Windows, macOS, Linux
クラス: AudioFormatManager, AudioFormatReader, AudioFormatReaderSource, AudioTransportSource, FileChooser, ChangeListener, File, FileChooser

はじめに

このチュートリアルのデモプロジェクトをここからダウンロードしてください: PIP | ZIP。プロジェクトを解凍し、Projucerで最初のヘッダーファイルを開きます。

このステップに関するヘルプが必要な場合は、Tutorial: Projucer Part 1: Getting started with the Projucerを参照してください。

デモプロジェクト

デモプロジェクトは、サウンドファイルの再生を制御するための3つのボタンのインターフェースを提供します。3つのボタンは次のとおりです:

  • サウンドファイルを選択するためのファイルチューザーをユーザーに提示するボタン
  • サウンドを再生するボタン
  • サウンドを停止するボタン

インターフェースを次のスクリーンショットに示します:

サウンドファイルの再生を制御する3つのボタンのインターフェース。
サウンドファイルの再生を制御する3つのボタンのインターフェース。

便利なクラス

AudioSourceクラス

Audio ApplicationテンプレートのgetNextAudioBlock()でオーディオをサンプル単位で生成することもできますが、オーディオを生成および処理するための組み込みツールがいくつかあります。これらにより、アプリケーションコード内でオーディオの各サンプルを処理することなく、高レベルのビルディングブロックをリンクして強力なオーディオアプリケーションを形成できます(JUCEが代わりにこれを行います)。これらのビルディングブロックは、AudioSourceクラスに基づいています。実際、AudioAppComponentクラスに基づくチュートリアル---例えば、Tutorial: Build a white noise generator---のいずれかに従っている場合、すでにAudioSourceクラスを使用しています。AudioAppComponentクラス自体はAudioSourceクラスから継承しており、重要なことに、AudioAppComponentとオーディオハードウェアデバイス間でオーディオをストリーミングするAudioSourcePlayerオブジェクトを含んでいます。getNextAudioBlock()関数で直接オーディオサンプルを生成することもできますが、代わりに複数のAudioSourceオブジェクトを連鎖させて一連のプロセスを形成することもできます。このチュートリアルではこの機能を利用します。

オーディオフォーマット

JUCEは、さまざまなフォーマットでサウンドファイルを読み書きするための多数のツールを提供します。このチュートリアルでは、これらのいくつかを使用します。特に、次のクラスを使用します:

  • AudioFormatManager: このクラスは、オーディオフォーマット(WAV、AIFF、Ogg Vorbisなど)のリストを含み、これらのフォーマットからオーディオデータを読み取るための適切なオブジェクトを作成できます。
  • AudioFormatReader: このクラスは、オーディオファイルの低レベルのファイル読み取り操作を処理し、一貫したフォーマット(一般的にはfloat値の配列)でオーディオを読み取ることができます。AudioFormatManagerオブジェクトが特定のファイルを開くように要求されると、このクラスのインスタンスが作成されます。
  • AudioFormatReaderSource: これはAudioSourceクラスのサブクラスです。AudioFormatReaderオブジェクトからオーディオデータを読み取り、getNextAudioBlock()関数を介してオーディオをレンダリングできます。
  • AudioTransportSource: このクラスは、AudioSourceクラスの別のサブクラスです。AudioFormatReaderSourceオブジェクトの再生を制御できます。この制御には、AudioFormatReaderSourceオブジェクトの再生の開始と停止が含まれます。また、サンプルレート変換を実行したり、必要に応じて事前にオーディオをバッファリングしたりすることもできます。

まとめる

これらのクラスを適切なユーザーインターフェースクラスと組み合わせて、サウンドファイル再生アプリケーションを作成します。この時点で、オーディオファイルを再生するさまざまなフェーズ---または_トランスポート状態_---について考えることが役立ちます。オーディオファイルが読み込まれると、次の4つの可能な状態が考えられます:

  • Stopped: オーディオ再生が停止されており、開始する準備ができています。
  • Starting: オーディオ再生はまだ開始されていませんが、開始するように指示されています。
  • Playing: オーディオが再生中です。
  • Stopping: オーディオが再生中ですが、再生が停止するように指示されており、その後_Stopped_状態に戻ります。

これらの状態を表すために、MainContentComponentクラス内にenumを作成します:

enum TransportState {
Stopped,
Starting,
Playing,
Stopping
};

インターフェースの初期化

MainContentComponentクラスのコンストラクタで、3つのボタンを設定します:

MainContentComponent()
: state (Stopped)
{
addAndMakeVisible (&openButton);
openButton.setButtonText ("Open...");
openButton.onClick = [this] { openButtonClicked(); };

addAndMakeVisible (&playButton);
playButton.setButtonText ("Play");
playButton.onClick = [this] { playButtonClicked(); };
playButton.setColour (juce::TextButton::buttonColourId, juce::Colours::green);
playButton.setEnabled (false);

addAndMakeVisible (&stopButton);
stopButton.setButtonText ("Stop");
stopButton.onClick = [this] { stopButtonClicked(); };
stopButton.setColour (juce::TextButton::buttonColourId, juce::Colours::red);
stopButton.setEnabled (false);

特に、最初にPlayボタンとStopボタンを無効にしていることに注目してください。Playボタンは、有効なファイルが読み込まれると有効になります。ここでは、これら3つのボタンのそれぞれに対してButton::onClickヘルパーオブジェクトにラムダ関数を割り当てていることがわかります(Tutorial: Listeners and Broadcastersを参照)。また、コンストラクタの初期化リストでトランスポート状態を初期化します。

その他の初期化

3つのTextButtonオブジェクトに加えて、MainContentComponentクラスには他に4つのメンバーがあります:

juce::AudioFormatManager formatManager;
std::unique_ptr<juce::AudioFormatReaderSource> readerSource;
juce::AudioTransportSource transportSource;
TransportState state;

ここでは、前述のAudioFormatManagerAudioFormatReaderSourceAudioTransportSourceクラスが表示されます。

MainContentComponentコンストラクタでは、標準フォーマットのリストを登録するためにAudioFormatManagerオブジェクトを初期化する必要があります [1]:

formatManager.registerBasicFormats(); // [1]

最低限、これによりAudioFormatManagerオブジェクトがWAVおよびAIFFフォーマットのリーダーを作成できるようになります。他のフォーマットは、次のスクリーンショットに示すように、プラットフォームとProjucerプロジェクト内のjuce_audio_formatsモジュールで有効になっているオプションによって利用可能になる場合があります:

オーディオフォーマットオプションを示すjuce_audio_formatsモジュールオプション。
オーディオフォーマットオプションを示すjuce_audio_formatsモジュールオプション。

MainContentComponentコンストラクタでは、MainContentComponentオブジェクトをAudioTransportSourceオブジェクトにリスナー [2] として追加して、その状態の変化(例えば、停止したとき)に応答できるようにします:

transportSource.addChangeListener (this); // [2]
ヒント

この場合、関数名は、JUCEの他の多くのリスナークラスのように単にaddListener()ではなく、addChangeListener()です。

AudioTransportSourceの変更への応答

トランスポートの変更が報告されると、changeListenerCallback()関数が呼び出されます。これは、メッセージスレッドで非同期に呼び出されます:

void changeListenerCallback (juce::ChangeBroadcaster* source) override
{
if (source == &transportSource)
{
if (transportSource.isPlaying())
changeState (Playing);
else
changeState (Stopped);
}
}

これは、単にメンバー関数changeState()を呼び出すだけです。

状態の変更

トランスポート状態の変更は、この単一の関数changeState()にローカライズされます。これにより、この機能のすべてのロジックを1か所に保つことができます。この関数は、stateメンバーを更新し、この新しい状態になったときに発生する必要がある他のオブジェクトへの変更をトリガーします。

ヒント

より経験豊富な読者は、このコードを構造化する別の方法として、ステートデザインパターンを使用することをお勧めします。

void changeState (TransportState newState)
{
if (state != newState)
{
state = newState;

switch (state)
{
case Stopped: // [3]
stopButton.setEnabled (false);
playButton.setEnabled (true);
transportSource.setPosition (0.0);
break;

case Starting: // [4]
playButton.setEnabled (false);
transportSource.start();
break;

case Playing: // [5]
stopButton.setEnabled (true);
break;

case Stopping: // [6]
transportSource.stop();
break;
}
}
}
  • [3]: トランスポートが_Stopped_状態に戻ると、Stopボタンを無効にし、Playボタンを有効にし、トランスポート位置をファイルの先頭にリセットします。
  • [4]: _Starting_状態は、ユーザーがPlayボタンをクリックすることによってトリガーされ、これによりAudioTransportSourceオブジェクトに再生を開始するように指示します。この時点で、Playボタンも無効にします。
  • [5]: _Playing_状態は、AudioTransportSourceオブジェクトがchangeListenerCallback()関数を介して変更を報告することによってトリガーされます。ここでは、Stopボタンを有効にします。
  • [6]: _Stopping_状態は、ユーザーがStopボタンをクリックすることによってトリガーされるため、AudioTransportSourceオブジェクトに停止するように指示します。

オーディオの処理

このデモプロジェクトのオーディオ処理は非常に簡単です:AudioAppComponentクラスを介して渡されたAudioSourceChannelInfo構造体をAudioTransportSourceオブジェクトに渡すことで、処理を委譲するだけです:

void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
if (readerSource.get() == nullptr)
{
bufferToFill.clearActiveBufferRegion();
return;
}

transportSource.getNextAudioBlock (bufferToFill);
}

有効なAudioFormatReaderSourceオブジェクトがあるかどうかを最初にチェックし、そうでない場合は出力を単にゼロにします(便利なAudioSourceChannelInfo::clearActiveBufferRegion()関数を使用)。AudioFormatReaderSourceメンバーはstd::unique_ptrオブジェクトに格納されています。これは、ユーザーのアクションに基づいてこれらのオブジェクトを動的に作成する必要があるためです。また、無効なオブジェクトに対してnullptrをチェックすることもできます。

また、使用している他のAudioSourceオブジェクトにprepareToPlay()コールバックを渡すことも忘れないでください:

void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override
{
transportSource.prepareToPlay (samplesPerBlockExpected, sampleRate);
}

そして、releaseResources()コールバックも:

void releaseResources() override
{
transportSource.releaseResources();
}

ファイルを開く

ファイルを開くには、**Open...**ボタンがクリックされたときにFileChooserオブジェクトをポップアップします:

void openButtonClicked()
{
chooser = std::make_unique<juce::FileChooser> ("Select a Wave file to play...",
juce::File {},
"*.wav"); // [7]
auto chooserFlags = juce::FileBrowserComponent::openMode
| juce::FileBrowserComponent::canSelectFiles;

chooser->launchAsync (chooserFlags, [this] (const juce::FileChooser& fc) // [8]
{
auto file = fc.getResult();

if (file != juce::File {}) // [9]
{
auto* reader = formatManager.createReaderFor (file); // [10]

if (reader != nullptr)
{
auto newSource = std::make_unique<juce::AudioFormatReaderSource> (reader, true); // [11]
transportSource.setSource (newSource.get(), 0, nullptr, reader->sampleRate); // [12]
playButton.setEnabled (true); // [13]
readerSource.reset (newSource.release()); // [14]
}
}
});
}
  • [7]: 短いメッセージでFileChooserオブジェクトを作成し、ユーザーが.wavファイルのみを選択できるようにします。
  • [8]: FileChooserオブジェクトをポップアップします。
  • [9]: ユーザーが実際にファイルを選択した場合(キャンセルではなく)、このif()は成功します。
  • [10]: AudioFormatManager::createReaderFor()関数を使用して、この特定のファイルのリーダーを作成しようとします。これは、失敗した場合(例えば、ファイルがAudioFormatManagerオブジェクトが処理できるオーディオフォーマットでない場合)、nullptr値を返します。
  • [11]: 先ほど作成したリーダーを使用して、新しいAudioFormatReaderSourceオブジェクトを作成します。2番目の引数trueは、AudioFormatReaderSourceオブジェクトにAudioFormatReaderオブジェクトを管理し、不要になったときに削除するように指示します。以前に割り当てられたAudioFormatReaderSourceを後続のファイルを開くコマンドで早期に削除しないように、AudioFormatReaderSourceオブジェクトを一時的なstd::unique_ptrオブジェクトに格納します。
  • [12]: AudioFormatReaderSourceオブジェクトは、getNextAudioBlock()関数で使用されているAudioTransportSourceオブジェクトに接続されます。ファイルのサンプルレートがハードウェアのサンプルレートと一致しない場合に備えて、AudioFormatReaderオブジェクトから取得したこれを4番目の引数として渡します。2番目と3番目の引数の詳細については、注意事項を参照してください。AudioTransportSourceソースは、必要なサンプルレート変換を処理します。
  • [13]: Playボタンが有効になり、ユーザーがクリックできるようになります。
  • [14]: AudioTransportSourceが新しく割り当てられたAudioFormatReaderSourceオブジェクトを使用しているはずなので、AudioFormatReaderSourceオブジェクトをreaderSourceメンバーに安全に格納できます。(上記のオーディオの処理で述べたとおり。)これを行うには、std::unique_ptr::release()を使用してローカル変数newSourceから所有権を譲渡する必要があります。
ヒント

新しく割り当てられたAudioFormatReaderSourceオブジェクトを一時的なstd::unique_ptrオブジェクトに格納することには、例外セーフであるという追加の利点があります。AudioTransportSource::setSource()関数呼び出し中に例外がスローされる可能性があり、その場合、std::unique_ptrオブジェクトは不要になったAudioFormatReaderSourceオブジェクトを削除します。この時点で生ポインタが使用されていた場合、AudioFormatReaderSourceオブジェクトを格納するために、例外がスローされるとポインタが宙ぶらりんのままになるため、メモリリークが発生する可能性があります。

ファイルの再生と停止

ファイルを実際に再生するコードをすでに設定しているので、適切な引数でchangeState()関数を呼び出すだけでファイルを再生できます。Playボタンがクリックされたら、次のようにします:

void playButtonClicked()
{
changeState (Starting);
}

ファイルの停止も同様に簡単です。Stopボタンがクリックされたとき:

void stopButtonClicked()
{
changeState (Stopping);
}
注記

演習: FileChooserオブジェクトを作成するときの3番目の引数(filePatternsAllowed)を変更して、アプリケーションがAIFFファイルも読み込めるようにします。ファイルパターンはセミコロンで区切ることができるため、このフォーマットの2つの一般的なファイル拡張子を許可するには"*.wav;*.aif;*.aiff"にする必要があります。

ポーズ機能の追加

それでは、アプリケーションに_ポーズ_機能を追加する手順を説明します。ここでは、ファイルの再生中にPlayボタンをPauseボタンにします(単に無効にするのではなく)。また、サウンドファイルが一時停止されている間、StopボタンをReturn to zeroボタンにします。

まず、TransportState列挙型に_Pausing_と_Paused_の2つの状態を追加する必要があります:

enum TransportState {
Stopped,
Starting,
Playing,
Pausing,
Paused,
Stopping
};

changeState()関数は2つの新しい状態を処理する必要があり、他の状態のコードも更新する必要があります:

void changeState (TransportState newState)
{
if (state != newState)
{
state = newState;

switch (state)
{
case Stopped:
playButton.setButtonText ("Play");
stopButton.setButtonText ("Stop");
stopButton.setEnabled (false);
transportSource.setPosition (0.0);
break;

case Starting:
transportSource.start();
break;

case Playing:
playButton.setButtonText ("Pause");
stopButton.setButtonText ("Stop");
stopButton.setEnabled (true);
break;

case Pausing:
transportSource.stop();
break;

case Paused:
playButton.setButtonText ("Resume");
stopButton.setButtonText ("Return to Zero");
break;

case Stopping:
transportSource.stop();
break;
}
}
}

各状態でボタンを適切に有効/無効にし、ボタンのテキストを正しく更新します。

_Pausing_状態で一時停止を求められたときに、実際にトランスポートを停止していることに注意してください。changeListenerCallback()関数では、一時停止または停止リクエストが行われたかどうかに応じて、正しい状態に移動するようにロジックを変更する必要があります:

void changeListenerCallback (juce::ChangeBroadcaster* source) override
{
if (source == &transportSource)
{
if (transportSource.isPlaying())
changeState (Playing);
else if ((state == Stopping) || (state == Playing))
changeState (Stopped);
else if (Pausing == state)
changeState (Paused);
}
}

Playボタンがクリックされたときのコードを変更する必要があります:

void playButtonClicked()
{
if ((state == Stopped) || (state == Paused))
changeState (Starting);
else if (state == Playing)
changeState (Pausing);
}

そして、Stopボタンがクリックされたとき:

void stopButtonClicked()
{
if (state == Paused)
changeState (Stopped);
else
changeState (Stopping);
}

以上です。これで、アプリケーションをビルドして実行できるはずです。

ヒント

このアプリケーションの変更されたバージョンのソースコードは、デモプロジェクトのPlayingSoundFilesTutorial_02.hファイルにあります。

注記

演習: AudioTransportSourceオブジェクトの現在の時間位置を表示するLabelオブジェクトをインターフェースに追加します。この位置を取得するには、AudioTransportSource::getCurrentPosition()関数を使用できます。また、MainContentComponentクラスをTimerクラスから継承させ、timerCallback()関数で定期的な更新を実行してラベルを更新する必要があります。RelativeTimeクラスを使用して、秒単位の生の時間を分、秒、ミリ秒のより便利なフォーマットに変換することもできます。

ヒント

この演習のソースコードは、デモプロジェクトのPlayingSoundFilesTutorial_03.hファイルにあります。

まとめ

このチュートリアルでは、サウンドファイルの読み取りと再生について紹介しました。特に、次のことを扱いました:

注意事項

AudioTransportSource::setSource()関数の2番目と3番目の引数を使用すると、バックグラウンドスレッドでの先読みバッファリングを制御できます。2番目の引数は使用するバッファサイズで、3番目の引数はバックグラウンド処理に使用されるTimeSliceThreadオブジェクトへのポインタです。この例では、バッファサイズとしてゼロを使用し、スレッドオブジェクトにはnullptr値を使用しています。これがデフォルトです。

参照