4月の開発記録 (2022)

4月が終わろうとしています。今月は副業で某XIVの冒険者を始めてしまって進捗がひどいことに…

ADC21セッション動画鑑賞会 #1 をやった

4/6にひさしぶりにオーディオ開発の勉強会を開催しました。

music-tech.connpass.com

見たのはVitalの開発者がウェーブテーブルの最適化手法について語っているセッションです。

www.youtube.com

ADC動画鑑賞会やろうとは昨年から思っていたのですが、例年だとカンファレンスの1週間後くらいに全部上がっていたのが、今年はなぜかいつまで待っても公開されず、出てきたと思えば毎週1本程度ちょろちょろと出すだけ…という体たらくで、いつ出来るのやら…という感じでした。今回の動画も含めて、実は今でも大半の動画は既にアップロードされているのにunlistedのままだったりします(公開動画から他の動画にリンクされているのに気づけば掘り出せます)。

この種の勉強会をひさびさに開催したこともあってか、当日は参加者の方々からさまざまな補足説明や参考情報の提供をいただけて(主催した自分が雑談の20%もしゃべっていないくらい)、非常に有益で楽しい会になったと思います。もともと録画して公開する予定はなかったのですが、録画くらいはしておいてもよかった…。初回からちょっと難易度が高すぎるかな?と思ったのですが、結果的には大正解だったと思います。1/5くらいはわたしがDSPの素人に毛が生えた程度の知識しかなくて話をリードする役に立たなかったことに起因しています(!) これはいいことなので継続してそうやっていきたいですね(!?)

第2回もカジュアルに1週間くらいでふらっと計画・実行したいと思っています。

AAP extensions

今月は、ほぼ1ヶ月ずっとandroid-audio-plugin-framework拡張機能を実現する方式を見直し、設計を書いては投げ、実装しては投げ…をやっていました。どんくらいずっとかというと、githubにほとんど草が生えないくらい(mainブランチでやっていないから一切が草にならない、という要因もありますが、開発ブランチでも更新頻度は低い)

拡張機能はAAPのようなプラグインフレームワークAPI安定性を維持するために「コアだけは安定させ、追加機能は全部拡張APIとして作り込む」というもので、VST3でもVST-MAとして実現していますし、LV2も大量のモジュールで編成されていますし、CLAPも数多くのfundamental extensionsを設計しています。

AAPも建前上はLV2やCLAPに近い拡張機能をサポートしていることになっているのですが、一方でもともとJUCEモジュールとして作り始めたこともあって(JUCEではAudioProcessorさえサポートできればよく、AudioUnitも対応しているので拡張機能なにそれおいしいの状態)、ちゃんとしたサポートが後回しになっていました。proof of conceptとして当初は「プラグインとホストで共有するASharedMemoryを管理する構造体」やら「MIDI 1.0を使うか2.0を使うか指定する構造体」だのをextensionとして定義して(本来前者は拡張にするのが相当ではない)、何となくやったつもりでした。

しかし今月になって「MidiDeviceServiceで出音を調整するのは無理があるし、カジュアルにプログラムチェンジで音を切り替えられるようにpresetsを実装するか〜」と雑に思いながら、実際に「拡張機能」に相応しいAPIを設計しようと思って着手してみると、LV2やCLAPとは全く異なるレベルの問題を解決しなければならず、実はこれめっちゃ難しいやつでは…!?となりました。メモリ空間が別々にあるので関数コールバックは使えない、ということは以前から把握していたのですが、もう少し上位の視点でまとめると、AAPはAudioPluginServiceとclient hostの分離プロセスの構造の上で以下の3つを実現する必要がありました:

  • プラグイン開発者もホスト開発者も自然に拡張機能を使えるようにする
  • 拡張機能の開発者がAAPの内部構造に依存せずに実装を提供できるようにする
  • libandroidaudiopluginがホストでもプラグインでも任意の拡張機能をハードコーディングなしでサポートできるようにする

これ、LV2やCLAPどころかAUv3でも実現していないやつでは…!? (デスクトップのプラグインフレームワークではdlopen/dlsymで一発だし、AUv3に拡張機能らしいものは無さそう)

これを実現するためには、AAPコア部分でやっているのと同じように、Binder等を使って拡張機能の呼び出しを処理できなければなりません。しかしプラグイン開発者がBinderまで意識してその処理を実装しなければならないという事態は(aap-juceやaap-lv2のような中間層が全部吸収することになるとはいえ)避けたいですし、ホスト開発者にプラグイン拡張機能を外から操作するコードをBinderをいじるレベルで実装させるというのも避けたいところです。

それならば、拡張機能の C APIを呼び出すホストと、拡張機能のC APIを実装するプラグインでは、シンプルなAPIの入り口だけを用意して、それらがBinderでクライアント・サービスを実現する部分については拡張機能の開発者が提供するようにすれば良いのではないか、と考え、extensionの開発者にはextension serviceも実装してもらう、という方針にしました。拡張APIで利用される「関数」は、ホスト側ではBinderを使ったいわゆるproxyが担うことになる、というわけです。ホスト「を」開発する人はそんなことを意識しなくても、get_extension(URI)だけでそのextensionの構造体が返ってくる、みたいな仕組みです。

プラグイン開発者が実装した拡張機能のコードが動作するのはプラグイン(サービス)側のアプリなので、プラグインによって拡張機能が呼び出されて処理している範囲では難しいことは何もありません。一方、そのプラグインアプリ全体としては、その拡張機能を呼び出すコードは、ホストからのリクエストをさばくAAPフレームワークのAudioPluginService(のBinder)であり、AAPフレームワーク開発者としては「誰が書いたかわからない拡張機能のコードをプラグインのプロセス空間で実行できる必要があります。そのためには、「Binder経由のリクエストで指定された拡張機能の指定された関数を呼び出す」ためのルーディングを実装して、それらのリクエストを拡張機能の開発者に渡して処理してもらう必要があります。

AAPとプラグインを繋ぐ部分と、AAPとホストを繋ぐ部分は、それぞれその拡張機能で出来ること・やるべきことを掌握している拡張機能の開発者が担うのが適切です。わたしは当初この部分を甘く見ていて、たとえば「ホスト開発者はpresets拡張機能をサポートするコードを自分で書いているのだからホスト側が直接presetsをサポートするコードを書けばいい」と考えていたのでした。それだとAAPのフレームワークのレベルで実装が追加されない限り、誰もこのproxy部分を開発できないわけです。

機能要件としては、この3〜4者(プラグイン開発者、ホスト開発者、拡張機能開発者、AAP開発者 = 自分)の役割をきちんと切り分ける必要があり、またそれぞれがアクセスできるAPIを適切に限定するのもフレームワーク設計でやるべきことです。特に現状どれもC APIとして提供することにしているので(拡張機能くらいはC++限定でも許されたいところですが)、実装言語であるC++との調整作業でさらにバグにハマって神経を削る…という感じです。

そのへんの話をgithub issueやドキュメントとしてまとめる作業もやってあって、ただ設計初期の内容なので更新しないとな…という感じです(あくまで雰囲気だけ共有):

ちなみに、これだけ設計を練っていても設計時点で解決できていない課題があって、この拡張機能はオーディオ処理の中で使えるようにはできていません。拡張機能をホスト側から操作することはあっても、プラグイン側からホストを操作できるようにはなっていませんし、オーディオ処理自体がBinderのリクエストの処理となっているので、その中でさらに拡張機能をBinder経由で「操作」することは考えられないためです。その代わり、ホストとは共有メモリのチャンネルを確立しているので、ホストからプラグインが必要としそうな情報を共有メモリ上に展開しておくことで、一般的なリアルタイム需要は満足できるはず、という設計になっています。たとえばLV2 Timeが提供する現在の演奏位置やテンポといった情報は、共有メモリデータだけで実現できるでしょう。

