プログラミング」カテゴリーアーカイブ

Grand Central Dispatch その3 apply

前回のコードのように同じ処理をforループでキューに渡すようなとき、それ専用の関数がディスパッチには既に用意されています。dispatch_applyです。

void
dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));

iterationsの数だけブロックをキューに渡して処理させます。ブロックはsize_tの引数を一つだけ持ち、よくあるforループと同じように0から1ずつ増やした値が渡されます。

まずは、サンプルコードです。

#include <stdio.h>
#include <dispatch/dispatch.h>
int main (int argc, const char * argv[]) {
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    __block int sum = 0;
    
    dispatch_apply(10, queue, ^(size_t i){
        sum += i;
    });
    
    printf("sum = %d\n", sum); // sumは45になる
    
    return 0;
}

これを実行すると、0から9までがsumに足されて45になります。

dispatch_applyはグローバルキューに渡していても、ちゃんと全てのブロックが処理されるのを待ちますので、dispatch_main()が無くてもアプリが終了する前に全て足された答えが出ます。ですが、sync的に順番に処理されている訳ではなく、並列に処理はされています。ただ、asyncと同じという訳でもなく、applyでは、呼び出したスレッドと別スレッドを組み合わせて並列にブロックが実行されているようです(asyncは別スレッドだけ)。

ブロックの途中でスリープさせたりとかして試してみると、applyでは同時に2つまでしか処理されません(試したのがMacBook Airでコアが2つだから?)。asyncだといくらでも同時に実行されてしまいます。このあたりは、使い道の違いって感じですかね。

Grand Central Dispatch その2 Queue

Queueを作る

ディスパッチの基本的な使い方は、処理したいコードをブロックや関数で用意しておいて、キューにそのブロックや関数を渡すという流れになります。

ディスパッチのキューには2種類あります。アプリ(OS?)に最初から一つ用意されているグローバルキューと、自分で作るキューです。グローバルキューは並列に処理できますが、自分で作るキュー(以後シリアルキューと書いて区別します)は入れていった順番に直列でしか処理できません。

キューはdispatch_queue_tという型のオブジェクトで表されます。ディスパッチのオブジェクトは他にもいくつかありますが、それは次回以降に後回しにします。

グローバルキューを取得するにはこの関数を使います。

dispatch_queue_t
dispatch_get_global_queue(long priority, unsigned long flags);

一つ目の引数にはキューが処理される優先度を指定します。普通はDISPATCH_QUEUE_PRIORITY_DEFAULTを指定すれば良いと思います。DEFAULT以外にもHIGHとLOWがあります。HIGHの処理が全て終わってからDEFAULT、DEFAULTが全て終わってからLOWという順番で処理されていくようです。2つ目の引数はヘッダを見るとReserved for future useとなっていて今は何も意味しないので、0を入れておけば良いです。

シリアルキューはこの関数で作ります。

dispatch_queue_t
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);

一つ目の引数はキューに付けられるラベルで、サンプルを見る限り逆DNS記法が推奨されているようです。でも、同じラベルでふたつキューを作っても、同じキューが返されるという事は無く、別のキューが作られるようです。2つ目の引数はこれまたヘッダにUnusedなんて書かれていて、こちらはNULLでも入れておけば良いようです。

なお、Carbonに慣れ親しんでいる方はお分かりかと思いますが、関数名にcreateとついているのでdispatch_queue_createで作ったキューは使い終わったら解放しなければいけません。それを行うのはdispatch_releaseです。

dispatch_release(queue);

使うかどうか分かりませんが、リテインカウントをあげる事も出来ます。

dispatch_retain(queue);

これらretainとreleaseはキュー以外のオブジェクトにも使います。

Queueに処理を渡す

キューを取得できたら、ブロックを渡して処理をさせます。キューを使って処理を開始させる関数はいろいろありますが、まず基本的な2つを見てみます。こんなかんじで定義されています。

void
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
void
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

ayncは非同期でバラバラに、syncは同期で順番に、キューにブロックを処理をさせようとする関数です。と説明すると、キューにも同じような用途で2種類あるのに何で関数がまた同じように2種類分かれているんだと思ってしまうのですが、ちょっと役割が違います。

ブロックが処理されるスレッドを調べてみると、asyncは呼び出し元のスレッドとは別のスレッドでブロックを処理させ、syncは呼び出し元と同じスレッドで処理をさせるようです。

実際の挙動としては、グローバルキューにasyncでブロックを渡すと別スレッドで並列に実行されますが、syncで渡すと現在のスレッドをつかうのでそのまま直列に実行されます。シリアルキューにsyncで渡せばもちろんそのまま直列に実行ですが、asyncで渡すと入れていった順番に別スレッドで実行されます。

グローバルキュー+syncの組み合わせはありえないような気がするんですが、もしかしたら想像もつかない用途があるかもしれません。他にもディスパッチ用の関数はたくさんあるんで意味が無さそうな組み合わせはいろいろ出てくるかもしれません。

それと、ここでつかえるブロックは返り値も引数もなしのブロックです。

実行してみる

実際にディスパッチを使ってみます。ディスパッチを使うには<dispatch/dispatch.h>をincludeしないといけないのですが、Objective-Cで使う場合はFoundationに既に含まれているので何も気にしなくても使えます。

以下、サンプルのソースコードです。単純にディスパッチを試すためC言語だけで組んであります。新規プロジェクトをCommand Line ToolのTypeをCで作成して、main.cを以下のコードに差し替えてください。あっ、もちろんSnow LeopardのXcode3.2以降で実行してください。

#include <stdio.h>
#include <stdlib.h>
#include <dispatch/dispatch.h>
int main (int argc, const char * argv[]) {
    
    dispatch_queue_t gQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_async(gQueue, ^{
        
        dispatch_queue_t sQueue = dispatch_queue_create("jp.objective-audio.sample1", NULL);
        
        for (int i = 0; i < 10; i++) {
            dispatch_sync(sQueue, ^{
                printf("block %d\n", i);
            });
        }
        
        dispatch_release(sQueue);
        
        printf("finish\n");
        exit(0);
    });
    
    dispatch_main();
    
    return 0;
}

dispatch_asyncで非同期で実行するブロックの中でシリアルキューを作って、10回ほどdispatch_syncでブロックを実行させています。その処理が終わったら”finish”と表示してアプリ終了です。

ブロックの記述は、わざわざ宣言とかしなくても、こんなかんじで関数の中に直接書けちゃいます。

最後のdispatch_main()というのはメインスレッドを休止させるもので、これを使わないとdispatch_asyncでグローバルキューにブロックを実行させる前にアプリがreturn 0で終了してしまいます。NSApplicationMain()かCFRunLoopRun()が走っている場合には必要ありません、というかdispatch_main()を呼び出すと固まります。

キューの種類を入れ替えてみたり、syncとasyncを入れ替えてみたりすると、どんな感じで動作するか分かるかと思います。asyncのところをsyncにするとdispatch_main()が無くてもちゃんと全部実行されますし。逆にsyncのところをasyncにすると、ブロックの処理が別スレッドに行ってしまうので、実行される前に”finish”となります。

とりあえず今回は以上です。asyncやsync以外にもディスパッチでブロックを実行させる関数があるので、また次回見ていきます。

Grand Central Dispatch その1 Block

Grand Central Dispatchについて日本語で丁寧に詳しく説明してくれるサイトはないかなぁと待っていたのですが、待っている間にひととおり自分なりに調べ終えてしまったので、まとめて書いておきます。まちがいに気がついたらご指摘いただけるとうれしいです。

Grand Central Dispatch(以下ディスパッチと書きます。リファレンスとか関数名とかGrand Centralって全然ついていなくてDispatchだけですし。)とはなんぞやというと、Snow Leopardから導入された並列処理のAPIです。並列処理といえばLeopardではNSOperationというObjective-CのAPIが導入されましたが、ディスパッチはC言語のAPIとして用意されていて、システムに近い低レベルなところで使えるものです。

あれこれ試した印象で言うと、コアが2つしかないCPUのMacでたいした事の無い処理をなんでもかんでもディスパッチで並列にしても、ディスパッチのオーバーヘッドが大きくて逆に遅くなってしまうので、どこで使うかってのはよく考えた方が良さそうです。Mac Proみたいにコアがたくさんあると効果が大きいんだろうなぁと思います。

Blockの構文

ディスパッチでは一つ一つの処理をブロックという単位でキューに渡して処理を実行します。ブロックの代わりに関数を使う事も出来るのですが、あえてこのタイミングでC言語を拡張して用意しているくらいですので、ブロックを使うというのが正当な使い方だと思います。

ブロックは変数に入れておく事ができ、ブロック名をname1とname2とすると宣言はこうなります。

void (^name1)(void); //返り値なし、引数なし
int (^name2)(float, int); //返り値int、引数floatとint

名前の頭にキャレット(^)を付けます。で、これらへ代入をするのはこうなります。

name1 = ^{
    //ブロックの処理を記述
};
name2 = ^(float f, int i) {
    //ブロックの処理を記述
    return (int)f + i;
};

ブロック本体は、キャレット(^)、引数、その後にブレース({})で囲んで記述します。name1のように引数がvoidなら省略できます。なお、name2のように返り値があったり、引数が2つ以上のブロックはディスパッチでは使いません。

ブロックは関数と違って、メソッドや関数の中に置けます。関数のような感じで外にも置けます。

呼び出す時は関数と一緒です。

name1();
name2(3.0, 5); //返りは8

ブロック変数

ディスパッチでブロックを使うときには返り値が無いとなると、処理結果を得るためにはどうするかって事なんですが、そのひとつにブロック変数というのがあります。たとえば、以下のようなコード。

#import <Foundation/Foundation.h>
int y = 0;
int main (int argc, const char * argv[]) {
    
    int x = 0;
    
    void (^inc)(void) = ^{
        x++; //エラー
        y++;
    };
    
    inc();
    
    printf("x = %d / y = %d\n", x, y);
    
    return 0;
}

これはx++のところでエラーが出てコンパイルが通りません。

ブロック内では、グローバル変数には自由にアクセスできますが、関数内のブロック外にあるローカル変数は、そのままでは読み込めるだけで書き込みは出来ません。ディスパッチでブロックを処理するときには、呼び出した関数とは別のスレッドで処理される場合があるので(というかそれがディスパッチを使う目的)、関数が終わったら消えてしまうローカル変数には戻せないという事だと思います。

ローカル変数に書き込むには__blockという修飾子をつけます。こうすることで、その変数が使われるブロックがすべて終わるまで保持されるようです。

#import <Foundation/Foundation.h>
int y = 0;
int main (int argc, const char * argv[]) {
    
    __block int x = 0;
    
    void (^inc)(void) = ^{
        x++;
        y++;
    };
    
    inc();
    
    printf("x = %d / y = %d\n", x, y); // x = 1、y = 1
    
    return 0;
}

とりあえずブロックはこんなところで、次回は実際にディスパッチを使ってみます。

日本語ドキュメントに追加

今日アップルの日本語ドキュメント見たらOS3.0の新機能のものが追加されてました。前からあった2.X用のドキュメントも、そのうち3.0用に直される模様。

iPhone Dev Centerの日本語ドキュメント
※もちろん要アカウント

追加になったのは、

・Apple Push Notificationサービス プログラミングガイド
・Store Kit プログラミングガイド
・Game Kit プログラミングガイド

の3つ。

ちょうどGame Kitでやりたい事があって調べようと思っていたからうれしいです。

同じページに日本語訳されているAudio Session プログラミングガイドは、全iPhoneアプリ開発者必読だと思いますので、ついでにぜひ。ちょこっとでも音を鳴らすなら間違いなく知っていた方が良いです。

iPhone OS 3.0のオーディオ周りの変更点

OS3.0がでてからもう結構時間が経ってますが、オーディオ周りの挙動に変更があったところをまとめておきます。

ロック時に音が途切れない

AudioUnitのRemoteIOを使っている場合、AudioSessionのPreferredHardwareIOBufferDurationを何も設定せずデフォルト状態のままだと、以前と変わらずロックしていないときは1024フレーム、ロック中は4096フレームと切り替わってしまいますが、一度値を設定しておくと、ロックしても設定した値そのままで切り替わらなくなったようです。

RemoteIOのデフォルトフォーマットが変更

OS2.2.1以前のRemoteIOのデフォルトのフォーマットは8.24の固定小数点でしたが、OS3.0からは16bitの整数に変わっているようです。ただし、MultiChannelMixerなどはあいかわらず8.24の様ですので、組み合わせて使う場合は自分で設定して合わせる必要があり、注意が必要です。

Objective-CでAudioSession

