今月はだいぶAAP(Audio Plugins For Android)で成果の出た1ヶ月になったと思います。1ヶ月を振り返ってみて割とびっくり。
Music tech meetup: LT meetup (3/4)
2月に告知したとおり、3/4にこの名義で開催したのですが、公募からはLTが1件も出てこなくて、目論んでいたLT meetupとはなりませんでした。当日まで自分だけが話すというステータスだったのですが、運営に参加していただくようになったhotwatermorningせんせいにCPUキャッシュ最適化まわりを話していただきました。
自分の「再履修: 2023年までのリアルタイムオーディオ処理」のLTスライドはspeakerdeckに上げてあります。
もはやlightening talkではなくlong talkという感じだったのですが、当日はスライドの半分くらいしか話せていません…(!) 「当日いきなり半分にも1/4にもなる可能性もあるからその前提でスライドを作る」という意気で書いたやつだったのですが、やっぱり本当に当日いきなり半分になったら自分の期待通りには回りませんでした…(!)
LT meetupとしてはだいぶ未遂に終わったのですが、このコミュニティの勉強会をここ数回やっていて、毎回「自分がしゃべるイベント」にしてしまっているのが良くないと思っていて(それはコミュニティの名義を「私物化」した個人活動になってしまっているし)、それは実現したいことではないので(個人でやるならAAPの話をしたい)、むしろ誰でもしゃべる機会があるmeetupを開催すべき、と思って今回の企画に至ったわけです。今回は「(運営を除いて)事前に特定の知人に発表を打診しない」公平モデルのやり方にしたのが、結果的にコミュニティの現状と合わなかった感じでした。誰も空白から最初の一歩は踏み出せず、鳥も卵も生まれなかったということです。
もっとも、事前にトークを打診するやり方は、いろいろ考慮することがあって、単に「やり方」の問題だけではないと思っています。自発的にしゃべってくれる人以外に依頼するとなると、プレゼンテーションの作業負荷を考えなければならなくなります(前回のZrythm勉強会を「自分が調べて話す」スタイルにしたのはそのためでした)。ワーストケースとしては「無償で講演を依頼されたんだけど」という「告発」を受ける可能性すら考慮しないといけなくなります。個人的にはそのテのやつを見ると「そりゃ無償でお願いするだろ」と思いますが、コミュニティ名義で依頼するならそうも言っていられない側面があります。公平な募集モデルは安全側に倒したやり方なんですね。
自分はオーディオ開発者コミュニティの分野では完全に有象無象なので、このテの依頼を懸念なく発行できるようなポジションにはいません。もうちょっと精進して自分の名前を売り込んでいかないと実現しないでしょう(あんましそうしようという動機がない)。
自分たちのコミュニティが特別に消極的だということはなくて、「LTを公募すればトークが勝手に集まってくる」「枠が空いていればここぞとばかりに応募が滑り込んでくる」みたいな状況は、他のコミュニティでもそんなに自然にはなさそうです。こんど4/21にはShibuya.apk #41で15分しゃべるのですが(AAPの話をするつもり)、この枠もずっと空いていて「そんなもんだったっけ…?」と思いながらサクッと取りました(Shibuya.apkはAndroid界隈では有数のコミュニティ勉強会)。
LTしたいひとがいないコミュニティだ、と捉えてもいいのですが、状況的に応募しにくかっただけの可能性も高く、そこを推測しても不毛なので、とりあえずは「他の人にしゃべってもらう」ことは忘れて、自分が勉強会のネタを思いついたときだけ開催するスタイルに戻ろうと思います(機会は作ったので「私物化〜」で忸怩たる気持ちはなくなった)。もちろんコミュニティとしては他の人が提案してもいいし、もし案を持ち込んでくれる(運営でもない)人がいれば対応するつもりです。
AAP new audio plugin buffer API and V3 protocol
2月の途中から、AAPのコアAPIに含まれていたAndroidAudioPluginBuffer
という構造体を引き回す設計を解体して、CLAPのようにaap_buffer_t
という構造体に、ポートごとのポインターを取得する関数ポインタをホストが設定して、プラグインはそれを呼び出す、というスタイルに変更しました。これによって、内部実装のleaky abstractionを隠蔽できたと思います。構造体の設計自体がやっつけだったのが、全プラグインで使われることになっていて、ずっと直したかったやつのひとつです。(他にもAndroidAudioPluginExtensionTarget
なんかも直したい…)
この変更作業の過程で気付いたのですが、aap-juceがAudioProcessor::process()
を呼び出すとき、そのバッファサイズは固定になっていて、これは場合によっては存在しない空白を作り出してノイズの元凶になっているのでは…と気付きました。JUCEのAudioProcessor
では、processBlock()
にframeCountを渡すことになっていて、JUCEホスティングアプリケーションではこの数がAAPの想定するバッファサイズとは毎回異なっている可能性があります。それも各アプリケーションのレベルでOboe StabilizedCallbackのように固定されたほうがよさそうではありますが、そうなっていないときのために、processBlock()
に渡されたフレーム数でAAPもprocess()
を呼べるようにAPIを変更するのが筋だと気付きました。
そういうわけで、半年前に2年ぶりにAAP "V2" protocolに更新したばかりですが、今月からAAP "V3" protocolとしてprocess()
がprocess(frameCount)
になっています。正式なリリースまでに他にも変更が加えられるかもしれませんし、現時点では破壊的変更がまだ躊躇なく行われるでしょう(関連リポジトリを全部自分で更新するのが面倒…という理由で躊躇することはあります)。
ちなみにこの変更の過程でMidiDeviceServiceのオーディオ出力が常にノイズまみれになるという割と大きなリグレッションが発生していて、しばらくその問題に取り組む作業が続きました…(問題はMidiDeviceServiceのリングバッファまわりの変更にあって解決済)
AAP: RT-safe input dispatcher locking in MidiDeviceService
LT meetup(と称する何か)でリアルタイムオーディオ処理について学びなおしたので、AAPでずっと雑に放置されていたMidiDeviceServiceでMIDI入力がAAPのprocess()
の処理中に飛んできた時に何が起こるかわからない問題に真面目に取り組むことにしました。
今回は(1)MIDI入力はnon-realtimeでJava APIから飛んでくる、(2)オーディオ処理はOboeでリアルタイムスレッドから必要に応じて呼び出される、というのをやっています。MIDI入力を反映するタイミングは保証できないのだから、オーディオスレッド側が入力に対応できないときは飛ばし、入力側のスレッドは必然的にブロックするので(最低でも)ロックをかけてから更新するというレベルでは協調する必要があります。
これは、勉強会資料でReal-Time 101のセッションから引用したさまざまな排他制御シナリオのうちのひとつ、「非リアルタイム処理ではwrite lockをかけられるようになるまで待って更新し、リアルタイム処理ではread lockの取得に失敗しても良い」パターンです。オーディオループでread lockの獲得に失敗してもオーディオ処理が正常に流せるように、MIDI入力処理スレッド側ではオーディオ処理のデータを直接更新しません。更新は入力イベントキューへの追加というかたちで行われます。オーディオ処理側はロックを取得できたら、イベントキューの内容をコピーしてきてキューを空にし(この間ロック解除を待っている入力処理スレッドはブロックされている)、それをオーディオループに渡して処理することになります。Real-Time 101ではspin lockしていましたが、モバイル端末で野放図にspin lockしてバッテリーを消耗したくはないので、nanosleepしてspinしています(Androidで動けばいいので!)。nanosleepは当然システムコールですが、RT safeなオプションで使っています。
AAP: Oboe StabilizedCallback in MidiDeviceService
RT safeな更新処理を実装してみたものの、AAP MidiDeviceServiceのaudio glitchは依然として大きいものでした。他に原因があるはず…と思いながらいろいろ試行錯誤したのですが、OboeのStabilizedCallbackを使うようにしたら一気に安定したので、これを使うことにしました。
StabilizedCallbackというのは、そうでないunstabilizedな状態とは異なり、オーディオループすなわちOboeのオーディオコールバックで、バラバラなサンプル数のオーディオブロックが渡されることはない、という処理モードです。デフォルトでは、Oboeは自分のオーディオバッファの処理状態(次のオーディオ処理までの空き時間の状態など)に合わせて最適なサイズを計算してコールバックを仕掛けてきます。AAPのようにプロセス間通信が必要になったり、DSPでもオーディオループごとに行われる計算処理(あるいはmemcpy()
呼び出しなど)が不必要に増えてくると、最適サイズとやらで小さいデータを送るようなmicro optimizationをかけられるとかえってパフォーマンスが落ちる…ということにもなりかねません。そういったオーディオアプリ開発者のニーズに応えて、Oboeには固定サイズでコールバックを回すStabilizedCallbackというAPIが用意されています。
StabilizedCallbackを「使うようにした」といっても、MidiDeviceServiceの中で使うようにした変更より(それもやりましたが)、JUCEホストアプリケーションの中で#if JUCE_USE_ANDROID_OBOE_STABILIZED_CALLBACK
というundocumentedな定義を追加したのが一番効いています。要するに、JUCEではこれを使っていなかったということです。Androidにはオーディオプラグイン環境が無いので(!)、StabilizedCallbackを使うのがデフォルトとはなっていなかったわけです。もちろん、Standalone AppとしてビルドされていたJUCEプラグインの移植でも全てこのunstableなcallbackがガンガン回っていたことになります…(この問題が影響する範囲が割と大きかったといえますが、これは次節で詳しく書きます)
StabilizedCallbackを使うべき場面はおそらく割と限られていて、Oboeのリポジトリに含まれるサンプルコードでもStabilizedCallbackは使わずにLatencyTunerを使っている、みたいな話があったりして、この辺のベストプラクティスはまだ流動的に動いている可能性があります。
AAP MidiDeviceServiceは、AAPの中では一番まともに動いている部分ですが、それでもMIDIメッセージの「抜け」が頻発し、出音もプラグインによっては頻繁に飛ぶ、みたいな状況でした。メッセージが抜けるのはどうやらkmmkなどのMIDIクライアントアプリ側の問題で、音飛びの最大の理由も次に説明するJUCEの問題が大きかったので、MidiDeviceServiceの問題は相対的には小さかったのですが、とりあえず自分の実装を安心できるものにしておくことは重要です。
AAP: struggling with JUCE pitfalls
AAPのオーディオ処理が雑なのは年単位で放置されていた問題だったのですが、今年はAAPを実用的なオーディオプラグインのエコシステムとして使える状態にしたいと思っているので、いつまでもオーディオ処理が雑なのは良くないと思って、少なくとも理由もなくノイズまみれになったりするのは何とかしようと思って、いろいろ調べ始めました。AAPはアプリケーションの境界を超えるオーディオプラグインの仕組みとしては世界レベルで最先端なので(!)、できることには限界がありますが、できる範囲では対策していこうと思っています。
そういうわけで、今月はこの方面でいろいろ調査して改善を施しました。まず、圧倒的に悪かったのはJUCEですね(!) どういうことなのか説明していきます:
- JUCEのオーディオプラグインプロジェクトでは、Standaloneプラグインフォーマットのアプリケーション(AudioPluginHostなど)は、iOSやAndroidでは「MIDIデバイスを全て検出して」「入力デバイスの1つを勝手に開いて使える状態にする」挙動になっている
- これは
#define JUCE_DONT_AUTO_OPEN_MIDI_DEVICES_ON_MOBILE 1
のように定義すると無効化できる(undocumentedな定義)
- これは
- JUCE on Androidは、MIDI入力デバイスのリストにMIDI出力デバイスがリストアップされるメチャクチャな実装になっている(既知の問題)
- JUCE on Androidが、Android MIDIデバイスの検出過程で、MidiDeviceServiceを全部openするので、デバイス上の全MidiDeviceServiceが「起こされて」プロセスとして残り続ける
- MidiDeviceServiceによってはこの時点でOboeのオーディオループが回り始める(MIDIデバイスを "open" しているんだから、それはそう)
- aap-juceで移植されたプラグインは、Activityを起動するとこのStandaloneモードのコードが走ることになる
- JUCE on AndroidはJUCEの初期化処理の過程でActivity起動部分を乗っ取って、
AndroidManifest.xml
のmain launcherに何を指定してもJuceAcitivityが起動するようになっている
- JUCE on AndroidはJUCEの初期化処理の過程でActivity起動部分を乗っ取って、
- AudioPluginHostや自作のプラグインホストでも(つまりjuce_audio_plugin_clientのどこか)、勝手にMIDIデバイスを全部開くコードがある。前出の
#define
はStandalone Appにしか適用されていないのでgrepでも見つからない…
要するに、JUCE on AndroidのStandaloneアプリケーションが立ち上がるとMidiDeviceServiceのどれかがOboeでオーディオループを回し始めるので(しかもJUCEは「入力デバイス」を開いているだけのつもり!)、複数のアプリケーションが同時にリアルタイムオーディオ処理を回している、ということになります。そりゃパフォーマンスも悪くなるわけだ…
これ調べるのだいぶ大変でした。
JUCEチームは、特にROLI傘下で開発を回していたときは、Androidサポートが明らかにおざなりで優先度が低かったので、こういうメチャクチャな実装が含まれていることが少なからずあります。プラグイン側はaap-lv2を使えばよさそうですが、ホスティングまわりではJUCE以外の選択肢がほしいところですね。JUCE以外の選択肢で最初に思いつくのはCarlaエンジンを使ったIlldaelなどですが、Carlaのビルドも現状あまりAndroidには向いていないです。
とりあえずの対策として、aap-juceではJUCE_DONT_AUTO_OPEN_MIDI_DEVICES_ON_MOBILE
の利用を必須とすることにして、Projucerプロジェクト用のビルドスクリプトではこれを強制しています。CMakeプロジェクトの方は現状あくまでCMakeLists.txt
に自分で書かないといけないので手作業対応になってしまうのですが、これ以上対策するかは現状では未定です。しかしAndroid端末側にopen時点でOboeループを回し始めるMidiDeviceServiceがあるともうどうしようもないので、根本的な対策にはなっていません。JUCEの問題コードを探り当てる必要があるでしょう。
とはいえ、これは全体的に見て悪いニュースではなく、この調査結果をもとにどういう対策を講じれば自分の環境では問題が発生しなくなるのか、だいぶ高い精度で分かるようになってきたので、たとえばスクリーンキャプチャーを公開したいと思ったときには、従来「奇跡的にうまくいくことがある」のレベルだったのが「だいぶ高い確率でできるようになった」まで向上しました。たとえばこれくらいの動画がカジュアルに録れるようになっています。
View post on imgur.com
imgur.com
AAP audio tracing support
JUCEの問題は、問題の全てというわけではなかったのですが、いかんせんどういう状態にある時にaudio glitchesが発生するのかを予測できないことが大きな問題でした。AAP自体のオーディオ処理で時間がかかりすぎている可能性もあれば、AAPで呼び出しているプラグインのDSP処理自体が遅い可能性もあるし、AAPの問題だとしてもaap-coreの問題かaap-lv2 / aap-juceの問題かもしれない。ちゃんとした計測が必要です。
aap-coreでもaap-lv2でもaap-juceでも、調べたい時に処理時間を計測できるように適当にAndroid logに計測のサンプリング結果を出すやつは#if ...
で有効化できるように残してあったのですが(ログ出力はリアルタイムオーディオ処理には向いていない)、もう少し真面目にプロファイリングしようと思って、今回はちゃんとAndroid profilerとATrace APIを使って結果を残すようにしました。Android profilerを使って実行するとAAP関連の項目が追加されるようになっています。
Androidオーディオのプロファイリング手法については、かつてAndroid Audioのdeveloper advocateだったDon Turnerが詳しくまとめていたのですが、彼はもうオーディオ専業ではなくなってしまって、この情報ではすでに計測できません。2023年のプロファイラーでは、CPU Tracingは記録だけを*.trace
に残して、プロファイリング結果はPerfettoを使って読み解くのが公式に推奨されるやり方です。そしてAAudioを有効にする手順を踏む必要があります。
このPerfettoを使ったプロファイリングの手順は、自分がざっくりWebで探した限りでは誰もまとめていなかったので、このAAPのドキュメントに最低限必要な情報をまとめてあります。Androidデバイス側でプロファイリング項目を設定する必要があります。あるいは、上記ブログの頃とは異なりAndroid Studioにはオーディオ関連のプロファイリングオプションを指定する手段が無いので、この辺の問題(報告した)ら、Giraffe Canary 10以降では修正が適用されたようなので、もしかしたらデバイス側の手順は(ASがオーバーライドしたプロファイル設定をadbで送れるように実装されているなら)不要かもしれません。
aap-juce-simple-host
JUCEの特にホスティングまわりは、AudioPluginHostのAndroid移植によって検証されていましたが、AudioPluginHostには様々な問題があって、特にAndroidデバイス上ではUIが全く使い物になりません。PCのタッチパッドでもわかると思いますが、ためしに指でコネクターを接続しようとしてみてください。細かい上に指が邪魔で目視できません。AAPの開発ではAudioPluginHostのスクリーンショットがよく出てきますが、全部エミュレーターだからマウスでできていることです。
そういうわけでもう少しモバイルに適したUIのAAPのdogfoodingに特化したテストアプリケーションを作らなければならない…というのは長年自分のwishlistに乗っていた懸案事項でした。2月についに思い立ってゼロから作ったのですが、デスクトップで動くものをビルドしただけで、Androidに持ってきたら、やっぱりまともに動作しなかった…という感じで↑の調査に踏み込んだ、というのが先月から今月にかけての流れです。いずれにしろ、調査の過程で、誰でも確認できるようなコードが公開されていることが重要だと思ったので、開発中のある時期から(ちゃんと動かなかったけど)公開してあります。今はそれなりに動くはずです。
AAP: working AAP GUI extension
V3 protocolとしてオーディオバッファのAPIが整理され、JUCEがさまざまな問題を引き起こしていたことがクリアになって、AAP自身の問題はあまり無いことがわかってきたので、いよいよ先月ガワ = GUI拡張機能だけ作ってあって表示制御しかできていなかったやつに、実際にMIDIメッセージを送信する部分を実装できる状態になりました。
とはいっても、最終的にはシンプルに、いまGUI表示制御を行っているlibandroidadudiopluginの実装で、MidiDeviceServiceが行っているようなMIDI2イベントキューをクライアント実装に追加して、クライアントAPIにユーザー(アプリケーション開発者)から渡されたMIDI2イベントとマージして送るようにしただけです。Web UI上で発生したイベントは、JavaScriptInterface
のAAP Interop実装によって、AudioPluginWebView
の生成した時に渡されたorg.androidaudioplugin.hosting.AudioPluginInstance
やaap::RemotePluginInstance
から生成されたorg.androidaudioplugin.hosting.NativeRemotePluginInstance
に送られます。
(たびたびMIDI2イベントと書いていますが、aap-lv2やaap-juceはMIDI1のイベントをUMPに変換して送っているので、プラグイン開発者がMIDI2を直接送る状況は現時点ではあまり考えられません。自力でプラグインやラッパーを実装する人のみ関係ありです。AppleがCoreMIDIやAUで内部実装が完全にMIDI2化しているのを隠蔽しているのと似たような構図です。)
Kotlinクライアントの実装だけではネイティブコードからちゃんと使えるかわからないのでaap-juceのホスティング実装であるjuceaap_audio_plugin_client
でも、AndroidAudioPluginInstance
クラスでcreateEditor()
をオーバーライドして常にWebView GUIを返すようにして(Web UIサポート実装の中にデフォルトUIが最初から用意されているので各プラグインが実装している必要はないわけです)、前記aap-juce-simple-hostをはじめaap-juce-plugin-hostでもGUI表示が行えて、鍵盤に反応して音が出ることを確認してあります。aap-juce-helioでもUIは表示されるのですが、モバイル環境だと(?)UI表示中はオーディオ処理が回っていないようで音が出ず、パラメーターの変更が演奏時には反映されているというかたちでしか確認できません。
この辺の成果は、前出のimgurにデモとして上げてあります。
これらの実装とは別に、そもそもGUIイベントがどのようにプラグインとホストの間で受け渡されるかが明確になっている必要があって、今月はその辺の設計を詰める作業も進めていました。GUIまわりはまだきちんと実装されていない部分も多く、これは5月以降の課題になりそうです。
AAPのGUIサポートは、2020年以来のlong-standing issueだったので、これをついに閉じたときは感慨深いものでした。(まあもっと古いopen issueも残っているのですが)
来月の予定
4/30にM3 2023春があるのですが、AAP最大の技術的課題であるGUIサポートに目処がついたこともあって、今回は満を持してこのAudio Plugins For Androidの設計と実装に関する同人誌を出す予定です。今月はそのための作業(執筆等)も始めていて、目次案を見返して「ホントに終わるのか…?」となっていますが、何とか月末までの発行に漕ぎ着けたいと思っています。そのため開発作業はだいぶ停滞する予定です。「5月以降の課題」と書いたのは勘違いではないです。
また、前出の通り4/21のShibuya.apkでもこの辺の話をする予定です。Android開発者はAndroid SDKくらいまでの知見はあってもNDKまわりはあんまし無いだろうし、オーディオプラグインの知見はほぼ皆無だと思うので、その辺からふんわり話して終わると思います。オフラインのみ・すでに満員のようなので、興味があればぜひ〜とは言いがたいところですが…! (興味がある方はM3のほうに来ていただければ、そちらで無限にお話ししようと思います)
4月はNAMM 2023もあってオンラインで眺めているはずで、おそらくNAMMではアップデートされたMIDI 2.0の仕様も公開されると予想されているので(アップデート自体はADC22とかで言われていた、Protocol Negotiationとかが消えるみたいなやつ)、だいぶ忙しくなりそうです。