一方で「拡張機能がホストに追加情報を問い合わせて処理を続行する」ような仕組みにもできないので、典型的な拡張機能が何かしら実装できないという可能性もまだあります。たとえば、拡張機能ではありませんがjuce::AudioProcessor::getPlayHead()juce::AudioPlayHeadを取得してtransportRewind()を指示するような機能は実現できません。この辺は実際に拡張機能をひとつひとつ実装してみないと見えてこないでしょう。

5月は…

…というわけで、今月はこの課題にずっとかかりっきりでアウトプットの少ない日々でした(まあそんなことより冒険者としての副業がry) この辺の知見もいずれ改めてまとまった文章として英語版を公開したいと思っています(未定)。

3月には「Android 13のMIDI 2.0をいじったり…」みたいなことを書いていたのですが、どうも現状でUSB MIDI 2.0しかサポートしていなくてMidiDeviceServiceではまだ使えないみたいなので、extensionsとpresetsの実装が終わったら、たぶん新しいaudio ports (channels)に踏み込んだりするんじゃないかなと思います(今書いてて気付いたけどportsって書くとaudio port / midi portの話なのかAndroidへの移植 = ports, portingの話なのか一見して分からないな? 前者の話です)。

オーディオプラグインのインスタンス生成スレッドに関する覚書

AAP (android-audio-plugin-framework) のホスト実装のひとつとして、JUCEのAudioPluginHostを利用しているのだけど、最近全面的に再構成しているホスティングAPIに基づいてJUCE統合(AndroidAudioPluginFormat)を書き直していて、だいぶ行き詰まった問題がある。プラグインを生成するスレッドの問題だ。結論からいうと、AAPやAU v3ではmainスレッド以外からプラグインを生成するか、あるいはmainスレッドをブロックしないことが求められる。mainスレッドをブロックするのは大抵のプラットフォームではアンチパターンあるいは禁止となっているから、ロジックとしては妥当だとわかってもらえるとは思う。

AAPを設計しているのは他ならぬ自分自身なので「求められる」などと他人事みたいに書いているのは不自然に見えるのだけど、自分の趣味で求めているわけではない。

AAPはホストとはAndroidプラットフォームとエコシステムの構造として必然的に別アプリケーションとなり、インスタンスの生成においてはプラグインAndroid ServiceとしてApplication.bindService()を呼び出して生成する必要がある。bindService()を呼び出した後、その結果はそのメソッド呼び出しで引数として渡したServiceConnection(インターフェース)の実装のonServiceConnected()メソッドがコールバックされるという流れで渡される。このonServiceConnected()はメインスレッドで実行されることになるので、メインスレッドが他のコードでブロックされていると、onServiceConnected()にお鉢が回ってこないことになる。

自分の場合、メインスレッドでkotlinx.coroutines.channels.Channelwait()で待機させておいてonServiceConnected()signal()を送るようになっていて、一生先に進まない状況に陥っていた。

juce_audio_processorsを使う場合の注意点

JUCEを使ってオーディオプラグインインスタンスを生成するコードを書いている場合、そのインスタンス生成にはAudioPluginFormat(Manager)::createPluginInstanceAsync()を使うことが期待される。createInstanceFromDescription()は同期APIなので、これを使うと生成結果が返ってくるまでその実行スレッドがブロックされることになる。

std::unique_ptr<AudioPluginInstance> AudioPluginFormat::createInstanceFromDescription (
        const PluginDescription & ,
        double initialSampleRate,
        int initialBufferSize,
        String & errorMessage)

void AudioPluginFormat::createPluginInstanceAsync (
        const PluginDescription & description,
        double initialSampleRate,
        int initialBufferSize,
        PluginCreationCallback)

AudioPluginFormat(Manager)::createPluginInstanceAsync()にはひとつ注意すべき事項として、実際のプラグイン生成処理が内部的にMessageManagerを経由してmainスレッド上で行われてしまうという問題がある。つまり、たとえ自分のホストアプリケーション上ではmainスレッド上でプラグイン生成されないように(juce::Thread等を使用して)別スレッドでこの関数を呼び出していたとしても、実際のインスタンス生成処理はmainスレッドで走ってしまう。

もしプラグインフォーマットのホスティングAPIを実装できる立場であれば(筆者はそう)、mainスレッドでのインスタンス生成を「禁止」しないようにできるが、そうでなければ、juce::AudioPluginFormatを自ら実装して、createPluginInstanceAsync()の実装の中でさらに別のnon-mainスレッドに処理を非同期に実行させる必要があるだろう。

プラグイン・サービス接続処理とインスタンス生成処理の切り離し

オーディオプラグインインスタンス生成はMIDIのポート接続と似ている側面がある。1つのMIDIバイスに接続して複数のMIDIポートを利用することがあり得るように、同一のプラグインの複数のインスタンスを生成するのは一般的なシナリオだ。

Androidアーキテクチャの場合、別アプリケーションのServiceに接続するのは1つのBinderでも、そこから複数のプラグインインスタンスを生成して利用するという構成になる。Android MIDI APIの場合、MIDIバイスへの接続はMIDI_SERVICEへの接続という非同期処理から始めることになるが、いったん接続したMIDIバイスのポート接続は同期的に開くAPIになっている。

仮想MIDIバイスであれUSB/BLEデバイスであれ、対象Serviceとのやり取りはIPC(Androidの場合はBinder)ベースになるので、ポート接続も非同期であるべきではないかとも思えるけど、いったん接続したMIDIバイスとのやり取りにはrealtime IPCが活用され得るため、スレッドの切り替えを伴う非同期APIではむしろ適切ではない可能性がある。

これと同じことがオーディオプラグインインスタンス生成についても当てはまるといえそうだ。AAPの場合は、接続処理は必然的に非同期にならざるを得ないが、すでに接続済みのAudioPluginServiceであれば同期的にインスタンス生成を行える。Androidフレームワークでは現状AAPホストのような任意のアプリケーションにおいてrealtime Binderを生成する窓口が開かれていないようなので、あくまで一般的なpthreadの優先度でしか接続できないが、それでも非同期スレッドによる切り替えが発生しないほうが良いだろう。

3月の開発記録 (2022)

早いもので3月が終わろうとしています。2月の終わりから全く世界情勢が安定せず、誰もが落ち着かない雰囲気の中で日々を過ごすことになったと思います。自分も例外ではないところですが、多かれ少なかれ影響を受けつつも、自分の作業は進められるだけ進めておきたいところです。

AAP hosting overhaul

2月からずっとやっていて、今日3/31にようやくmainにマージできた作業です。

2月はAAPのnative (C++) hosting APIの仕切り直しに着手していましたが、3月も引き続き泥沼に入っていました。native hosting APIは現時点では (1) MidiDeviceServiceの実装と (2) aap-juce-plugin-host (JUCE AudioPluginHostの移植)の2つしかなく、aap-juce-plugin-hostは「起動時にインストールされているオーディオプラグインの全てをbindService()で接続する」という割とシビアな実装になっていました。これには当然ながら(?)理由があって、まずBinder APIに基づいてServiceと接続する必要があったため不可避だったのです。が、aap-juceに基づくプラグインの移植は今や↓のように大所帯になっていて(README.mdからのコピペ)、今やこれを全て接続するわけにはいきません:

そういうわけで、native hosting API仕切り直しにあたっては、native APIからServiceのAPI…を操作するAAPのKotlin API…をJNI経由で呼び出してBinderを生成する(そのBinderはJNI経由でネイティブコードに渡されてNative Binderとなるわけで多重に境界を往復するわけですが)…という仕組みを作りました。

fixing JUCE Thread