いままでのC言語でのAPIだけでなく、AVFoundationにAVAudioSessionというObjective-CのAPIも加わり、より簡単にAudioSessionが使えるようになったようです。でもなぜかIOBufferDurationが設定できないんですよねぇ。使い方が間違ってるんでしょうか。

ExtAudioFileでMP3やAACなどの圧縮ファイルが使える

AVAudioPlayerでは以前から読み込めていましたが(AudioQueueでもでしたっけ?)、OS3.0からはExtAudioFileでも読み込めるようになったようです。個人的に録音に興味が無いので試していませんが、AACとかは書き込みも出来るかも。

Bluetooth経由で音の再生が可能に

BluetoothのA2DPに対応し、ワイヤレスで音の再生が可能になりました。が、手元にあったBluetoothレシーバ(以前使っていたTOSHIBAの携帯用のやつ)で試しに聴いてみたところ、音質は別にいいのですが、レイテンシーがひどすぎて、Touch the Waveとか楽器アプリで使うのはありえないという印象です。

ミュージックライブラリにアクセスが可能に

と聞いて期待したんですが、一瞬で打ち砕かれました。あくまでメタ情報の取得と、用意されているプレイヤーでの再生が出来るだけで、オーディオファイルを直接読み込む事は出来ません。なんかよく、ミュージックライブラリに直接アクセスなんて書いてありますが、「間接的」って表現のほうが合ってると思います。あ、でも、僕の認識が間違っていて、実はオーディオファイルの読み込みができるなんて情報があればぜひとも教えていただきたいです。

AudioUnit MultiChannelMixerを使う

いままでiPhoneのAudioUnitはRemoteIOしか使っていなくて、新規アプリ開発に向けてMixerのUnitを使ってみようと思ったら、なかなかうまくいかなくてハマってしまったのでメモしておきます。

以下が、MultiChannelMixerの初期化のコードになります。

- (void)setupMixerUnit:(AudioUnit *)fMixerUnit busCount:(NSUInteger)fBusCount
{
    AudioComponent component;
    AudioComponentDescription description;
    description.componentType = kAudioUnitType_Mixer;
    description.componentSubType = kAudioUnitSubType_MultiChannelMixer;
    description.componentManufacturer = kAudioUnitManufacturer_Apple;
    description.componentFlags = 0;
    description.componentFlagsMask = 0;
    AURenderCallbackStruct callback;
    callback.inputProc = MixerCallback;
    callback.inputProcRefCon = self;
    
    AudioStreamBasicDescription format;
    format.mSampleRate = 44100;
    format.mFormatID = kAudioFormatLinearPCM;
    format.mFormatFlags = kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger;
    format.mBitsPerChannel = 16;
    format.mChannelsPerFrame = 2;
    format.mFramesPerPacket = 1;
    format.mBytesPerFrame = format.mBitsPerChannel / 8 * format.mChannelsPerFrame;
    format.mBytesPerPacket = format.mBytesPerFrame * format.mFramesPerPacket;
    
    component = AudioComponentFindNext(NULL, &description);
    AudioComponentInstanceNew(component, fMixerUnit);
    
    UInt32 elementCount = fBusCount;
    AudioUnitSetProperty(*fMixerUnit, kAudioUnitProperty_ElementCount, kAudioUnitScope_Input, 0, &elementCount, sizeof(UInt32));
    
    for (AudioUnitElement i = 0; i < elementCount; i++) {
        AudioUnitSetProperty(*fMixerUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, i, &callback, sizeof(AURenderCallbackStruct));
        AudioUnitSetProperty(*fMixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, i, &format, sizeof(AudioStreamBasicDescription));
    }
    
    AudioUnitInitialize(*fMixerUnit);
    
    //ボリュームを変更してみる
    for (AudioUnitElement i = 0; i < elementCount; i++) {
        AudioUnitParameterValue volume = 0.8;
        AudioUnitSetParameter(*fMixerUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, i, volume, 0);
    }
}

設定の順番が重要です。
1 - MultiChannelMixerのAudioUnitをインスタンス化
2 - インプットバスを追加
3 - インプットごとにコールバックやフォーマットをセット
4 - AudioUnitを初期化
5 - 初期化後に、プロパティの取得やパラメータの取得・設定

あとは、AUGraphでつなぐなり、RemoteIOのコールバック内でAudioUnitRenderするなりって感じですね。

