今回は、Audio Queueを使ってみよう。Audio Queueは、Audio Toolboxフレームワークの中において、主役と呼べるライブラリだ。柔軟なオーディオの再生が可能になる。

楽器アプリを実現するためのライブラリとして、前回はSystem Sound Serviceを使った。今回のAudio Queueがこれと大きく異なるのは、再生音の繰り返しの有無である。Audio Queueを使えば、任意のポイントで繰り返し再生を行う事ができるようになるのだ。これにより、楽器アプリの表現力は大きく広がる事になる。

Audio Queueとバッファ

Audio Queueの特徴は、名前の通り、音データをキューイングすることだ。音データを格納するためのバッファを複数用意して、そこにデータを格納してキューを作成する。バッファへのデータの格納は、アプリケーションへのコールバックで行われる。

Audio Queueの動作を理解するには、Appleが公開しているドキュメントを見るのが分かりやすい。次の図が、Audio Queueを使って音を再生するときのプロセスだ。

Audio Queueの動作図(Apple Developer Connectionより)

最初に、アプリケーションはキューとバッファを作成し、初期データをバッファに格納する(1)。バッファにデータが入ると、それはキューへ移動する。その状態で再生を行うと(2)、キューにあるバッファを読み込みながら再生が始まる(3)。バッファのデータを読み込み終わると次のバッファへと移動し(4)、空になったバッファがコールバックを使ってアプリケーションに通知される(5)。アプリケーションはバッファに必要なデータを満たして、再びキューに追加するのだ(6)。

これがAudio Queueの再生プロセスだ。バッファへのデータ格納が、キューにあるデータを再生しきる前に完了する事ができれば、スムーズに音を再生する事ができる。

キューとコールバックによる方式は、多くの利点がある。まず、事前にすべてのオーディオデータを用意する必要がない。再生する直前に、必要な分だけデータを用意すればいいのだ。これにより、使用メモリは少なくなる。また、バッファに任意のデータを格納できるとこも注目だ。この仕組みのため、単純にオーディオファイルの先頭から最後まで再生するといったものだけでなく、任意のポイントでループさせたり、別のオーディオファイルのデータを組み合わせたり、といったことが可能になる。または、プログラム中で波形を作成して再生するといった、シンセサイズ機能も実現できる。

欠点をあげるならば、データを格納してから実際に再生されるまでの時間がアプリケーションから決定できない、ということがある。つまり、レイテンシが長いという事だ。レイテンシを短くするには、バッファの数を少なくしたり、サイズを小さくすればいい。だがそうすると、バッファへのデータ格納が再生時間に間に合わないという自体が発生する。これは、音飛びという現象になって現れてしまう。

以上のことから、Audio Queueは長時間の音楽再生には向くが、ゲームのようにユーザの反応にリアルタイムに即応するタイプのものには不向きと言えるだろう。

データフォーマットの決定とAudio Queueの作成

では、Audio Queueの使い方を説明しよう。先に言っておくが、なかなか面倒なところがある。

Audio Queueとバッファを作成して、音を再生する準備を整えるには、以下のような手順が必要になる。

  1. 再生するオーディオのデータフォーマットを決定する
  2. データフォーマット、コールバック関数などを指定して、Audio Queueを作成する
  3. バッファの数と大きさを決定する
  4. バッファを確保する

まず、Audio Queueに渡すデータのフォーマットを決定する必要がある。もしオーディオファイルを再生するのであれば、そのオーディオフォーマットを取得する事になる。データフォーマットは、AudioStreamBasicDescription構造体で指定する。

List 1.

struct AudioStreamBasicDescription {
    Float64 mSampleRate;
    UInt32  mFormatID;
    UInt32  mFormatFlags;
    UInt32  mBytesPerPacket;
    UInt32  mFramesPerPacket;
    UInt32  mBytesPerFrame;
    UInt32  mChannelsPerFrame;
    UInt32  mBitsPerChannel;
    UInt32  mReserved;
};

この構造体で注目してほしいのは、mFormatIDフィールドだ。ここにオーディオフォーマットを指定する事ができる。実はAudio Queueでは、MP3やAACの再生を行う事ができる。それらのフォーマットはここに指定するのだ。

もしオーディオファイルからデータを読み込むのであれば、そのフォーマットは、Audio FileライブラリのAPIである、AudioFileGetPropertyを使って取得する事ができる。次のようなソースコードになるだろう。

List 2.
    OSStatus    status;
    UInt32      size;

    // オーディオファイルを開く
    AudioFileID audioFileId;
    status = AudioFileOpenURL(
            (CFURLRef)[NSURL fileURLWithPath:path],
            kAudioFileReadPermission, 0, &audioFileId);

    // オーディオデータフォーマットを取得する
    AudioStreamBasicDescription audioBasicDesc;
    size = sizeof(audioBasicDesc);
    status = AudioFileGetProperty(
        audioFileId, kAudioFilePropertyDataFormat, &size, &audioBasicDesc);

まず、AudioFileOpenURLを使ってオーディオファイルをオープンする。そして、AudioFileGetPropertyを、kAudioFilePropertyDataFormatを指定して呼び出す。これで、データフォーマットが取得できるのだ。

続いて、Audio Queueを作成しよう。Audio Queueには、再生用と録音用のものとがある。今回使うのは、再生用だ。AudioQueueNewOutputを使って作成する。

List 3.

    // オーディオキューを作成する
    AudioQueueRef   audioQueue;
    status = AudioQueueNewOutput(
            &audioBasicDesc, audioQueueOutputCallback, self,
            CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0, audioQueue);

第一引き数に先ほど取得したデータフォーマット、第二引き数にデータを核のするためのコールバック関数を指定する。コールバック関数は、次のような型のものになる。

List 4.

typedef void (*AudioQueueOutputCallback) (
    void                 *inUserData,
    AudioQueueRef        inAQ,
    AudioQueueBufferRef  inBuffer
);

第二引き数がAudio Queue、第三引き数がこの後に説明するバッファ型になっている。

バッファサイズの決定とバッファの作成

続いて、バッファを作成しよう。まず、バッファの数と大きさを決定する必要がある。

バッファの数は任意だ。多くすればその分再生が安定する。その代わりに、レイテンシが大きくなる。ここは、アプリケーションの動作を見ながら調整する事になる。

問題は、バッファの大きさだ。詳しくは後述するが、実はバッファにオーディオデータを書き込む際は、それが何パケット分のデータに相当するかを指定する必要がある。適当にデータを書き込むだけではだめなのだ。従って、バッファに何パケットのデータを書き込み、そのサイズがいくつなのか、を知る必要がある。

これは、非圧縮データであれば簡単だ。パケット毎のデータサイズは固定になる。問題はMP3やAACといった、圧縮データの場合だ。パケットサイズが可変になる。こういったときには、そのオーディオファイルにある最大パケットサイズを求めて、それをサポートできるようにしよう。そして、任意の時間のデータを格納できるようにバッファサイズを決定する。

List 5.

    // 最大パケットサイズを取得する
    UInt32  maxPacketSize;
    size = sizeof(maxPacketSize);
    status = AudioFileGetProperty(
            &audioFileId, kAudioFilePropertyPacketSizeUpperBound,
            &size, &maxPacketSize);

    // 単位時間あたりのパケット数を計算する
    Float64 numPacketsPerTime;
    numPacketsPerTime = audioBasicDesc.mSampleRate / audioBasicDesc.mFramesPerPacket;

    // 0.5秒分のパケットデータサイズを計算する
    UInt32  bufferSize;
    bufferSize = numPacketsPerTime * maxPacketSize * 0.5;

バッファサイズは決定できた。あとは、バッファを作成すれば、完了である。

List 6.

    // オーディオキューバッファを作成する
    AudioQueueBufferRef audioBuffer;
    status = AudioQueueAllocateBuffer(audioQueues, bufferSize, &audioBuffers);

これでキューとバッファの準備は整った。次回は、バッファにデータを格納するコールバックについて説明しよう。