構想としてはシンプルですが、よくある話ながらAudioPluginHostが実行できるようにコードを書き換えて動作させてみたら全くうまくいきません。まず自分のライブラリのクラスをJNI経由で呼び出そうとしてもClassNotFoundExceptionで堕ちてしまう。JNIのClassNotFoundExceptionはJNIをそれなりに使ったことがある人なら、あるいはJNI経由で他言語とinteropするフレームワーク(XamarinとかReact Nativeとか)のユーザーであれば馴染みがあると思いますが、JUCEでも同じ目に遭いました。

この話は前回ここに書いたネタに繋がってきます:

atsushieno.hatenablog.com

ここにまとめたパッチは、その後この記事の英語版を作成してからJUCEへのPRとして登録したのですが(長らく「contributionは受け付けない」方針だったJUCEが1月にポリシーを変更していたのです)、現状音沙汰なしです(issueにしろPRにしろJUCEはだいたいcontribution体験が悪いのであんましお勧めしないです。JetBrainsとかは割とおすすめ。dotnetもそれなりにいいほうだと思います。)

JUCE AudioPluginHostをいじっている過程でこのパッチを作成したということは先のエントリ(日本語)でも言及している通りで、まずはこの問題を追及してパッチを作る作業でそこそこ時間が溶けました。

async-ify plugin instancing API

スレッディングの問題を片付けたら、いよいろJNI経由でContext.bindService()を呼び出してAudioPluginServiceとbindするコードを実装しました。

bindService()は呼び出してもすぐに返ってくるもので、実際にServiceに接続できたらServiceConnection.onServiceConnected()というメソッドにコールバックが返ってくるようになっています。そのため、JNI経由で呼び出すインスタンス生成のメソッドではkotlinx.coroutines.channels.Channelインスタンスを作って.wait()させておいて、onServiceConnected()のコールバックの中で.send()して続行させるコードを書いていましたが、これがまた案の定うまくいかずにハマりました。AudioPluginHostでaap-juceのjuceaap_plugin_clientモジュール経由でAAPの新しいnative APIプラグイン インスタンスを生成しようとしても、途中で返ってこなくなるのです。

しばらく調べてみると、ServiceConnection.onServiceConnected()は(生成スレッドを問わず)常にmainスレッドで呼び出される仕組みになっていました。JUCEのアプリケーション・ループはmainスレッド上で動作しており、そこから呼び出されるインスタンス生成のネイティブ関数もmainスレッドで動いていたので、ここでブロックされていたわけです。

この問題も別エントリでもうちょっとだけ詳しくまとめようと思いますが、結局この問題を解決するために、AAPのAPIにasyncificationを施すことになりました。"asyncification" というのは一部で使われている非同期API化(あるいは非同期APIの提供)をあらわす俗語です。xxx()メソッドに対応するxxxAsync()を生やすやつです。AAPの場合、同期APIで実装すること自体が無理なので、非同期APIのみで作り直すことになりました。

今まで同期APIで設計していたものを非同期APIで作り直したので、内部的には大幅なコードの書き換えを伴うことになりましたが、差分はAAP本体とaap-juceのモジュールのレベルで吸収しているので、移植したアプリケーションのレベルでは書き換えが必要ないところまで収まりました(別途ヘッダファイルのパスの統一などを施しているので、AAP 0.6.xから0.7.0へのアップグレードでは書き換えが必要になっています)。

GitHub Sponsors set up

これは去年の話なのですが、わたしが一時期アプリ開発をちょっとだけ(本当にちょっとだけ)手伝っていたサンプラーアプリがあって、もともとはその開発者がAAPに興味をもって連絡を受けてやっていました。AAPは当時まだJUCEのエフェクトプラグインの移植が使い物にならなくて、「まだ…その時ではない…」と言っていたのでした。サンプラーでエフェクトプラグインが適用できないんじゃ利用機会が無いですし。

それで今は特に一緒にやっていないのですが、そのコミュニティで開発者にAAPを紹介されたという人がコンタクトしてきて、sponsorさせてほしいと申し出をいただけたので、ありがたい2〜と思いながらやりかけで放置していたGitHub sponsorsを設定していました。スポンサープログラムを始めるといろいろサポートを考えないと…となるところですが、あまり気負わずに運用していこうと思います。donationwareとして始めてしまうと仕事見つけづらくなるかな〜とか考えてしまいますし。

WeMove, ADC21動画鑑賞会

しばらく前からMusic Hackspaceというオンラインセミナーを運営しているところで無料のセミナーがあるとたまに参加しているのですが(最初はAndroid Audioだったような気がする)、最近#WeMoveというWomen in Audioのコミュニティと連携して25回の無料セミナーを毎週のように開催するという狂気のような企画があるので(言い方…!)たまに見ています。off-siteではUKの夕方にやっているのでリアルタイムで見られる人はだいぶ限られると思いますが、録画も上がっているので興味がある人はチェックしてみるといいかもしれません。

musichackspace.org

あと最近ようやく昨年のaudiodevcon 21の動画が上がってきています。1週間に1本みたいな感じでちょろちょろと出ていて、このペースでいつ終わるんだろう…?となっていたのですが、最新の動画を最後まで見ていて、見慣れない他のADC21動画へのリンクが出てきたので「あれ?」と思いながらクリックしたら、何やら非公開リストにすでに大量の動画が上がっているらしいことがわかりましたw そういうわけで、そのへんの動画を見ながら勉強する動画鑑賞会を企画しています。来週1度やってみる予定なので興味のある方はお気軽にどうぞ(2020年にやったMusic Tech Meetupのメンバーの承諾のもとこのグループで公開しています)。

music-tech.connpass.com

その他

2〜3月には他にもちょいちょい試したことがあるのですが、今のところボツネタばかりです。最近のボツネタだとBYODを移植しようとしましたが、全く動いていないです。うまくいかなかったVialFX(謎)とかもあるし、まあそういうこともあるよね〜という感じで草の生えていないgithubを眺めています。

4月の予定

native hosting APIの書き直しがざっくり終わったので、3月に公開されたAndroid 13 DP2Android MIDI APIMIDI 2.0対応でもいじって遊んでみたいという気持ちがあります。AAPのMIDI2対応もテコ入れし直しの機運ですね。

4月にはM3 2022春が開催される予定ですが、今回はサークル申し込みしませんでした。今回は入場規制がないので、まだ安全に開催できるのかちょっと未知数だなと思っていることが正直あります(特に音楽関係はソフトウェア業界と違ってリアル空間で生きている人が多いこともあってかそこそこルーズなので…)。せっかく休みにしているので、この機会(?)にAAPの作業を進めておきたいなと思っています。

JUCE on Android : fixing ClassNotFoundException on non-main thread

調査の端緒

JUCE on Androidには、メインスレッド以外ではJNI経由でAndroid frameworkとJUCEのクラス以外でまともにJNI呼び出しが行えないという問題が存在する。自分が初めての発見者というわけではないようで、JUCE forumに同様の報告がある。

forum.juce.com

これは(詳しい分析はここではすっ飛ばして)juce::ThreadAndroid NDKのドキュメントに沿ってjava.lang.Threadを適切に生成せずに処理を開始してしまうことに起因する。

https://developer.android.com/training/articles/perf-jni#threads

It's usually best to use Thread.start() to create any thread that needs to call in to Java code. Doing so will ensure that you have sufficient stack space, that you're in the correct ThreadGroup, and that you're using the same ClassLoader as your Java code. It's also easier to set the thread's name for debugging in Java than from native code (see pthread_setname_np() if you have a pthread_t or thread_t, and std::thread::native_handle() if you have a std::thread and want a pthread_t).

いったん処理を開始してしまうと、後からjava.lang.ThreadsetContextClassLoader()で適切なClassLoaderを設定することができなくなる。だからjuce::Threadの動作開始前(pthread_create()の呼び出し前)に対処しておく必要がある。

Android/Dalvik/ART編

OboeのオーディオスレッドでもJNI呼び出しが失敗する

ここまで調べて気がついたが、このような問題はJUCEのみで生じるわけではない。たとえばOboeはネイティブ(C++)オーディオAPIであり、AudioStreamでコールバックスレッドからonAudioReady()を呼び出す仕組みになっている。このコールバックスレッドはOpenSLESあるいはAAudioが内部的に生成していて、その実体はpthread_create()で生成されている。ここにはJNI・JVMが一切絡んでこないので、後からJNIEnv*JavaVM::AttachCurrentThread()で取得できたとしても、そのスレッドに対応するjava.lang.ThreadにはClassLoaderが適切に設定されていないのではないか。

この仮説に基づいてひとつ実験してみた。Oboeのhello-oboeサンプルに手を加えて、JNIEnv*を取得してFindClass("androidx/constraintlayout/widget/ConstraintLayout")を2箇所で呼び出してみた。ひとつはJNI呼び出しを実装する関数の中で、もうひとつはOboeのonAudioReady()コールバック実装の中である。わざわざConstraintLayoutクラスを指定しているのは、これがclasses.dex(いわゆるmain dex)に含まれていないであろうという前提に基づいている。(d8が同一のclasses.dexにまとめるような動作になっていたら必ずしも成立しないが。)

https://gist.github.com/atsushieno/8e6b0bc82005cdeef65254f9e8bcec1b

果たして予想通り、JNI呼び出しの中ではFindClass()はnon-nullなjclassを返し、オーディオコールバックの中ではnullptrを返した。オーディオコールバックスレッドからJNIをまともに使うのは無理そうだ。

Oboeとしてはこれは想定されている動作であって、公式のgithub discussionsにもそのような議論がある。

github.com

実際的な意味では、オーディオコールバックはリアルタイムに処理が行われることが期待されている。JNIで操作されるART VMはリアルタイムセーフではないので、ARTに何らかの動作を期待するのであれば、atomic operationを使ったnon-thread-localな共有メモリの読み書きなど、何らかの間接的な手段が必要になりそうだ。

これはもちろんOboeが暗黙的に想定するリアルタイムのオーディオスレッドという文脈だからJNI非サポートが正当化できるのであり、non-realtimeなjuce::ThreadなどでJNIをサポートできないことを正当化することはできない。

AAudioのrealtime audio threadはpthread

調べた順序は書いた順序と逆になるが、OboeというかAAudioの中でコールバックスレッドがどのように作られているか、JUCEの問題と性質が同じなのではないかと思って調べてみた。これは、概ねpthread_create()から逆引きして突き止めた。ここではcs.android.comのシンボル参照を辿れるCall Hierarchyという機能が便利だ。

pthread_create()を呼び出すcreateThread_l()のCall Hierarchy この検索結果はAAudioのソースを指している。OboeはOpenSLESとAAudioへのアクセスを共通化する、ある意味(非常に狭い)クロスプラットフォームAPIであり、その実体を追及する意義はあまり無い。AAudioだけ追及すれば概ね足りるので、OpenSLESのほうは追及していない。

https://cs.android.com/android/platform/superproject/+/master:frameworks/av/media/libaaudio/src/core/AudioStream.cpp;l=476;bpv=0;bpt=0

ちなみにほぼ余談だけど、このスクショのようにコールグラフを追ってみた感じ、AAudioの実装はAAudioServiceで、AIDLで規定されたプロトコルに沿って操作しているようだ。BinderはAndroid 8.0でリアルタイム処理も実現できるように改良されていて、オーディオ処理でも使えるようになっている(ただし/dev/binderには開放されていないようだ)。中にはframeworkのコードなのにOboeServiceなどという名前も使われていたりして(frameworkのほうはAAudioが適切なはず)、歴史が垣間見える。

https://cs.android.com/android/platform/superproject/+/master:frameworks/av/services/oboeservice/AAudioServiceStreamBase.cpp

解決策: java.lang.Threadを使う

冒頭で引用したAndroid NDKのドキュメントにあった通り、java.lang.Threadを使うのが正解だとしたら、JNIに正しく踏み込めるような処理をC++のnon-mainスレッドで行いたい場合は、java.lang.Threadを生成して、そのAPIを操作するスタイルで実装するのが正解だろう。実行するRunnerの中で本来スレッドにやらせたかったネイティブはJNI経由で呼び出せるし、もし必要があればその処理の前にpthread_self()を呼び出せばそのスレッドのpthread_tハンドルも取得できる。ただ、java.lang.Thread側にも固有の状態変数が存在しているので、java.lang.ThreadAPI経由で操作するほうが安全ではある(たとえばAndroidのojluni実装の場合、setPriority()ではネイティブ設定の他にjavaフィールドの値も設定するし、getPriority()ではそのフィールドの値が返される)。

もっとも、スレッドの中止に関しては、Androidに関してはpthread_cancen()が存在しないので、java.lang.Thread.stop()もまともに実装されておらず、これに関して心配する意味は無い(!) スレッドに渡す処理のループ条件を適切に管理して「正常終了」させるのが正しい実装アプローチだ。

pthread_create()でスレッドのpthread_tを取得してから、それをもとにjava.lang.Threadインスタンスを生成する方法は無い。

java.lang.ThreadはどこでClassLoaderを正しく設定しているのか

JUCEもAAudioもなまのpthreadだからClassLoaderが設定できていないらしいことはわかったが、ではどこでどう処置すればjava.lang.Threadは想定通りのClassLoaderが設定できるのだろうか。

先に結論だけ書くと、実のところjava.lang.ThreadsetContextClassLoader()はいつでも呼び出し可能になっている。start()の時点で(のみ)その設定値が参照され、以降は単に無視されるということだろう。そう理解して深くは追及していない。

もう少し技術調査したい人向けにいくつか情報を補足しておく(今回の調査の目的からは外れるので自分では深入りしていない):

  • 正しいClassLoaderが設定されていないと、apkに含まれるclasses*.dex(classes.dex, classes1.dex, classes3.dex, ...)を期待通りにロードしてclassを解決してくれない。これはどうやらARTでも変わらない。(getSystemClassLoader()については後で言及)
  • java.*API実装は、Android 24以降はApache Harmonyからopenjdk libcoreベースの実装に切り替わっており、Android固有の部分はojluniというART向けの実装と組み合わされている部分が多い。
  • JNIにおいてClassLoaderはthread localの領域に保存される情報なので、スレッドごとにこれを保持しておく必要がある。
  • JNIにおける各種呼び出しはJNIEnvを通じて行うが、これはJNIのエントリーポイントの実装などで引数として渡されでもしない限りは、JavaVM::GetEnv()なりJavaVM::AttachCurrentThread()なりを使用して、スレッド別に取得(・生成)しなければならない存在だ。
  • AttachCurrentThread()呼び出し時にそのネイティブスレッドに対応するjava.lang.Threadが存在しない場合は生成される。この時点で生成されるjava.lang.Threadで名前から参照解決できるクラスは限られている。java.lang.*などのランタイム型は解決できそうだが、少なくともapk内に複数あるclasses*.dexの全てをロードして解決してはくれない。ARTなのでClassLoader.getSystemClassLoader()で返されるClassLoaderが複数のclasses*.dexに対応している、というわけではなさそうだ。
    • system ClassLoaderは素朴なPathClassLoaderで(parentはBootClassLoader)。mainスレッドで返されるClassLoaderも非mainスレッドで返されるClassLoaderも違いはない(同一インスタンスかどうかは未確認)。

3/14追記: 「mainスレッドで返されるClassLoaderも非mainスレッドで返されるClassLoaderも違いはない」と書いたが、そもそもgetSystemClassLoader()で返されるClassLoaderはdex分割に対応した完全体のClassLoaderではないようだ。試しにJava.initialiseJUCE()を呼び出す部分(Projucerが生成するJuceAppでもいいし、自作のコードでもいい)で次のようなコードを実行してみよう(Kotlin):

        javaClass.classLoader.loadClass("com.rmsl.juce.Java")
        ClassLoader.getSystemClassLoader().loadClass("com.rmsl.juce.Java")
        com.rmsl.juce.Java.initialiseJUCE(context.applicationContext)