ミキサーユニットをMacで使う場合のコードが、アップルのQ&Aのページにのっていたりするんですが、そこにはインプットバスを設定するプロパティがElementCountじゃなくてBusCountになっていて、iPhoneでは使えないのがハマった原因でした。よくヘッダを見ると、MacではkAudioUnitProperty_BusCount = kAudioUnitProperty_ElementCountって定義されてます。

ミキサーユニットにはオーディオフォーマットのコンバーターが組み込まれているので便利ですね。今回のコードだと、インプットに16bit・44.1kHz・ステレオのデータをそのまま渡せば8.24の固定小数点に勝手に変換してくれます。ただOS3.0だとRemoteIOのデフォルトのフォーマットが8.24ではなくなっているので、アウト側のフォーマットを設定して合わせる必要があります。

ちなみに、オーディオのデータをコールバックで渡すときは、for文で回してコピーするのではなく、出来るだけmemcpyとかで丸ごとコピーできるように、うまくフォーマットを指定した方が良いです。全然パフォーマンスが違います。

時差

iTunes Connect上でのリリース日を、アプリのアップデートが公開された日に設定すると、AppStore上のリリース日もアップデートされた日に更新されるというのは開発者のみなさんなら周知の事実だと思います。ですが、今日2月14日にReady for saleが来た!と思って2月14日に設定したら、アメリカの人からUSのストアにアプリが無いよってメールが来ました。

日本やその他いくつかの国を見てもちゃんとあるのに、USに無いのはどうしてだろうと考えてみたら、アメリカはまだ2月13日だったんですね。自分の場合、審査がおわってReady for saleがくるのは日本時間の午前中なので、一つ前の日に設定するように気をつけなくちゃいけないなと思いました。

ところで、エリカさんの「iPhone デベロッパーズ クックブック」が届きました。先行発売されているのを立ち読みしたときから思っていたのですが、オーディオに関してはAudioUnitもOpenALもまったく触れられていないのが残念でした。いや、触れられていないどころか、全く存在しないような書き方です。逆に、Celestialっていう非公式のAPIを説明していたりして、かなりハックよりの内容ですね。

それより、AppleのiPhone用日本語ドキュメントにCore AudioとCore Animationに関する4つが追加されているようです。少なくともオーディオに関してはエリカ本は決して参考にせず、公式ドキュメントを参考にすることをお進めします。

AudioSession その3 IOバッファサイズ

今回は、iPhoneオーディオのIOバッファサイズについてです。とりあえず、前回までのコードに、以下のようなsetIOBufferSizeメソッドを追加して、initにその呼び出しを加えてください。

- (void)setIOBufferSize
{
    Float64 sampleRate;
    UInt32 size = sizeof(sampleRate);
    AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareSampleRate, &size, &sampleRate);
    
    Float32 bufferSize = 256./sampleRate;
    size = sizeof(bufferSize);
    AudioSessionSetProperty(kAudioSessionProperty_PreferredHardwareIOBufferDuration, size, &bufferSize);
}
- (id) init
{
    self = [super init];
    if (self != nil) {
        [self setupAudioSession];
        [self setIOBufferSize];
        [self setupOutputUnit];
    }
    return self;
}

前回までのコードのコールバック内は、フレーム数に応じてサイン波の周波数がかわるようになっていましたので、変更を加えないときより高い音が再生されると思います。bufferSizeの256の所がバッファサイズのフレーム数ですので、そこを変更すれば任意のフレーム数に設定できます。コールバック内にprintfとかでinNumberFramesをログに出すようにすれば、ちゃんと変わっているのが分かると思います。

IOバッファサイズは秒数で指定しますので、今回のコードではハードウェアのサンプリング周波数を取得して、目的のフレーム数を割って値を求めています。プロパティの名前にPreferredってついているので指定した秒数の近いところに設定されるという感じでしょうか。Appleのサンプルでは、256に変更するときにちょっと少なめの0.005秒を指定していました。

ちなみにAudioSessionのプロパティのゲットは、AudioSessionがアクティブになっていないと出来ないようです。セットはいつでも大丈夫みたいです。Appleのサンプルをみると、プロパティをセットして、AudioSessionをアクティブにして、プロパティをゲットするというのがセオリーっぽいです。

IOバッファサイズはデフォルトの状態では1024フレームです。ロックしてスリープすると強制的に4096フレームになります。ただしOS 3.0では、今回のサンプルのようにAudioSessionSetPropertyでIOバッファサイズを一度設定すると、ロックしてもIOバッファサイズが変わらなくなったようです。また、カテゴリをPlayAndRecordなどにするだけでも変わりません。

AudioSession その2 AudioCategory

iPhoneのアプリでオーディオを再生・録音する場合に、iPodの音楽を鳴らしたままミックスして出したり、ロックした時にも音を出せるようにするには、AudioSessionSetProperty関数を使ってAudioCategoryというのを設定します。

AudioSessionSetPropertyはこんな感じで宣言されています。

extern OSStatus
AudioSessionSetProperty(AudioSessionPropertyID inID,
                        UInt32                 inDataSize,
                        const void             *inData)

AudioSessionSetPropertyの第一引数にはAudioSessionPropertyID、第二引数はプロパティのサイズ、第三引数には設定する値をポインタで渡します。CoreAudioではおなじみの構成です。

AudioSessionSetPropertyは色々とオーディオ関係の設定できますが、とりあえず今回はAudioCategoryの設定だけをやってみます。前回のコードのsetupAudioSessionメソッドを以下のように書き換えてください。

- (void)setupAudioSession
{
    AudioSessionInitialize(NULL, NULL, InterruptionListener, &outputUnit);
    
    UInt32 category = kAudioSessionCategory_AmbientSound;
    AudioSessionSetProperty(kAudioSessionProperty_AudioCategory, sizeof(category), &category);
    
    AudioSessionSetActive(true);
}

AudioCategoryのプロパティの型はUInt32と決まっていますので、その変数を作ってポインタで渡すという感じになります。プロパティの型の種類はものによってバラバラですが、ヘッダを辿るとコメントに書いてあります。

これで実行すると、iPodの音楽を鳴らした状態でアプリを起動しても音楽が止まらず、サイン波がミックスして再生されると思います。

プロパティのkAudioSessionCategory_AmbientSoundの部分をkAudioSessionCategory_MediaPlaybackに変えてみてアプリを実行すると、iPodの音楽はフェードアウトしてしまいますが、ロックしてスリープするときにサイン波は鳴りっぱなしになります(でも、このコードではピッチは下がってしまいますが)。

カテゴリの動作一覧はAudioSession Programing Guide内にありまして、日本語に置き換えてみるとこんな感じでしょうか。

PropertyID サイレントスイッチの効果(およびスクリーンロック時の消音) 他アプリのオーディオ再生 使用例
UserInterfaceSoundEffects ある する ユーザーインターフェースなど
AmbientSound ある する ゲーム効果音やバーチャル楽器など。iPodの音楽を再生しながら鳴らすもの
SoloAmbientSound ある しない デフォルト。ゲームサウンドなど。他アプリの音を再生しないもの
MediaPlayback ない しない オーディオファイル再生
LiveAudio ない しない ライブパフォーマンス系。バーチャル楽器など
RecordAudio ない しない 録音のみ
PlayAndRecord ない しない 再生と録音

何もカテゴリーを設定しないとSoloAmbientSoundになってしまいます。それと残念ながら、iPodの音楽を鳴らしながらスクリーンロック中に音を出すってことは出来ないみたいです。

※2009/10/7追記
OS3.0以降、MediaPlaybackなどのカテゴリでも、kAudioSessionProperty_OverrideCategoryMixWithOthersを使う事でiPodの音楽を流しながら音を出す事が出来るようになっています。また、LiveAudioとUserInterfaceSoundEffectsはDeprecatedになってます。

また、ちょっと分からない部分なんですが、RecordAudioにしていたからといって再生が出来なくなるという訳ではなかったりするので、結局のところAudioCategoryの設定は3パターンしかないようです。もし今の段階で違いが無かったとしても、今後のOSのバージョンアップで何か変化があるかもしれませんけど。

※追記 2009/1/25
AudioCategoryの種類によってルートチェンジのときに違いがあるみたいですね。それはまたちゃんと調べたらルートチェンジで1つエントリを書きます。

といったところでAudioCategoryはこの辺で。次回は、ずっと悩まされ続けてきたIOバッファあたりについて書いてみようかと思います。

AudioSession その1 Interruption

以前、RemoteIOでのオーディオ再生というエントリでRemoteIOでオーディオ再生をする方法を書きましたが、それだけだとiPhoneのアプリとしては設定が不足です。オーディオを扱うアプリでは、AudioSessionというAPIを使って、オーディオに関する情報の取得や設定を行います。SystemSoundServiceなんかのお手軽再生系では無用の様ですが、AudioUnitを使う場合には必須といえるでしょう。

ちなみに、AudioSession系の関数はシミュレータではエラーが出て一切受け付けてくれず、実機にしか効果が及びません。また、デフォルトのオーディオIOのバッファサイズは実機では1024サンプルですが、シミュレータではMacのオーディオインターフェースの設定そのままですので、オーディオの動作の検証は必ず実機で行いましょう。

※OS 3.0にはAVFoundationにAVAudioSessionというObjective-CでAudioSessionが扱えるフレームワークが追加されましたが、動作に?な部分がありますので、あえてC言語のフレームワークの使い方そのままで記述しておきます。

AudioSessionで出来る事はいろいろありますが、いくつかあげてみると

・iPodのミュージックの音を鳴らしたままにするか、止めてしまうかの設定
・スリープしたときにアプリの音声を停止するかの設定
・通話などが割り込んできて自分のアプリの音声が停止する時の通知を受ける
・ヘッドホンがささったりなど、オーディオのルーティングが変わったときの通知を受ける
・オーディオハードウェアの設定の変更や、設定されている情報の取得

といったところでしょうか。必ずやっておかなければいけないのは、外部から割り込みが入ってオーディオが停止する時と、その後復活する時の処理です。これを適切にやっておかないと、アプリに戻ってきたときに音声が再生できなくなります。

では、以前のエントリRemoteIOでのオーディオ再生のYKAudioOutput.mのコードに以下の関数とメソッドを追加してください。initは修正になります。

void InterruptionListener(void *inUserData,
                          UInt32 inInterruption)
{
    AudioUnit *remoteIO = (AudioUnit*)inUserData;
    if (inInterruption == kAudioSessionEndInterruption) {
        AudioSessionSetActive(true);
        AudioOutputUnitStart(*remoteIO);
        printf("EndInterruption\n");
    }
    
    if (inInterruption == kAudioSessionBeginInterruption) {
        AudioOutputUnitStop(*remoteIO);
        printf("BeginInterruption\n");
    }
}
- (void)setupAudioSession
{
    AudioSessionInitialize(NULL, NULL, InterruptionListener, &outputUnit);
    AudioSessionSetActive(true);
}
- (id) init
{
    self = [super init];
    if (self != nil) {
        [self setupAudioSession];
        [self setupOutputUnit];
    }
    return self;
}

setupAudioSessionメソッドでAudioSessionの設定を行っています。AudioSessionInitialize関数ではInterruptionListnerという関数を登録して初期化しています。以下がAudioToolbox/AudioServices.hの中にある宣言です。

extern OSStatus
AudioSessionInitialize(CFRunLoopRef                      inRunLoop,
                       CFStringRef                       inRunLoopMode,
                       AudioSessionInterruptionListener  inInterruptionListener,
                       void                              *inClientData)

1つ目と2つ目のRunLoopのところはそれぞれNULLを入れておけば、デフォルトで登録されます。4つ目のinClientDataは自由に使えるところなので、今回のコードではAudioUnitのポインタを渡してAudioUnitのスタートとストップを出来るようにしています。

AudioSessionInitializeで初期化が出来たら、AudioSessionSetActive(true)を呼ぶとAudioSessionが開始されます。

AudioSessionが開始されるとInterruptionListener関数がオーディオの割り込み時に呼ばれまして、inInterruptionにkAudioSessionBeginInterruptionがくれば割り込み開始(アプリのオーディオが停止)、kAudioSessionEndInterruptionなら割り込み終了(アプリのオーディオが復活)となりますので、それぞれに応じて処理するようにします。

割り込みが開始された場合にはAudioUnitをストップするだけで良いのですが、終了のときにはAudioSessionSetActiveを先に読んでおかないと、たぶんAudioUnitStartでアプリが落ちます。

※OS 3.0ではアプリが落ちたりする事は無いようですが、AudioSessionSetActiveを呼ばないとAudioUnitが再開しないのは変わらないようです。

最低限これだけやっておけば良いと思いますが、今回のコードだと、アプリ起動時にiPodのミュージックが停止したり、ロック時に音が止まったりとデフォルトの状態のままですので、その設定の仕方は次回やります。