2016年7月20日
オーディオアプリ開発でありがちな4つの間違い
(2016-6-28)by Michael Tyson
本記事は、原著者の許諾のもとに翻訳・掲載しております。
ここで論じているのは、オーディオアプリの開発者が陥りがちな 4つの間違い 、 より良く開発する方法 、 問題個所の発見方法 です。主に開発者向けの内容ですが、開発者以外の方にも知っておいてもらいたいと思います。ここでは、開発者向けの診断ツールである Realtime Watchdog を紹介し、 人気のあるオーディオライブラリの調査結果 を提示します。
オーディオアプリの開発はとてつもなく楽しいです。やりがいを感じるし、創造力を発揮できる範囲が大きく広がり、ひとたび開発が終われば、 誰かがクリエイティブなツールとして使ってくれるのです! こんな分野は多くないし、この領域で働けるなんて非常に幸運だと自分でも思っています。
しかし、仕事でオーディオアプリを扱う時には深く考えなければならない部分もあります。オーディオアプリの開発者としてユーザに対する責任があるのです。大前提として、ユーザを公共の場で困らせてはなりません。ミキサーから耳をつんざくような中域の音が出てきたら、DJはありがたがらないでしょう(でも、これはクラブ次第かも。喜ばれたりして?)。あるいは、伴奏に使うドラムマシンが常軌を逸した音を出せば奏者は演奏を投げ出すでしょう。同じことはプライベートな場合でも起こります。ただ収録しただけのはずなのに、録音した音の途中に巨大なクリック音が出てきたりすると、ユーザは開発者を呪うでしょう。
今や世の中はAudiobus/IAA以降の世界です。ユーザのセットアップは多岐のアプリにわたり、1つ悪さをするのがあると全てが台無しになる上に、問題がどこから生じたのかも分かりません。
もしLoopy HDが途中で不具合を起こしたらと、想像してみてください。
「The Tonight Show」のオーディオ担当のエンジニアが私に話してくれたところでは、上記のコーナーでLoopyを選んだ主な理由は「エンジニア自身が長年Loopyのユーザで、Loopyがいつも安定して信頼できるものだから」ということでした。
典型的なセッション中にアプリが不具合を起こす確率がたった1万分の1だとしても、もしアプリが1日に1万セッションを行えば1日に1回の不具合が出ます。これは珍しいことではありません。1日に2万セッションなら不具合が2回です。間違いなく、ほとんどの音楽アプリはこれよりも高い確率で不具合を起こしています。
たった1回の不具合でライブ演奏中のミュージシャンは自分の全セットアップを完全に信じられなくなります。 トラブルシューティングできないセットアップの1つがアプリです。その理由は、それが不透明なシステムだからです。それゆえ、使っている個々のアプリ全部がトラブルシューティングの対象になってしまいます。全アプリの使用を止めなければなりません。全ミュージシャン仲間に向けて、Facebookで怒りの投稿となってもおかしくなく、この記事を読んでいる人々の望みとは正反対のことが起きてしまうのです。
つまり、私がこの記事でフォーカスしたいのは、私たちオーディオアプリの開発者が負っているこの種の注意義務のことです。 なぜなら、音楽アプリは安定して信頼できるものでなければならないからです。いつ、いかなる時でも。
先にお伝えしておきます。私は完璧なソフトウェア開発者ではありません。全くそんなことはありません。間違いもあるし、できる限りのことをしてなくそうとしても、バグがひそかに入り込んでしまいます。ですから、この記事に傲慢なところがあっても、上に座って見下しているというよりも、脇に立ったまま指摘しているのだと考えてください。
Audiobus と The Amazing Audio Engine の仕事を通じて、数多くの開発者たちのコードを詳しく知りました。それは、この分野に来た新人のコード、有名企業で働く開発者のコード、双方が含まれています。
残念なことですが、私が思っていた以上に、私が見た 両方の グループのコードのどちらもがオーディオを扱う際の基本的なルールを1つ以上破っています。実のところ、 常に 見かけるのです。私は頭がおかしくなりそうです。
さらに驚いたことに、そこから生まれた注目のライブラリの多数もルールを破っていました。 この記事の末尾に、人気のあるオーディオエンジンのライブラリに対する簡単な調査結果を載せましたので見てください。 最近Loopyの不具合を大急ぎで修正しなければなりませんでした。結局のところ、Loopyにサードパーティのライブラリ(オーディオエンジンではない何ものか)が余計なことをやって不具合が引き起こされていました。
では先ほど言った基本ルールは何でしょう? 4つのわかりやすいルールはこれです
- オーディオのスレッドではロックを保持しないこと。
pthread_mutex_lock
や@synchronized
など。 - オーディオのスレッドではObjective-CやSwiftを使わないこと。
[myInstance doAThing]
やmyInstance.something
など。 - オーディオのスレッドにメモリを割り当てないこと。
malloc()
やnew Abcd
や[MyClass alloc]
など。 - オーディオのスレッドではファイルやネットワーク入出力を扱わないこと。
read
、write
あるいはsendto
など。
これらは横暴なルールに見えるかもしれません。しかし、この4つのどれかを破ると、苦労して作ったオーディオのコードが、予想だにしない時にバチバチと音を立てたりクリックを発生してしまうような、めちゃくちゃな状態になります。アプリの中にサードパーティのライブラリがあり、それがルールを破っていても同じことが起き、さらに悪い状態になります。
アプリのオーディオエンジンが不具合を起こしうる原因は他にもたくさんあることに留意してください。例えば、ロジックのエラー、適切ではないルーチン、あるいは、単にデバイスの能力を超えた要求などです。しかし、上記4つは見つけて対応するのが簡単です。私は 常に この4つを見ています。
これらのルールを破ると、耳をつんざくような破壊音がとどろいたり、わずかなクリック音が出たりしてしまいます。気付かないかもしれませんが、ぜひ避けてください。
では、何がこういう不具合を引き起こすのでしょうか?
基本的には、実行時のタイムアウトです。以下、どのようにして起きるかの説明です。
あらゆるオーディオアプリの実行時には最低2つのスレッドが実行されます。メインのスレッドとオーディオのスレッドです。しばしば他のものも動きます。たとえば、ネットワークのスレッドやユーザインターフェースをレンダリングするスレッドです。
限られたリソースであるCPUは、これらのスレッドと現在動作中の他のアプリ全てとの間で共有されます。
オーディオのリアルタイムのレンダリングはかなり過酷です。システムは n 秒ごとに n 秒のオーディオデータをオーディオハードウェアに送り出さなければなりません。そうしなければ、バッファが枯渇し、ユーザは不快な雑音や異常音を聞くことになります。再生中の音から無音への移行は難しいのです。
オーディオスレッドは一定の間隔で処理され、たった数ミリ秒以下という非常に厳しい時間制約の中でタスクを完了させなければなりません。 リアルタイム スレッドなので、特定の権限が与えられます。UI(上記では青色のスレッド)で何かが起こったり、ネットワーク操作(オレンジ色のスレッド)が実行されていたりしても、オーディオのレンダリングが優先されます。CPUはオーディオスレッドを処理するために 全てを中断します。 最優先されるのは、それが必要だからです。
ここまではいいですか? 次から厄介になります。
現在使用されている一般的なオペレーティングシステムはどれも本当の”リアルタイム”OSではありません。”最善の努力”をするアプローチを取っているので、リアルタイムに近づけるよう努力はしているのでしょうが、確かな保証はされていません。つまり、全くどうすることも出来ない何かが起こり、オーディオスレッドが中断され、タイムアウトしてしまう可能性があるということです。
ですから、私たちが目指すのは、うまくいかない場合のリスクを最小限に抑えることです。
オーディオスレッド上で実行中のコード内で前述のルールの1つを破ると、厄介なことが起こります。メインスレッドと共有のデータ構造を使用しているコードがあるとしましょう。それは再生する音符のリストかもしれません。そして恐らく、ユーザのキー操作に応じて、そのリストに対し、音符の追加や削除が必要になるでしょう。
// Define some types
struct Note {
int noteId;
float frequency;
float velocity;
uint64_t startTime;
};
struct NoteList {
int noteCount;
struct Note notes[1]; // noteCount-1 Notes follow
};
struct NoteList * __noteList;
...
// Functions to add and remove notes
- (int)addNoteWithFrequency:(float)frequency velocity:(float)velocity atTime:(uint64_t)startTime;
- (void)removeNoteWithId:(int)noteId;
これで、リストを取得し、リスト内の音符に基づいた音を生成するオーディオレンダリングのコードが出来ました。しかし、私たちはメインスレッドとオーディオスレッドが共有するリソースを使って作業しています。いつでも中断されたり、同時に実行されることすらあったりするので、オーディオスレッドがデータを読み込んでいる最中にメインスレッドによって編集され、クラッシュやデータの破損が生じてしまうといった状況に陥る可能性があります。
こういった並行性の問題に対処するため、通常はロック(別名mutex、Mutual Exclusionオブジェクト)を使用します。これにより、一度に1つだけスレッドが処理されます。共有されたデータ構造とやり取りする際は、既にロックされているかどうかを確認します。ロックされている場合、ロックが解除されるまで待ちます。解除されたら、自スレッドのためにロックし、処理が終わったら、ロックを解除します。そうすれば、同時にアクセスするのを避けられます。
先ほどの例に続けて、こことリストを操作する関数の両方で、mutexを使って自スレッドを保護していきましょう。
pthread_mutex_t __noteListMutex;
void MyAudioRenderFunction() {
// Lock it up
pthread_mutex_lock(__noteListMutex);
// Make noise
for ( int i=0; i<__noteList->noteCount; i++ ) {
ProduceAudioForNote(&__noteList->notes[i]);
}
// Okay, we're done, unlock
pthread_mutex_unlock(__noteListMutex);
}
出来ましたね。さあ、ここからです。
メインスレッドによってリストが更新されている最中に、オーディオスレッドで pthread_mutex_lock
に達すると、どうなるでしょうか。CPUはオーディオスレッドを ブロック し、そのオーディオスレッドを放置して、ブロックされていない別のスレッドを優先します。メインスレッドでリストの更新を完了するのに時間がかかりすぎると…
そうです。タイムアウトしてしまい、オーディオシステムに不具合が発生しました。以下のように聞こえます。
http://atastypixel.com/blog/wp-content/uploads/2016/06/Glitch.ogg
それでは、リストを更新するコードを超高速で実行する何かと入れ替えてみたらどうでしょうか。短時間ロックを維持するだけであれば、大丈夫ですよね?
いいえ。とんでもありません。長時間ロックを維持することさえなければ大丈夫だと考える開発者もいますが、間違っています。理由は以下の通りです。
上記の図にあった黄色のスレッドを覚えていますか? それがメインスレッドより少し優先度が高いとしましょう。恐らくアプリはMIDI作業を行い、タイムクリティカルなオフライン処理やネットワーク通信を行っているでしょう。これら全てがより高い優先度を必要とするかもしれません。
マルチスレッドに関する問題は、手に負えないレベルです。スケジューラ ―CPUの注意をそらす不可解なケダモノ― が、どこででもスレッドを中断させ、CPU時間をより多くの時間が必要な他のスレッドに渡してしまいます。そして、それがたった1つのプロセス内で起こるのです。さらに、スケジューラは、動作中の他のアプリのスレッドをCPUに処理させる必要もあります。スケジューラは、忙しいやつなのです。
そのため、こんなことが起こります。
メインではないスレッド(黄)がメインのスレッド(青)よりも優先度が高いため、スケジューラはCPU時間をメインスレッドから奪います。そのメインスレッドを、さらにオーディオスレッドが待っています。これが 優先順位逆転 と言われる現象です。
メインスレッドをロックしている時間を最小限にすることによって、この発生確率をいくらか下げていますが、完全に除去しているわけではありません。
そして、それは、単に十分ではないと言うべきでしょう。
そう、これは、私たちや小さなテスタグループにとっては今のところ適度に動くかもしれません。しかし、アプリは、ユーザベースで1日に数千セッションを行っている可能性があります。AudiobusやIAAのマルチアプリケーション環境と組み合わせれば、リスクは増えます。処理数が増加するためです。もし、典型的なセッション中に不具合が発生する確率がたった1万分の1だとしても、アプリが1日に1万セッションを行なえば、毎日ユーザの誰かのアプリに不具合が起こるでしょう。
だから、ロックしてはいけないのです。Objective-CやSwiftの何が問題なのでしょう?
Objective-CやSwiftを使ってオーディオをレンダリングすると非常に便利かもしれません。オブジェクトを渡して、継承、多相性などが可能になるからです。サードパーティのオーディオライブラリの多くがそうしています。しかし、何が問題なのだと思いますか?
Objective-CとSwiftには、通常の処理の一部にロックがあるのです。
Objective-Cのメッセージ送信システム、つまり、Objective-Cメソッドの呼び出しの裏には、ロックの保持を含む多くの処理を行うコードがあります。信じていませんね? ご自分でよく見てください。ソースコードは、opensource.apple.comにあります。 objc_msgSend
が __class_lookupMethodAndLoadCache3
を呼び出し、これが lookUpImpOrForward
を、これが、そう、 lock(runtimeLock)
を呼び出し、これはグローバルな rwlock_t
で、あと、 lock(cacheUpdateLock)
ともう1つを呼び出します。
ところで、ドットシンタックスを使ってプロパティにアクセスすると( myInstance.property
)、Objective-Cメソッドコールとみなされます。従って、この方法も使ってはいけません。
実際、ARCがObjective-CやSwiftのオブジェクトを保持するようにはできません。ご想像の通りでしょうが、保持メカニズムもロックするからです。これを確認してください。 sidetable_retain
が table.trylock()
を呼び出し、もしこの呼び出しに失敗すると、 table.lock()
を呼び出す sidetable_retain_slow
を呼び出します。
メモリ割り当てではどうなる?
malloc
の類についてです。これは、作業のためのメモリブロックを割り当てる関数ですが、実行時間が無制限という点に問題があります。ということは、あまり時間がかからないと予想していると、予想よりもずっと時間がかかる可能性があり、優先順位逆転と同じような問題となります。
面倒ですが、 この内容の言及 の他に、ここに直接参照があるのではと必死に探しました。ソースコードまで調べるのはやめにしました。何をどこに探しに行けば良いのか分からなかったし、iOSやOS Xは閉鎖的なシステムなので、見つかるかどうかの確証がなかったからです。だから、少なくとも現時点では、この件についてよく知っている人の言葉を参考にしましょう。
無制限の実行時間に加え、 malloc
はロックも使います。
ファイルやネットワーク入出力でも同じことが起こります
入出力関数の全て、つまり、 read
、 fread
、 fgets
、 write
、 send
、 sendto
、 recv
、 recvfrom
などは、mallocのように実行時間が無制限です。メインではないスレッドである必要があります。
libdispatchやブロック使用は?
残念ながら、これらも使うべきではありません。ブロックを保持したり、保持を解除したりしない限り、オーディオスレッドでブロックを安全に呼び出すことが 出来ます が、オーディオスレッドでブロックを 生成 すると、オブジェクトの保持やメモリ割り当てを行うことになり、ひいては、どちらもロックにつながります。しかし、この件に関して、良い知らせがあります。すぐに知りたければ、AEMessageQueueについての説明に行ってください。
だったら、どうすべき?
既に説明した通り、広く使われているオペレーティングシステムは、どれも本当の リアルタイム OSではありません。ということは、保証がないということです。ですから、トラブルが起こる可能性を 最小限にする しかありません。
そして、良い知らせは、これに対応しているツールがたくさんあり、非常に簡単に使えるものもあるということです。
まず、Objective-CはCに基づいて構築されていて、Objective-Cのインスタンスは、実質的にC構造体のように@implementation block内のCの関数からアクセスできます。
FFCrewMember * jayne;
...
jayne->location = FFLocationBunk;
これは、シンプルで古いポインタ逆参照で、Objective-Cが不正にロックなどする余地はありません。リアルタイムスレッドで使うのに確実に安全です。
従って、オブジェクトを渡すことも使用することもできます。しかし、先ほど言ったように、いかなる保持も避けなければなりません。もう一度言っておきます。Objective-Cインスタンスの変数を宣言する際、 __unsafe_unretained
属性を使ってARC関係の処理を回避する必要があります。
void MyCFunction(__unsafe_unretained FFFertileLand * thisLand) {
__unsafe_unretained FFFertileLand * yourGrave = thisLand;
}
簡単でしょう?
注意:公開前のこの記事へのフィードバックで、Tempo Rubato (NLog, Nave, iSEM)のRolf Wöhrmann氏から、Objective-CやSwiftのオブジェクトにオーディオコード内から いかなる 参照もすべきではないとの指摘がありました。 __unsafe_unretained
属性付きでもダメです。代わりにCかC++変数で渡すだけにすべきとのことです。この2つを完全に分離させる方が良いそうです。これが確実に最も安全な方法で、効果的な防衛戦略です。もし疑うなら、やってみてください。
スレッド間の同期については?ロックを交換するにはどうすればいい?
これについては、色々な方法があります。Appleは libkern/OSAtomic.h
のヘッダ内で、 OSAtomicEnqueue
と OSAtomicDequeue
、 OSAtomicAdd32Barrier
と OSMemoryBarrier
など、とても便利な機能を数多く提供しています。また、ロックがない場合に備えてまともな対処法を用意している限り、tryLock( pthread_mutex_trylock
など)が利用できると考えられます。
知っていると便利ですが、最新のプロセッサなら、 int
、 double
、 float
、 bool
、 BOOL
、ポインタなどのスレッド上の変数に安全に値を割り当てることができます。また、読み取る際には一部の値だけが割り当てられているため、別のスレッド上で読み取ってもバラバラになる心配はありません。これは、変数が自然にアライメントされる限り(例として、Objective-Cのインスタンス変数やアンパックの構造体であれば自然にアライメントされます)、 バイト、半語、語長などの割り当てが、不可分だからです ( ARM アーキテクチャ リファレンス マニュアル ARMv7-A および ARMv7-R エディション )。他のタイプの変数には必ずしも当てはまらないので注意してください。例えば、32ビットのプロセッサで uint64_t
という変数を割り当てると、うまくいきません。なぜなら、処理の途中で他のスレッドから値を読み込めるようにするために、プロセッサは値の格納に2つに分かれた命令を必要とするからです。
しかし、もし面倒が嫌なら、便利な支援ツールはたくさんあります。皆さんがこれらのツールを使ったとしても、私は皆さんを責めたりしません。同時に起こるプリエンプティブな操作をとことん考えようとすると、私も頭が痛くなってきます。
私が使っているものをご紹介します。
TPCircularBuffer は、数年前に自分で書き、今でも日常的に使用している循環バッファライブラリです。この循環バッファライブラリは、広く利用されています。これを使うと、1つのスレッドの一端にデータを置き、そのデータを他のスレッドから引き出すことができます。その際、ロックは不要で、循環バッファをラップポイントと共に使っているということを完全に無視させてくれる仮想メモリのトリックを使います。これはインターリーブな AudioBufferLists
とインターリーブではない AudioBufferLists
の両方で、読み取りや書き込みを可能にしてくれます。また、 AudioTimestamp
の値も含んでいます。これらはどれも、Core Audioの利用を簡単にしてくれます。また、 The Amazing Audio Engine 2 に AECircularBuffer
として.組み込まれています。
The Amazing Audio Engine 2 の AEManagedValue はポインタ変数を提供します。このポインタ変数は、慎重に管理されているため割り当てが不可分で、オーディオスレッドが値を使い終えた直後にのみリリースされます。これは、どんなデータ構造やObjective-Cクラスを指す場合にも使うことができます。値を変える時は、オーディオスレッドに干渉しない古い値だけがリリースされます。
前述したNoteリストの例を先に進めるために、AEManagedValueを使ってNoteListポインタへの参照を維持し、変更があった場合は単純に割り当てをやり直すことができます。
@interface MyClass ()
@property (nonatomic, strong) AEManagedValue * noteList;
@end
@implementation MyClass
- (instancetype)init {
...
self.noteList = [AEManagedValue new];
...
}
- (int)addNoteWithFrequency:(float)frequency velocity:(float)velocity atTime:(uint64_t)startTime {
// Get old list, and copy it to new one
struct NoteList * oldNoteList = self.noteList.pointerValue
struct NoteList * newNoteList = malloc([self sizeOfNoteListWithCount:oldNoteList->count + 1]);
memcpy(newNoteList, oldNoteList, [self sizeOfNoteListWithCount:oldNoteList->count]);
// Update
newNoteList->count++;
newNoteList->notes[newNoteList->count-1] = ...;
// Assign new list - old value will be automatically freed at a safe time
self.noteList.pointerValue = newNoteList;
}
void MyAudioRenderFunction(__unsafe_unretained MyClass * self) {
// Get latest value
struct NoteList * noteList = AEManagedValueGetValue(self->_noteList);
// Make noise
for ( int i=0; i<noteList->noteCount; i++ ) {
ProduceAudioForNote(¬eList->notes[i]);
}
}
もう少し簡単にするために、ここでもThe Amazing Audio Engine 2の AEArray をAEManagedValueに構築し、オーディオスレッド上で安全にアクセスできるNSArrayとC言語の配列の間にマップを実装します。もしくはObjective-CのオブジェクトとC言語の構造体の間にマッピングするブロックを与えます。
上の例をもう一度振り返ってみましょう。NSArray内に、 MyNote
Objective-Cクラスがあるとします。
@interface MyClass ()
@property (nonatomic, strong) NSMutableArray * playingNotes;
@property (nonatomic, strong) AEArray * noteArray;
@end
@implementation MyClass
- (instancetype)init {
...
self.playingNotes = [NSMutableArray array];
self.noteArray = [[AEArray alloc] initWithCustomMapping:^void *(id item) {
// We'll provide a map between the Objective-C MyNote instance, the properties of
// which we cannot safely access on the audio thread; and our C struct, which we
// *can* safely access.
// This happens on the main thread during a call to "updateWithContentsOfArray",
// and the pointer we return will be freed automatically when the original
// Objective-C object is removed from the array.
struct Note * note = malloc(sizeof(struct Note));
note->frequency = ((MyNote*)item).frequency;
note->velocity = ((MyNote*)item).velocity;
note->startTime = ((MyNote*)item).startTime;
return note;
}];
...
}
- (int)addNote:(MyNote *)note {
// Update our array
[self.playingNotes addObject:note];
[self.noteArray updateWithContentsOfArray:self.playingNotes];
}
void MyAudioRenderFunction(__unsafe_unretained MyClass * self) {
// Enumerate the pointers in the array
AEArrayEnumeratePointers(self->_noteArray, struct Note *, note, {
ProduceAudioForNote(note);
}
}
最後に、またしても The Amazing Audio Engine 2 の AEMessageQueue を使うことで、ブロックをオーディオスレッドで機能させるスケジュールを設定することができます。
[self.messageQueue performBlockOnAudioThread:^{
_state = newState;
}];
別の使い方として、メインスレッド上のターゲットやセレクタのスケジュールを安全に決めることもできます。
AEMessageQueuePerformSelectorOnMainThread(
self->_messageQueue,
self,
@selector(doSomethingWithTrack:),
AEArgumentScalar(track),
AEArgumentNone);
これは、libdispatchに少し似た働きをしますが、オーディオスレッドは完璧に安全です。
問題があった場合、どのようにわかるのでしょうか?
私は、問題の診断を少し簡単にするツールを開発しました。このアイディアの元になったのは(有名な Audulus の) Taylor Holliday です。
これは、 Realtime Watchdog という小さなライブラリで(バージョン1に引き続き、 The Amazing Audio Engine 2 にも組み込まれています)。これをプロジェクトに加えれば、オーディオスレッド上で安全でない動作が行われていないかを監視し、もし何かを見つけたら警告を出してくれます。
全てを検出することはできないでしょうし、Apple自身のシステムコード内で起こることもキャッチできません。ですが、コードと、あなたが使っている静的ライブラリでは、若干のロックとメモリ・アロケーション、Objective-Cが使用しているもの(Swiftではありません)、オブジェクトが保持しているもの、そしてcommon IOが取得しているものなら発見することができるはずです。
これを使用するためには、”RealtimeWatchdog”を Cocoapods Podfile に追加し(“ pod 'RealtimeWatchdog
“)、 pod install
を実行するだけです。これでできました。自動的にデバック構成のための全ての違反が通知されるようになります。またリリース構成においては何も動作しません。デバック構成では、Objective-Cのメッセージ送信が少し遅くなるでしょう。そこで AERealtimeWatchdog.h
で定義された REALTIME_WATCHDOG_ENABLED
をコメントアウトすることで、いつでも割り込みを抑制することができます。
もしCocoapodsを使用しない場合、 GitHubリポジトリ 上のインストラクションをチェックしてみてください。
The Amazing Audio Engine 1または2を使っている場合は、動作させるために AERealtimeWatchdog.h
で定義された REALTIME_WATCHDOG_ENABLED
をアンコメントしてください。
最終的な考え方
携帯性、利便性、手ごろさ、そしてプラットフォームの持つ力のために、ますますミュージシャンはハードウェアを売って、iOS志向になり、すなわち楽器を手放し、アプリで楽曲制作を行うミュージシャンが増えてきています。私は、しばしばユーザからLoopyとAudiobusがどんなに彼らの創造的な可能性の世界を広げているかを聞きます。その声からも とても 刺激ややりがいを感じました。
しかし同時に、失望と不満という声も聞きます。アプリがバグったり、正しく動作しなかったり、とプログラムの問題回避法を必要とし、不安を引き起こします。開発者たちと一緒にその問題に取り組んでみた場合、しばしばオーディオスレッドのルールに反していることが原因となります。つまりロックを持たない、Objective-C/Swiftを使用しない、メモリを割り当てない、IOを動作させないということです。
このアドバイスは「システムレベルで何が起きているか」という推測に基づいている、と言及する(ここではRolf Wöhrmannの知己に富んだ言葉を言い換えます)ことは意味があるでしょう。しかしiOSとMac OS Xは閉じられたシステムです。 opensource.apple.com 経由で機器の中のピークを得ることだけができます。その成果は とても 微々たるものです。こういったルールに従ったとしても、上手く行かなくなることもあります。そこで経験に基づいた推測をしてみます。理論的には、それが難解でテクニカルだとしても、テストをして実験してみます。
私のアドバイスですか? 注意して聞いていてくださいね。 Core AudioのAPIメーリングリスト に参加して、質問してみてください。また、Xcodeを開いてオーディオコードで何が起きているか見てみてください。 Realtime Watchdog を試して、あなたのコードとあなたが使っている外部のライブラリをチェックするために使ってください。記事の最後に、人気のあるライブラリとオーディオスレッドの安全性を記したリストがあります。
またC++をチェックすることを考慮してみてください。オーディオではObjective-Cより安全で、C単体より多くの機能を与えてくれる傾向があります。
もしクラッキングやグリッチが起こっていて、それらをどうにかしなければならないのであれば、コードの最適化を検討したいと思うでしょう。特に Accelerate framework などを使用して、あらゆるスカラ演算をベクトル演算と入れ替えます。
コードの書き方にさらに注意を払うことによって、iOSオーディオをユーザにとってよりよい経験に作り上げることができます。すると今度はより真面目なユーザやインフルエンサをプラットフォームへ引きつけることができます。それは全ての人にとって利益となるはずです。
楽しいコーディングを!
さらに知識を深めるために
- Doug Wyattによる WWDC 2015セッション508: Audio Unit Extensions
(“…メモリを割り当てることができないので、これは制限的な環境です…事実、ブロッキングを生じる呼び出しは、どんなものであれ全く使ってはいけません。例えばmutesの取得や、セマフォの待機などです…もともと安全ではないので、Objective-Cのランタイムを避けます…困ったことに、Swiftのランタイムもまったく同じ方法を取ります”) - Ross Bencinaによる Real-timeオーディオプログラミング101: 時は何も待たない
- Mike Ashによる なぜCoreAudioは難しいのか
- Jeff MooreによるCoreAudio APIメーリングリストにおける寄稿 (“あなたはまた、IOProcからObjCやCFオブジェクトを呼び出すことはできません。どれも結果的にグリッチングを引き起こすことになります”)
- Wikipediaから 優先順位の逆転
- opensource.apple.com
- アトミック操作 vs ノンアトミック操作
謝辞
私の仕事をチェックし、意義あるフィードバックをしてくれた開発者の皆さまに、多大なる感謝の意を表します。
- Aurelius Prochazka ;彼のSwiftオーディオエンジンのフレームワークは、 AudioKit です。
- Wooji JuiceのCanis Lupus ;彼らのアプリはたくさんありすぎて言及しきれませんが、代表的なものは Grain Science と Hokusai 、 Ferrite です。
- Harmonicdog のHamilton Feltman ;彼のアプリは、 MultiTrack DAW です。
- KymaticaのJanatan Lifedahi ;彼のアプリは AudioShare 、 AUM 、 SECTOR 、その他たくさんあります。
- Tempo RubatoのRolf Wöhrmann ;彼のアプリは NLog 、 Ruckers 1628 と、関与したアプリ Nave と iSEM があります。
- Taylor Holliday ;彼のアプリは、 Audulus です。
そしてもちろん、私のパートナー、 Audiobus や Audanika のSebastian Dittmannにも感謝しています。
人気のあるオーディオエンジンのライブラリと、そのオーディオのスレッドの安全性に関する調査
ここでいう“警告”とは、必ずしもライブラリを完全否定するものではないことに注意してください。これは、そのライブラリがトラブルの可能性を上げるということを意味するのではなく、おそらくそのような部分もあるということです。これが価値あるものかどうかを決めるのは、あなたです。私は、それぞれのケースを最大の努力で評価しましたが、間違いがあるかもしれません。どんなコメントも大歓迎です。
最終更新は2016年6月28日。
[TABLE]
注:あなたが使っているライブラリがこのリストにない場合、あるいはリストが間違っているので確認してほしい場合は、コメントを残すか私にメールを送ってください。もしもあなたが、安全ではないとされているライブラリの開発者で、このリスト作成後にアップデートした場合も、同様にしてください。リストを更新します。
付録
JUCEにおけるmutexの使い方についての、Julian Storerのコメント
クライアントが所属あるいは解放される時にAudioDeviceManagerをロックすることと、それらのソースが加えられたり取り除かれたりする時にAudioSourceクラスをロックすることは、以下のような場合には非常に似たユースケースになります。
- 稼働時間の大半において、オーディオスレッドでロックが保持されている。
- 他のスレッドがこれを捕えることが非常に稀(多くはアプリのランタイム全体で1回か2回だけ)である。これを捕える時は、少数のバイトをシャッフルする間の数ナノ秒しかロックを保持しない。
これらの特定のクラスがmutexを使う方法は非常に単純な解決法で、ユースケースの大半で欠陥なく動きます。継続的にオーディオの録音再生グラフのトポロジーを破棄することのないアプリは、オーディオストリームに干渉を引き起こすことはまずありません。エッジケースを非難するアプリは、時々マイナーなグリッチを引き起こします。エンドユーザはこれに気づくかもしれませんし気づかないかもしれません。より複雑なロックフリーの実装により、余分な複雑性とバグのリスクが与えられますが、全体的に考慮すると、多少の問題を妥協して実装するのも悪くはないと選択だと思います。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa