その後のその後

iOSエンジニア 堤 修一のブログ github.com/shu223

Audio Unit 再入門

Core Audio においてもっとも低レベルに位置する Audio Unit。リアルタイムで高度なオーディオ波形処理を行いたい場合や複雑なルーティングによるオーディオ処理を実現したい場合、これを使用する必要が出てきます。


が、このフレームワーク、個人的には使用頻度があまり高くない *1 ので、ひさびさに触ってみた際にとっつきにくさを感じました。


慣れてしまえば 全体的なコンセプトはシンプル なのですが、関数の引数がやたら多かったり、構造体の要素がやたら多かったり、慣れてないC言語APIだったりするので、久しぶりに触るとそのあたりが複雑に感じてしまうのかなと。


そんなわけで、次に久しぶりに Audio Unit をいじるときに、 そのあたりの「シンプルな全体感」と、「複雑に感じてしまう部分」を切り分けて見ることができるよう、メモっておきます。

基本的な考え方

Audio Unit の基本コンセプトは、「いろいろなユニットを複数接続し、オーディオ処理を実現する」というもの。


で、そのひとつひとつのユニットがノード(AUNode)、それらが繋がっている全体がグラフ(AUGraph)。この考え方や呼称は直観的にもわかりやすいです。


そして実際の実装手順としても、大まかにいうと下記のように非常にわかりやすいです。この流れさえ把握しておけば、後述する「一見複雑そう」な諸々が出てきても全体感は見失わずにすむかと。

基本的な実装の流れ

1. グラフ(AUGraph)作成

NewAUGraph(&graph);
AUGraphOpen(graph);


2. ノードをグラフに追加する

AUGraphAddNode(graph,
               &ioUnitDescription,
               &ioNode);


3. ノードを接続する

AUGraphConnectNodeInput(graph, ioNode, 1, ioNode, 0);


4. グラフを初期化して処理開始

AUGraphInitialize(graph);
AUGraphStart(graph);

で、これに、下記要素が絡んできます。

  • AudioComponentDescription
  • Audio Unit のプロパティ設定
  • AudioStreamBasicDescription (ASBD)
  • コールバック
  • Audio Converter Sevices や Extended Audio File Services 等の関連サービス


このあたりが絡んでくることによって、コードも長く複雑になり、オーディオフォーマットC言語の知識も必要になってきて、そのあたりの知識の乏しい自分が久々に Audio Unit に触るとうわー難しいってなるのかなと。そんなわけで、整理のため以下にひとつひとつ紐解いておきます。

AudioComponentDescription

前述した基本手順の「ノードをグラフに追加する」手順において、 「どのユニットをノードとして追加するか」 を指定するものが AudioComponentDescription という構造体。


たとえば Remote IO ユニットの場合は下記のようになります。

AudioComponentDescription cd;
cd.componentType            = kAudioUnitType_Output;
cd.componentSubType         = kAudioUnitSubType_RemoteIO;
cd.componentManufacturer    = kAudioUnitManufacturer_Apple;
cd.componentFlags           = 0;
cd.componentFlagsMask       = 0;

AUGraphAddNode(graph, &cd, &ioNode);


こうやって書いてしまうと全然難しいものではないのですが、構造体の要素の数が多く、各定義名も長いので、パッと見のコードの複雑さを助長している気がします。


そんなわけで TTMAudioUnitHelper という Audio Unit のヘルパーライブラリでは、下記のように サブタイプだけ指定すれば AudioComponentDescription を取得できる ラッパーメソッドを用意してあります *2

+ (AudioComponentDescription)audioComponentDescriptionForSubType:(OSType)subType;

Audio Unit のプロパティ設定

グラフに追加した各ノード(のユニット)にプロパティをセットするには、まずノードから AudioUnit 構造体を取得します。

AudioUnit ioUnit;
AUGraphNodeInfo(graph, ioNode, &cd, &ioUnit);


で、取得した AudioUnit 構造体を引数に渡しつつ、プロパティをセットします。

AudioUnitSetProperty(ioUnit,
                     kAudioOutputUnitProperty_EnableIO,
                     kAudioUnitScope_Input,
                     1,
                     &flag,
                     sizeof(flag));


引数が多いですが、関数の定義を見ればわかるかと。

AudioUnitSetProperty(AudioUnit              inUnit,
                    AudioUnitPropertyID     inID,
                    AudioUnitScope          inScope,
                    AudioUnitElement        inElement,
                    const void *            inData,
                    UInt32                  inDataSize)


プロパティのgetも同様です。

AudioUnitGetProperty(AudioUnit              inUnit,
                    AudioUnitPropertyID     inID,
                    AudioUnitScope          inScope,
                    AudioUnitElement        inElement,
                    void *                  outData,
                    UInt32 *                ioDataSize);


プロパティを set / get する実装は、引数が多いので何となく難しい感じに見えてしまうところがありますが、上記の通りやってることはシンプルです。

AudioStreamBasicDescription (ASBD)

オーディオデータフォーマットを表現するための構造体。

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


ユニットごとにサポートしているオーディオデータフォーマットが違うため、ノード間で滞りなくオーディオデータを流すためにこれをプロパティからセットしてやる必要があります。


この ASBD、以下の点を個人的に整理できてないので、また別途記事を書こうと思っています。

  • どのノード間において明示的に get / set する必要があるのか
  • どのノード間において AUConverter ユニットや後述する Audio Converter Sevices で ASBD を変換する必要があるのか
  • ASBD が合ってなければ AUGraphInitialize 実行時にエラーを返してくれる?

コールバック

再生するにしても録音するにしてもこのコールバックの実装は不可欠だし、リアルタイム波形処理もここで行うことになるので、Audio Unit のキモになる部分といえます。が、下記理由により個人的にはややこしく感じてしまいます。

  • いろいろなコールバックの登録方法がある
  • いろいろなコールバックの種類がある
    • 後述する Audio Converter Sevices もコールバック内で処理を行う
  • 引数それぞれの役割を把握してないとAudioUnitのキモである波形処理を書けない
  • C言語的な知識がしっかりと要求される


ここでは、コールバックの何がややこしいのか、という把握だけにとどめておいて、コールバックの詳しい話は別記事で行いたいと思います。

Audio Converter Sevices や Extended Audio File Services 等の関連サービス

サウンドファイルの再生は、AVAudioPlayer を使用すれば恐ろしく簡単にできるのですが、Audio Unit を使う場合、オーディオデータを RemoteIO で再生できるフォーマットに変換するために、Audio Converter Sevices や Extended Audio File Services を利用する必要があります。


高レベルAPIに慣れてしまった僕のようなゆとりiOSエンジニアからすると「たかがファイル再生」と油断しているところに、Audio Converter Sevices では変換用コールバックが再生用とは別途必要だったり、マジッククッキーなるよくわからない概念が登場したりするので、「Audio Unitこわい」という印象を持ってしまう要因のひとつになってしまっている気がします。


また Audio Converter Sevices を使うにしろ Extended Audio File Services を使うにしろ、この手の実装はファイルから読み込んだオーディオデータを保持しておくために独自の構造体を定義して取り扱うことが多く、他人のサンプルコードを参考にしようとしても、「パッと把握しづらい」というつらさもあります。


このあたり、AUConverter ユニットや AUAudioFilePlayer ユニットを使用すればもっとシンプルにできるのかなと期待しつつ、また別途記事を書こうと思います。

参考記事

Audio Unit を含む、iOSのオーディオ処理に役立つ参考書籍を下記記事にまとめています。


Audio Unit のユニット種別は AudioComponentDescription 構造体の componentSubType によって規定されますが、その一覧を下記記事にまとめています。


参考になるサンプルコードのまとめ。使用されているユニットも付記してあるので手前味噌ながら便利です。

*1:自分の場合は1年3ヵ月ぶり2回目

*2:まだ全てのサブタイプをサポートしていませんし、オプションは考慮できていません。pull request大歓迎です。