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;
}

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

BigStopWatch v2.3 公開しました

BigStopWatchのver2.3が公開されました。

新機能は、バッテリー残量の表示です。インストール時には機能がオフになっていますので、必要な方は設定でオンにしてください。バッテリー残量表示の機能を付け加えた事で、OS 3.0のみの対応になっています。

あと、ラップ/スプリット機能・カウントダウン・積算の各機能をオン・オフ出来る設定を追加しました。これら3つ全てをオフにすると、このアプリを最初に作ろうとしていた理想の状態になったりします。もともとは積算も付けたくなかったんですが、さすがにそれはシンプルにしすぎだろうと思って、苦肉の策で小さい円の中にRESETを浮かび上がらせるということにしましたので。

ちなみに、アニメーション周りをまたちょっと変更していまして、多少パフォーマンスが落ちています。iPhone 3Gとかだとカクッとする事が多くなってしまったかもしれません。そのかわり内部的にはアニメーションを自由にいじくれる様になってるんですけど…。

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

今日アップルの日本語ドキュメント見たら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とか楽器アプリで使うのはありえないという印象です。

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

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

OS3.0対応状況

Touch the WaveのOS3.0対応バージョンはAppStoreに提出してはいるのですが、いまだ公開されていません。他の様子も見ていると、最近はかなり審査に時間がかかっているみたいですので、しばらくおまちください。今月中には出てくれますかねぇ…。

Touch the Waveを3.0で動かした場合のどんな不具合が出るかというと、ダウンロード画面・プレイリスト画面・プレイヤー画面を行ったり来たりしていると、そのうち移動できなくなり、それでも移動しようとすると落ちてしまうという感じです。ViewController周りの実装をいいかげんにしていたツケが回ってきたというところでしょうか。

ということで、3.0に対応するだけバージョンは審査待ちですが、そんなこんなしているうちに、もうちょっといろいろ変更してるバージョンは大体できあがってます。まあ、もうちょっとじっくりと動作を検証して7月あたりに公開しようかなぁと思っています。

BigStopWatchは今のバージョンのままで大丈夫だと思います。こっちはOpenGL ESがほとんどで、問題出てきそうなCocoaTouchなAPIはほとんど使っていませんので。

3.0対応

iPhone OS 3.0公開のタイミングに合わせてTouch the Waveをいろいろといじくり回していたのですが、予想していた7月より1ヶ月も早かったので、とりあえず3.0に対応しただけバージョンをApp Storeに提出しておきました。

OS3.0に対応しただけと言いつつ新機能もあるのですが、3.0からの新機能に関わる部分で、まだ書いていいかどうかわからないので、とりあえず公開してからのお楽しみという事にしておきます。基本的にはView周りの挙動が変わって落ちたりしていたので、その修正です。

他のアプリの3.0対応の駆け込みアップデートが多かったりして、普段よりも時間がかかったりするんでしょうかねぇ。今までの感じだと16日くらいにはReady for saleになると思うんですけど。

あと、提出したアプリはOS2.2.1以前は切り捨てているのですが、17日より早くレビューが終わったらOS3.0公開前にApp Storeに並んじゃうんでしょうか。ちょっと気になります。

renew

iPhoneのDeveroper Programの更新の案内が来ていたので、さっさと更新しました。たしか60日以内にって事が書いてあったような気がしたので、期限が切れる2ヶ月前になったら順次メールが届くっていう感じなんでしょうか。

去年の7月にアクティベートしようとしたときにはAppleに電話しなくちゃいけなかったりとめんどくさかったのに比べて、さすがに更新なだけにあっさり一発でアクティベートできました。でも、アクティベーションコードが来たのが購入してから1日以上とあいかわらず遅かったですね。

ところで、最近はTouch the WaveのOS3.0対応をのんびり進めております。ただ3.0に対応するだけならちょっとした変更でよいのですが、このタイミングに合わせていろいろとやっているので、完成するのはまだまだ1ヶ月以上かかりそうです。プレイヤー画面をフルOpenGLで作り直したり、3.0の新機能を使ってみたり。3.0の正式版が出るのが早かったら間に合わないかもしれません。

あと、そんな感じなんで当分このページもあんまり更新できないと思います。

BigStopWatch v2.2 公開

予告通り、BigStopWatchのv2.2が公開されました。

主な変更点は前回のエントリーに書いた通り、
・Normal modeの廃止

・カウントダウンの秒数を、5、10、15、20、25、30秒の中から選択できる
・オリエンテーションの種類をふやした
といったところです。

オリエンテーションの設定は、アップデートするとクリアされてAuto Rotationになりますので、ぐるぐるまわして遊んだ後は、お好みの位置に固定した方が良いと思います。

あと、波紋のようにタップ位置が表示されたり、タップした文字がボワワーンとなったりしてます。目盛りの線も前よりきれいになってます。回転している線がギザギザせずアンチエイリアシングなのは、一応、最初からのこだわりどころです。

BigStopWatch v2.2

BigStopWatchのv2.2を今日提出しました。致命的な不具合とかを見つけてしまわなければ、来週にはAppStoreにアップされると思います。

変更点は、

・Normal modeの廃止
・カウントダウンの秒数を、5、10、15、20、25、30秒の中から選択できる
・オリエンテーションの種類をふやした

という感じです。その他にも細かいアニメーションがついたりしてます。結構、Advanced modeへの変更の仕方がわからないという声が多かったので、2つモードを作るのもやっぱりめんどくさいですし、Advanced modeだけにしちゃいました。

今回のバージョンアップは、機能的にはそんなに大幅な変更ではないのですが、OpenGLでCoreAnimation的に描画するような仕組みを1から組み直してまして、90%くらいごっそりと中身は変わっています。

その成果の一部がオートローテーションで、こんな感じの動きになっています。Safariなんかのローテーションと違って、常に60フレーム弱のスムーズな描画です。

ちなみに、動いている最中もちゃんとRESET-LAPボタンそのものの位置をタップできます(たしかCoreAnimationでは出来なかったはず)。最初はいろいろ難しく考えてなかなか実現できなかったんですけど、OpenGLの描画と同じジオメトリ変換を加えたCGAffineTransformを作って、CGAffineTransformInvertでタッチしたポイントを逆変換すれば良いだけの話でした。

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とかで丸ごとコピーできるように、うまくフォーマットを指定した方が良いです。全然パフォーマンスが違います。