実行してみると、2行目でClassNotFoundExceptionになることだろう。これは、system ClassLoaderでは不十分だということを示している。

JUCE編

JNIClassBaseの概要: Java Interopの必要性

JUCEでは内部的にAndroid APIと相互運用しなければならない部分が少なくない。たとえばjuce_audio_devicesmidi_ioAPIを実装するにはandroid.media.midiを使う必要がある(Native MIDI APIはminSdkVersion 29になるのでJUCEのようなフレームワークが採用できるものではない)。そもそもjuce_gui_basicsが利用するjuce_eventsのアプリケーションループもandroid.app.Activityandroid.app.Application.ActivityLifecycleCallbacksAPIを使わないと操作できない。

JNIを利用すれば、少なくともmainスレッドから有効なClassLoaderが提供されている範囲では、これらを実現することは可能だが、毎回JNIのboilerplate codeを書くのはしんどいので、JUCEでは内部実装用にJava APIに対応するJNIクラスをマクロで短いコードで定義・利用できるような仕組みが作り込まれている。これはJNIClassBaseというC++のコードを用いて実装されている。この仕組みを使うと、AndroidフレームワークJava APIの仕組みの上でJNI経由で動作するC++コードを、比較的シームレスに作成できるようになっている。

これはJUCEの外側でも利用できないわけではないが、外部向けに作られたものではないので(少なくともAPIリファレンスには出現しない)、利用するなら将来的な破壊的変更もあり得るという認識が前提になる。

もうひとつ、JUCEではC++Javaのインターフェースやクラスのメソッドのオーバーライドを実装しなければならない場面がある。この仕組みはそれなりに高度で、やっていることはAndroidと他言語のバインディングとあまり変わらない。Xamarin.AndroidJava Binding、React Nativeのbridge、FlutterのPlatform Channel、そういった機構の内部を知っていれば理解しやすいだろう。バインディング自動化機構は無いし、様々な部分を手書きで実装する必要があるので、最小限のランタイムという感じだ。

ランタイムの中心となる実装はjuce_core/native/juce_android_JNIHelpers.hおよび.cppに定義されている。

JNIClassBaseを使用したJavaクラス(のproxy)の定義

JUCEのC++コードで操作されるJavaオブジェクト(のうち、このバインディング機構で制御されるもの)は、そのクラス定義をJNIClassBaseで宣言されている。DECLARE_JNI_CLASSDECLARE_JNI_CLASS_WITH_MIN_SDKDECLARE_JNI_CLASS_WITH_BYTECODEといったマクロを利用して宣言することになる(これらの違いは後述するが、どれも全て最後のやつに行き着く)。

juce_android_JNIHelpers.hで定義されているJavaClassLoaderクラスを例に、いくつかのポイントを箇条書きで説明しよう:

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \  
 METHOD       (findClass, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;") \  
 STATICMETHOD (getSystemClassLoader, "getSystemClassLoader", "()Ljava/lang/ClassLoader;")  
  
  DECLARE_JNI_CLASS (JavaClassLoader, "java/lang/ClassLoader")  
#undef JNI_CLASS_MEMBERS
  • DECLARE_JNI_CLASSマクロの最初の引数は、定義されるクラスとその名前で定義されるstaticなグローバル変数の名前になる。
  • メンバーの定義にはJNI_CLASS_MEMBERSという名前のマクロが使われる。このマクロはその内容自体が現在定義しようとしているクラスのメンバーを列挙するかたちで「定義」し、DECLARE_JNI_CLASS_*マクロからメンバー定義するときに呼び出されるもので、このマクロによる定義・宣言が済み次第#undefで消される(消すべきものだ)。
  • メンバー定義は、フィールドとメソッド、それからコールバック関数の類がstaticとnon-staticで別々に定義される。JNI_CLASS_MEMBERSを実際に呼び出すのはDECLARE_JNI_CLASS_WITH_BYTECODEマクロであり、METHODSTATICMETHODといったマクロ引数の実際の内容はこのマクロから与えられるので、このマクロを利用する側が気にする必要はあまりない。マクロなので内容のチェックは期待できない。
  • これはjava.lang.ClassLoaderクラスのインスタンスを操作するときに、JavaClassLoader.getSystemClassLoaderJNIEnv::CallStaticObjectMethod()の引数に指定するときに有用だ。

JNIClassBaseのBLOB

JNIClassBaseではバイトコードuint8[]定数のBLOBで保持していることがある。

static const uint8 invocationHandleByteCode[] =  
{31,139,8,8,215,115,161,94,...,12,5,0,0,0,0};  
  
(..)

DECLARE_JNI_CLASS_WITH_BYTECODE (JuceInvocationHandler,
   "com/roli/juce/JuceInvocationHandler", 10, invocationHandleByteCode, sizeof (invocationHandleByteCode))

これは実はdexバイトコードを抜き出してgzip圧縮したもので、これが指定されている場合、JNIClassBaseInMemoryDexClassLoaderあるいはDexClassLoaderを使ってこの配列をClassとしてロードする(InMemoryDexClassLoaderを使うか使わないかは、実行時のSDK versionで決まり、in memoryでない場合は一時ファイルに出力する)。どのBLOBにも、対応するjavaソースがJUCEのソースツリーに含まれているはずで、ソースコードの隠蔽などを目的としているわけではない(はずだ)。

(非Android開発者向けに多少入門的な話を書くと、Android*.javaのソースをコンパイルすることはあっても、*.classJavaバイトコードはapk/aabのビルド時にclasses*.dexというDalvikバイトコードに変換してあり、実行時にClassLoaderでロードできるのも*.classファイルではなく*.dexだけだ。)

独自のdexバイトコードは、JUCEで何らかのJavaクラスを提供しなければならない場面で、*.javaソースコードAndroid Studio / Gradleでビルドする代わりに、C++コンパイルだけで完結できるように作られているようだ。その技術選択にはかなり議論の余地があるが、現状ではそうなっている。

今回の調査の成果のひとつとして、BLOBを全て削り落として、代わりにbuild.gradle(.kts)でJUCEの*.javaソースから必要なコードをコンパイルする方式のパッチを作った。 https://gist.github.com/atsushieno/1ab9f9d4a1f9119db1d2ac88b4257bcb

BLOBを使用する代わりにJavaソースコードをビルドするアプローチにした場合、build.gradle(.kts)上でそれぞれのBLOBに相当するJavaソースをコンパイル対象として明示的に指定する必要がある。どのモジュールを選択しているときにどのディレクトリを追加する、といった処理をProjucerやCMakeモジュールに追加するか、あるいは全てのモジュールのJavaコードを全てコンパイルする必要がある。後者のほうがずっとシンプルだが、ソースコードによっては追加の依存モジュールが必要になる。たとえばjuce_product_unlockingではfirebaseのモジュールをdependenciesに追加する必要がある。これは割とニッチなモジュールで、通常は必要ない。必要ないものをdependenciesに常に指定するのはあまり健全ではない。

いや、そもそもJUCEは今でもCMakeをAndroidでサポートしていないんだった。CMakeを使えるようにするには、自前でAndroidプロジェクト全体を用意するアプローチになる。それであれば、使用するJUCEモジュールに合わせてbuild.gradle(.kts)を調整するのは全く難しくない。それすらやりたくないということであれば、Projucerのbuild.gradle生成をちょっと工夫させるだけでも良いだろう。

もちろん、BLOBを利用するのはビルドパフォーマンスの向上のためとは言い難い。これらBLOBがあろうと無かろうとJuceApp.javaなどが存在する以上、javaコードのコンパイルは発生するし、ほとんどの場合そのコンパイルは一度だけしか起こらない(何しろコードが書き換わらないので)。

JNIClassBaseの実行時利用

JNIClassBaseはcom.rmsl.juce.JUCE.initialiseJUCE()あるいはThread::initialiseJUCE()が呼び出されたときに(前者は後者を呼び出すので実質的に同じ)、JNIClassBase::getClasses()関数のstatic storageにあるArray<JNIClassBase> classesに含まれている全てのクラスをそれぞれのinitialise()でロードする(DECLARE_JNI_CLASS_*マクロを使うと自動的にこのArrayに登録される)。この時点でもしバイトコードが定義されていたらロードされ、メソッドの引数型など依存クラスがあれば全て芋づる式にロードされる(はず)。

JNIClassBase::initialise()java.lang.Classをロードする手順はいくつか条件分岐があり多少コードが長いが、ポイントだけ押さえると、dex BLOBを含むJNIClassBaseのロードには、InMemoryDexClassLoaderなどが使用される。実際に行われているのはJNIでClassLoaderによるクラスのロード(の試行)程度の内容だ。

JUCEで宣言されていないクラスをJNIClassBaseで宣言する

#include <juce_core/juce_android_JNIHelpers.h>を書けばどこでも宣言できるはずだ。正しく記述できなければ実行時エラーになるところまではできた。

今回の調査の一環で、pthreadの代わりにjava.lang.Threadを使ってjuce::Threadを書き直したパッチを作成したが、この中でJavaLangThreadというクラスを独自に定義している。これは最終的にjuce_android_JNIHelpers.hに直接追加したが(Threadの実装はjuce_coreにあるため)、自分のアプリケーションのソースに置いても、シンボルが解決できる限りビルドできるだろう。

AndroidInterfaceImplementer

さて、この節の冒頭で言及したもうひとつの課題として、JUCEのJNI interopではJavaのインターフェースやクラスのメソッドのオーバーライドをC++で実装しなければならない状況がある。たとえばjava.lang.Threadのコンストラクタにはjava.lang.Runnableを渡す必要があるが、自分でこのインターフェースを実装してrun()でJUCE/C++のコードを実行したい場合は、Runnableの実装が必要になる。

3/15追記: 初出時「インターフェースやクラスのメソッド」と書いたが、抽象クラスの実装として使えるようには見えないので取り消し線を追加した。Xamarin.AndroidでもXxxImplementerとXxxInvokerは別物だ。

このような場合に、足りないJava実装をどうやって提供するか、いくつか方法が考えられるが、JUCEではjava.lang.reflect.Proxyjava.lang.reflect.InvocationHandlerという仕組みを活用して、Javaコードのビルド時生成を一切行わないかたちで実現している。

Proxyは、Javaオブジェクトとして振る舞うが、名前と引数型からメソッドを呼び出すときに、Proxy生成時に渡されたInvocationHandlerの実装のinvoke()メソッドを使う:

abstract Object invoke(Object proxy, Method method, Array<Object> args);

このInvocationHandlerは、java.lang.reflect.Proxy.newProxyInstance()に引数として渡される。

public static Object newProxyInstance (
    ClassLoader loader, Class[]<?> interfaces, InvocationHandler h);

これで生成されたObjectは、メソッド呼び出しをInvocationHandlerによって解決するので、Javaインターフェースの実装として機能するというわけだ。

JUCEの具体的な実装としては、JUCEInvocationHandlerというInvocationHandlerの実装が、JavaインターフェースをC++で実装したクラスで用いられている。javaコードで実装されていて、例によってdexバイトコードjuce_android_JNIHelpers.cppに埋め込まれている。JUCEInvocationHandlerは実装の詳細だが、invoke()の中でネイティブコードにコールバックするだけなので、われわれこの機構のユーザーがこのクラスを意識する必要は、通常はない(ただクラッシュ時にstacktraceに出現するので気になるだろう)。

このソースファイルの中には、CreateJavaInterface()という関数が定義されている。この関数がjava.lang.reflect.Proxy.newProxyInstance()を(JNI経由で)呼び出してProxyオブジェクトを返す。返されたjobjectのProxyはそのままJNIで利用できるというわけだ。CreateJavaInterface()にはいくつかオーバーロードがあるが、ひとつだけ引用しよう:

LocalRef<jobject> CreateJavaInterface (AndroidInterfaceImplementer* implementer,  
  const StringArray& interfaceNames,  
  LocalRef<jobject> subclass)

ここで初出のAndroidInterfaceImplementerクラスは、JavaインターフェースのC++実装で使われる基底クラスになる。この仕組みを利用してJavaのabstractメソッドを実装するときは、このクラスのinvoke()関数をオーバーライドする。たとえばjuce_android_Files.cppにあるMediaScannerConnectionClientクラスがどう実装されているか引用しよう:

class MediaScannerConnectionClient : public AndroidInterfaceImplementer
{
    (...)
    jobject invoke (jobject proxy, jobject method, jobjectArray args) override
    {
        auto* env = getEnv();

        auto methodName = juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName));

        if (methodName == "onMediaScannerConnected")
        {
            onMediaScannerConnected();
            return nullptr;
        }
        else if (methodName == "onScanCompleted")
        {
            onScanCompleted();
            return nullptr;
        }

        return AndroidInterfaceImplementer::invoke (proxy, method, args);
    }
};

このinvoke()java.lang.reflect.Proxy.invoke()の実装とほぼ同じ役割を担っている。ここでメソッド呼び出しを自前で解決できるので、この機構のユーザーはC++コードを書くだけでJavaインターフェースを実装できるというわけだ。

まとめ: java.lang.ThreadでJUCE::Threadの実装を差し替える

さてそろそろまとめ…というか本題に戻ろう。今回の調査の最終的な目的は、JUCEのnon-mainスレッドからでもJNIを問題なく呼び出せるようにコードを作り変えることにある。

Android/Dalvik/ART編でざっと見てきた通り、C++コードでpthread_create()を呼んでしまうと、それをjava.lang.Threadに紐づけ「かつ」ClassLoaderを正しく設定する、ということは出来なそうだ。それであれば、pthread_create()している部分で代わりにjava.lang.Threadを生成し、そのRunnable引数の中で先のAndroidInterfaceImplementerによるProxyを使って、ネイティブコードの中でpthread_self()を呼び出してpthread_idを取得することで、正しいClassLoaderをもつスレッドが生成できるはずだ。

という考え方に基づいてJUCEのパッチを作成した: https://gist.github.com/atsushieno/5e28f5a0a319dc24fa8a7579f826b3aa

まだ未完成だが(setPriority()などが実装されていない)、とりあえずはこれでClassNotFoundExceptionは発生しなくなった。

実際にはこれは2つのパッチの組み合わせになっている

後者は単にBLOBローダーの仕組みを取っ払ったパッチなので、本当は前者だけで動いてほしいのだけど、まだ未調査の原因でSEGVになる。この仕組みを取っ払った以上、build.gradleを書き換える作業も必要になるのだけど、そこまでは出来ていないし自分のAudioPluginHostの移植でも自動書き換え機構が必要になるかどうかわからないのでProjucerなどは何も変更しておらず、upstreamに取り込める状態ではない(JUCEチームはいずれにせよ基本的にパッチを受け付けないので、彼らが自分で実装する必要があるだろう)。

3/14追記: Android/Dalvik/ART編の追記に繋がる話だが、JUCEではgetSystemClassLoader()を使っている部分がある。これは上の追記部分で説明した通りダメなやつなので、これを使っている部分で適切なClassLoaderを使うことで、BLOBローダーの仕組みを残したままバグを解消できた。

これと上の1つ目のパッチならば、プロジェクト生成で抜本的な変更を必要としないので、JUCEにも当てやすいパッチになるだろう。

2月の活動記録 (2022)

先月ちらっと書いたのですが、今月はAndroidオーディオプラグインの抜本的な仕切り直しを計画していました。

今年は完全個人プロジェクトを卒業して他の人にも使ってもらえるようなところまで持っていきたいという気持ちがあります。去年も1人で続けていましたが、だんだん個人の開発リソースでは足りない/主要な開発作業が進められなくなりつつあります(LV2とJUCEのプラグイン移植をAAPの最新版に追従させるだけでもそれなりの仕事になるのです)。Androidオーディオアプリの開発者の人たち(希少種!)からたまに「手伝えることがあれば言ってくれ」と言ってもらえてありがたいのですが、他人を巻き込むためには、既存コードはあまりにもスパゲッティすぎるのと設計がぐちゃぐちゃなのと、だんだん開発を進めるうえでブレーキになりつつあったので、一度やっておくべきと思った次第です。

仕切り直しを「計画していた」と過去形になっているのは、とりあえず現時点ではプラグインフレームワークとしての互換性が維持された範囲でリファクタリングを行っているためです。一番過激な仕切り直し構想では、新規リポジトリ(未公開)にGUI統合なども考慮した新しいモジュール構造を作って既存コードをビルドできるところまでやっていました。ただこれは現在リファクタリング中のソースコードと大きく乖離してしまっていて、再利用されることはないでしょう。

AudioPluginMidiDeviceServiceの安定化

AAPをオーディオプラグインAPIとして他の人に使ってもらえるようにするには、APIがまだまだ安定しませんし、実のところリアルタイムで安定的に使えるほど実装の品質も良くありません。そういう現状でAAPを使ってもらえるようにするには、現状2つの方向性があると思っています。

  • 仮想MIDIバイスとして利用できるようにする: これならAndroid MIDI APIの範囲でしかいじられないので、APIの安定性を気にする必要がほぼありません。できればプリセットまでサポートしたいところです。
  • サンプラー等で静的にエフェクトをかけられるようにする: リアルタイムで厳しくても静的なら問題ないだろうし、それでも一定の需要はあるだろうという考えです。基本的なAPI安定性が必要になるのと、UI無しでどこまで利用可能性があるのかやや未知数なことがあります(これもプリセット次第かもしれない)。GuitarMLみたいにモバイルでのリアルタイム利用が現実的でなさそうな分野では有用かもしれません(まだ移植したことがない)。

ただ、MIDIDeviceServiceの実装が、1月時点では非常に不安定で、「他のMIDIバイスに切り替えたら落ちる」「2回インスタンス生成したら落ちる」みたいなレベルだったので、コードの改善が急務でした。現在ではこれらの問題は解決していますが、それはネイティブコードにそれなりに抜本的に手を加えて実現したものです。

ネイティブ実装まわりはしばらく真面目にデバッグする必要がなかったこともあって、開発体験が非常に悪かったのですが(NDKとAndroid Studioの完成度に大きく依存するし…!)、今はデバッグビルド用にAddressSanitizerなどを利用しやすく整備したので、少なくともそこでやる気をフルブーストする必要はなくなりました。本当はAndroid NDKのバージョンをr21からr23に上げたかったのですがNdkBinderまわりのリグレッションに当たってしまい、修正されるまで棚上げです…

理想的な安定化が実現できたかというと、長大なリソースロード時間を要するプラグインなどがまだクラッシュする問題などがあって、他の人に「使ってくれ〜!!」と言うにはまだ片付けるべき作業があると思っています。(ただ先に後述の課題を片付けたいところ…)

aap-lv2-string-machine

去年は雨後の竹林のようにプラグイン移植のリポジトリを生やしていたものですが、今年はなるべく控えめにしようかな…と思っていました。が結局今月もひとつ作ってしまったり…(!?)

AAP-LV2の移植まわりで長らく問題になっているのがdragonfly-reverbの移植がまともに機能しない問題です。オーディオ処理した結果の波形がめちゃくちゃになったり音が何も聞こえなくなったり…。それで、まず問題を切り分ける目的で、dragonfly-reverbが使っているプラグイン開発フレームワークであるDPFを使った他のプラグインとしてstring-machineを移植してみました。2時間とかからずに移植できて問題なく動いたり…

github.com

そんなわけでDPFには問題がないことがわかったので、もう少し別の角度から検証が必要そうです。DPFプラグインはビルドがMakefileなので、どうしてもandroid-native-audio-buildersでバイナリビルドせざるを得ず、Android Studioからデバッグどころかprintfデバッグすらできないのが難儀なところです…

AAP 2022 roadmap and design docs

今月の半ばに、2022年版の開発ロードマップを公開しました。実のところ半分くらい2021年版から引き継いでいますし、1年かけてこれを進めるとか、逆にこれ全部やるのに1年かける予定だとか、そういうものではないです。他にもやることあるだろうし。

github.com

designing new ports and parameters

これで、まずは地味だけど重要な部分として、プラグインのポートとパラメーター(現在存在しないコンセプト)から着手しようと思い、新しいポート設計の構想から練り直したのですが、すでにいくつかissueとして登録してあったのをまとめるだけで割としんどい作業でした。しかも1年以上ずっと残っていたり…

github.com

AAP開発当初は、オーディオプラグインのポートはとりあえずLV2と同じ方式にしておけば良いだろうと考えていました。オーディオプラグインのコードに手を出し始めたのも4年くらい前からだったと思います。それからそれなりに経験を積んで、プラグインのパラメーターは何百もあったりするからポートを何百も作るのではなくパラメーターとしてちゃんと作ったり、設定にはlv2:patchみたいな新しい仕組みでやり取りすべき、みたいなことが分かってきました(この知見自体LV2コミュニティでも割とモダンな知識)。

パラメーター変更を受け付ける手段は、デスクトップのオーディオプラグインフレームワークでもGUIを含むイベントメッセージングの統合部分で課題になります。AAPの場合は(長らく着手できていない)GUI統合がプロセスをまたぐこともあり、またAndroidのServiceとしてクライアント(GUI)から接続することを考えると、接続は1つだけど対象インスタンスもクライアントも複数あって…といったモデルを考えなければなりません。

そんなわけで新しく設計ドキュメントをまとめたわけですが、コレに着手する前にやることがある。そう…

ネイティブAPIの破壊的な整備

まずネイティブAPIがスパゲッティなのをどうにかしないと…となったわけです。スパゲッティになっていることにはいくつか歴史的な理由があるのですが、やはり去年まででいろいろプラグインを移植してみて分かってきたことが増えてきたのと、デスクトップとは根本的に設計が違うものを無理に合わせてきたことが積み上がってのことでしょう。これもとりあえず大掛かりな変更作業になりそうなので、先にドキュメントとしてまとめたのですが、長期的に残したい内容でもなかったのでissueで済ませています。

github.com

MIDIまわりのリファクタリングの際に、1年以上固定していたバージョン0.6.xを0.7.0に上げたところだったので、今のうちにいろいろ破壊的変更を加えてしまおうという気持ちでアグレッシブに手を加えています。ユーザーがいないうちにやってしまおうという感じです。あまりアグレッシブに変更すると自分でも大量の移植コードが追従できなくなるのでやりたくはないのですが、この辺はバランスですね…。この作業は現在進行形でmainにもマージされていないので、月を跨いで継続していく予定です。

1月の活動記録(2022)

2022年最初の活動記録です。といっても実のところ今月は書けるようなことがびっくりするくらい無いですね。というのは、昨年末に書いた話ですが、いじっていたemscriptenまわりのやつとか、他にも何点かあるんですが、ほとんど全滅状態で、あとAndroidオーディオプラグインも、抜本的な仕切り直しを計画しながら現行コードをでかめにリファクタリングしていたりなどして、まだ進行中だし今月ネタになるようなことではないです。

Vital batch-import-wavetablesブランチとopen-vital-resources

今月は割とVitalの音作りで遊びながらソースコードを読み解いたりしていて、それはそれで調べものの結果ちょっとした資料はできつつあるのですが、文章にするほどまとまってもいなくてアウトプットにはならない感じです。

ただ、資料とは別のアウトプットの方向性として、Vitaloidを作ったのと関連して、OSSビルドでも利用可能なプリセット集合を作らないといけないな…という気持ちになっています。Vitalの「プリセット」を構成するのは、.vitalプリセット定義ファイルの他に、.vitaltableというウェーブテーブル定義と.vitallfoというLFO波形定義があり、これらが何一つOSS版に含まれていません。

これでは困るので、少なくともウェーブテーブルくらいはフリーのwavファイルから利用できるようにしてみよう、と思って、いろいろ試行錯誤しました。そもそもウェーブテーブルシンセサイザーにおけるウェーブテーブルの何たるかも知らないところからスタートしたので、たとえば「最大2048サンプルで…」みたいなところから「Serumのclmヘッダに情報が…」みたいな話とかまで習得したレベルです。これをVitalのコードリーディングと合わせて進めました。

それでVitalのフォーラム等で紹介されているフリーリソース(主にSerum用など)をもとに、Vitalの.vitaltableファイルを生成するツールを作ろうと思ったのですが、VitalがWAVファイルをGUI上でインポートした時にやっていることを全て自前で外部のツールから再実装するのは面倒だということに気付いたので(しばらくいろんな言語で試してみてから…)、むしろVitalのソースコードに手を加えてバッチインポート機能(WAVのあるディレクトリを指定してロードしたら全部.vitaltableにしてくれるやつ)を作ろう…となって、結果独自ソースツリーが出来上がりました(コード変更量は大したことないはず):

github.com

これをもとにCC0(現状)で公開されているリソースを取り込んだリポジトリを作ってあります:

github.com

フリーのウェーブテーブルリソースのほうはどう調達したかというと、まずWaveEditというエディタで作成されたウェーブテーブルがCC0で大量に公開されているのを全部取り込んでいます。大量と言っても凄まじい量というわけではないので程よく豊富だと思います。同様にkimurataro.comのFree WavetablesもCC0でWaveEditからいい感じの量で作られているので、これも取り込んであります。

あと、これは手作業で作ったものですが、LFO定義が何一つ存在しないのはさすがに不便極まりないので、直線や三角波、擬似サイン波などの.vitallfoファイルをいくつか作成しました。擬似ランダムなどはまあみんな自作できるでしょう…(適当に作っても良いのですが)。

本当はFactoryにあるBasic Shapesに相当するもの.vitalプリセットなどを作って含めても良いのですが(単純波形の組み合わせでしかない)、key frameをどう配置するか等でそれなりに工夫が効いてくる可能性もあるので、もうちょっと機械的に生成できる何かにするか、独自プリセット素材を作るか、考えてからがいいかなあと思っています。

あとは、ここまで書いてきたことを、もう少し誰にでも分かるように説明する資料を完成させたいところですが、これはいずれ機が熟したら…という感じです。

12月の活動記録(2021)

12月の活動記録です。2021年の〜はめんどくさいのでやめました。毎月書いてるからただの繰り返しになりそうですし。

JzzMidiAccess

atsushieno/ktmidiはKotlin MPPでMIDIバイスに接続できるAPIを提供しています。その実装はプラットフォームごとに、かつOSごとに異なるというある意味地獄絵図のようなマトリックスになる…はずですが、Kotlin/JVMではRtMidiをJNAでアクセスしてWin/Mac/Linuxサポートをまとめて実現していたり(Linux用にはALSA実装も提供しています)、Kotlin/JSとKotlin/NativeについてはMIDIデータ(SMFとMIDI 2.0 UMP)を操作するAPIだけ使えるようにしていました。それでしばらく放置していたのですが、ふとCompose for Webでも遊んでみようと思って、それならその前にKotlin/JSで使えるMIDI Access実装を用意すべきだと考えたのでした。

Kotlin/JS実装で面倒なのは、browser環境とnodejs環境の前提がまるで違うということです。browserは要するにWeb MIDI APIで、nodejsの場合はrtmidiなどを使えば、一応最低限の機能は実現できます。ただ、この面倒な問題はKotlin/JS固有ではなく、少し探してみると、Webでもnodeでも統一的に扱えるAPIを提供しているjzzというライブラリがありました。開発元のJazz-Softは、Web MIDI APIのブラウザサポートがまだChromeにも無かった頃からブラウザプラグインで使っていた人なら覚えている人もいるかもしれませんが、あのブラウザプラグインがまさにJazz-Softです。

あとKotlin/JS、KotlinとJSのinteropがどうなっているのか、明確にドキュメントになっていないっぽい部分も多くていろいろ手探りしなければならないところがあります。たとえばJZZのMidiOutのsend()に渡すarrayはByteArrayでもIntArrayでもなくArray<Int>でないと実行時エラーになる…みたいなことになります。

もうひとつ、これはほぼ偶然なのですが、このタイミングでKotlin/JS実装に手を出したのは割と正解っぽい要素がひとつあって、今月リリースされたkotlinx-coroutines 1.6.0からsuspend funのテストが書けるようになっています。これまで書けなかったというわけです。詳しくはzennに書いています(っていうほど詳しくもないけど)。

zenn.dev

KSPで最速のコード解析・生成を実現する @ アンドロイド・アンサンブル(C99)

2年ぶりにコミケが開催されるというのでTechBoosterからAndroid同人誌の新刊が発行されたのですが、その中でKSPの記事を1本書いています。~Androidの本というかビルドシステムの本みたいになってるというのは内緒。~

techbooster.booth.pm

augeneのSystem.Reflectionを使ったXMLリアライザーの代替をKSPのコードジェネレーターで無理やり置き換えた体験をもとに書いたわけですが、MultiplatformでハマってissueやらKotlin slackやらでフィードバックしつつ解決したビルドのハマりどころに触れたり、そもそも全体的に存在意義が簡単にわかる仕組みではないのでその辺をかみくだいて説明する感じの内容になっています。

MML to MIDI 2.0 to DAW @Music LT & Modernize MML for 2022

12/14にMusic LTというイベントがあって、IoT LTという巨大コミュニティ(昔のAndroidの会が無数の支部の上部組織みたいになってたやつのIoT版)から派生したイベントだったのですが、IoT関係なくてもおkというのでLTで参加させてもらってきました。

iotlt.connpass.com

このスライドに沿ってしゃべっています。

speakerdeck.com

これに続いて(ホントは事前に出したかったのですが)、ひさびさにgithub.ioのほうでModernize MML for 2022という一連の記事を出しています。(1)から(5)まであります(!) 書くのが量的にたいへんだったし日本語ではここでちょいちょい活動記録として書いているので今回は英語のみです。

atsushieno.github.io

Vitaloid

これはいったん書いて出したので改めて書くことはほぼ無いのですが:

atsushieno.hatenablog.com

このスクリーンサイズをどうにかしたいなあと思ってJUCE本体のコード(juce_audio_plugin_clientStandaloneFilterAppあたり)をいろいろいじって試行錯誤しているのですが、この辺はvitalが独自にいじっている部分とかぶっているところでもあって、今のところうまく行ってない感じです。Viewportを独自に追加して内容を大きく表示するくらいはできても、縮尺がおかしかったりポインターイベントの座標が連動しなかったり…

あと昨日ふと思ってこっちでもやってみようと思ったのですがjuce_openglサポートが無かったのでその辺を追加して)動かしてみるところから必要なので、そこからさらにvital独自パッチも取り込んだビルドを作る…というキメラなビルドが必要になるので、ちょっとメンテナンスコスト高いな〜という感じです。まあjuce_openglが動いて出来そうならやると思います(期待値はまあ半々といったところ)。

onwards...

そんなわけで今年はVitaloidまわりをいじりながら年を越すことになると思います。来年はAndroidオーディオプラグインをもう少し進めていきたいですね。ではまた来年。