Q4の活動記録 / ネイティブコードを伴うAARパッケージ編成のベスト?プラクティス

10月後半から今日に至るまで最近の活動記録を流していなかったわけですが、実際引っ越したり錬金術変拍子ゲームをやっていたり技術記事を書いたりしていて、あんましコードを書いてはいませんでした。というのは半分くらいは本当で、半分くらいは、やっていたことが半分くらいは前回公開したAndroid NDKのPrefabに対応しようとしてポシャっていたからです。まあまあ光明が見えてきたので、半ばQ4の作業記録を兼ねてまとめておこうと思います。

目次


aap-lv2 0.1.3 releases

今なにやら自分のメインプロジェクトとなっているっぽいAndroid用オーディオプラグイン機構ですが、開発を快適に進めるために必要なパッケージの分割と参照のあり方をいろいろ模索していて、しばらくビルドのリファクタリングを模索していました。それが昨日ある程度着陸したので、一つの開発リリースバージョンとしました。

github.com

パッケージ分割と再編の背景

AAPの現状として、コアライブラリと「最低限これくらいはほしい」を実現するためのプラグインの移植を含めて、ざっとこんなものが今のビルド対象です。Kotlinの参照とネイティブの参照が両方あったりなかったりするのですが、細かく入れると見づらいので消しました(どっちにしろ「多い」って言いたいだけですし)。

  • core (android-audio-plugin-framework) : コア部分
    • androidaudioplugin : C++とKotlinのプラグイン用ライブラリ
    • androidaudioplugin-ui-androidx : プラグインリストや詳細表示に使われるKotlinのUIライブラリ(appcompatベース)
    • aaphostsample : Kotlinのホストサンプル、その2つのネイティブ実装(appcompatベース)
  • aap-lv2 : LV2プラグインを移植するプロジェクト
    • androidaudioplugin-lv2 : LV2の共有ライブラリとTTLファイルなどがあれば残りをコーディング不要でプラグイン化できる基盤ライブラリ
    • aap-mda-lv2 : LV2プラグインのリファレンス実装ともいえるmda-lv2プロジェクトの移植(開発中によくdogfoodingで使われている)
    • aap-ayumi : ごくシンプルなPSG音源をLV2化して取り込んだもの(開発中によくdogfoodingで使われている)
    • aap-guitarix : Guitarixの移植。これがあれば「ポピュラーなエフェクターがある」って言えそう
    • aap-sfizz : Sfizzの移植。これがあればSFZベースのさまざまな楽器が使える
    • aap-fluidsynth : Fluidsynthの移植。これがあればSF2/SF3ベースのさまざまな楽器が使える
  • android-native-audio-builders : 以上のプロジェクトで使われるネイティブコードだけをビルドするリポジトリ
    • serd/sord/lv2/sratom/lilv : いわゆるLV2 toolkitとなるライブラリ
    • mda-lv2 : mda-lv2のAndroid用ビルド(Androidではネイティブだけでは動作しないのでaap-mda-lv2に組み込む)
    • guitarix : GuitarixのAndroid用ビルド(これもaap-guitarixに組み込む用)
  • aap-juce : JUCEホストとプラグインを移植するプロジェクト
    • AudioPluginHost : JUCEに含まれるプラグインホスト。オーディオノードグラフ化して繋いだときの動作確認とかに便利(まだ正常動作するほうが少ない)
    • andes, SARAH, OB-Xd, Magical8bitPlug2, OPNplug : JUCEで作られた各種シンセプラグイン(aap-lv2のほうはシンセらしいシンセが無い)
    • AugenePlayer : tracktion_engineを使った楽曲プレイヤー(まだ一度も正常動作していない)

最初の最初はこれ全部が1つのandroid-audio-plugin-frameworkリポジトリに入っていたんですが、LV2とJUCEのプラグイン移植が増えてくるとすぐ無理になってきたので、aap-lv2とaap-juceに分割しました。これがどうやら1年前くらい。

今年はこれにGitHub ActionsでCIを設定して回ったのですが、けっこう大規模なビルドで、aap-lv2は30分くらい(一度60分くらいかかったのがあるのですがさすがにGitHub側の異常値っぽい)、aap-juceに至っては3時間かかっている状態でした。JUCEのほうは未着手なのでまだ長いままなのですが、さすがに待ち時間が長すぎるので何とかしようと思いました。

もうひとつの問題は、開発に際してプロジェクト参照のネストが深くなりすぎるところです。aap-fluidsynthのリファクタリング前のGradleプロジェクト構造はこんな感じでした。

  • aap-fluidsynth (in aap-lv2)
    • androidaudioplugin-lv2 (in aap-lv2)
      • androidaudioplugin (in android-audio-plugin-framework)
    • androidaudioplugin-ui-androidx (in android-audio-plugin-framework)

当時はaap-lv2がandroid-audio-plugin-frameworkとside by sideでチェックアウトされることが前提となっていて、ビルドの正しさを検証する意味では良くありませんでした(CIビルドを実行するタイミング次第でandroid-audio-plugin-frameworkの最新版がチェックアウトされてくる状態だった)。それでsubmodule化したわけですが、そうするとプラグイン移植ごとにさらにリポジトリを分割するとネストが一層ひどくなって、これが複数のプロジェクトで修正が同時に稼働するとなると、変更を加えたソースツリーが行方不明になりがちです。

このままの状態ではリポジトリを分割できない。というわけで再編策を練りました。

基盤ライブラリの参照を安定化する

プロジェクト全体がこれくらいの規模になってくると、コア部分の変更が直接アプリケーション側に影響する場面が少なくなり、またアプリケーション側も最新のコア部分に追従しなければならない場面も減ってきます。昔取った杵柄でいえば、monoランタイムの変更にgtk-sharpmonodevelopが追従する必要はないし、monodevelopのプロジェクトシステムを使ったIDEアドインとか、そもそもGtkアプリケーションのプロジェクトが追従する必要も無いわけです。

AAPのコア部分であるandroidaudioplugin.aarには今後も変更がどんどん修正されていくわけですが、基本的にはandroidaudioplugin-lv2.aarで対応するものであり、アプリケーション側が追従を迫られる場面はそうそう無いので、概ね参照バージョンを固定してしまっても問題がありません。(JUCE側のアプリケーション分割は別の大掛かりな作業が必要になるのですが、それは主にProjucerが生成するAndroidプロジェクトのテンプレートとどう向き合うかみたいな大きな技術的課題があるためなので、今後まとめて考えることにしています。)

そして、近年になってGradleのMaven Publish pluginAndroidプロジェクトでも現実的に使えるようになってきており、Mavenパッケージをローカルシステム上のリポジトリキャッシュ上に発行したりそこから参照解決したりできるようになっています(このGradleプラグイン自体はかなり昔からあるみたい)。

リファクタリング前のaap-fluidsynthプロジェクトの構造は、プラグイン移植の開発者にとってのコードの構造としてはいささかオーバースペックです。プラグイン開発を一般に呼びかける段階になったら、androidaudioplugin-lv2.aarはMavenリポジトリから取得されるべきです。build.gradleでmavenLocal()からMavenパッケージを参照できるかたちにすれば、自分みたいなコア部分からアプリケーションまで全体的にいじる可能性のある開発者でも、プラグイン移植のみの開発者でも、同じようなプラグインプロジェクトのコードが書けます。

変更箇所 リファクタリング リファクタリング
dependencies (build.gradle) implementation project(':androidaudioplugin') implementation 'org.androidaudioplugin:androidaudioplugin:0.6.3'
settings.gradle include ':aap-fluidsynth', ':androidaudioplugin' include ':aap-fluidsynth'

この他にもsettings.gradleにはprojectDirの指定などがあるのですが、まあ省略します。リファクタリング後の書き方のほうが一般的なアプリケーションプロジェクトっぽくなっていることがわかるでしょう。

コア部分に変更を加えたいときは、ローカルのmavenリポジトリキャッシュに発行して、それを使ったプラグインでは参照のバージョン番号を変えます。アプリケーションのビルドを別途実行するのは面倒だという場合は、一時的にリファクタリング前のプロジェクト構成に戻してやれば良いだけです。

現状でMaven Centralには何も発行されていないので、AAPのアプリケーションをビルドする場合は誰もがandroid-audio-plugin-frameworkをビルドしてGradleでpublishToMavenLocalタスクを実行する必要がありますが、一度やっておけばよいことです。

同じことがaap-lv2のandroidaudioplugin-lv2についても言えます。

現状ではどのLV2プラグイン移植のプロジェクトもaap-lv2をsubmoduleとしていますが、これは半分くらいはMavenパッケージが発行されていない現状では一度ローカルでビルドする必要があるためであって、その意味ではパッケージを発行するようになったらいつでも.gitmodulesからいつでも消せます。

ネイティブ参照解決地獄

さて、ここまでは綺麗にできるKotlinの部分です。AAPのメイン部分はC++で書かれていてNDKでビルドされるネイティブコードです。前回Prefabの記事を書きましたが、PrefabはAARの参照とCMakeLists.txtのちょっとした記述の追加だけで参照を自動的に解決できる仕組みであり、そしてまだ実用に耐えられるものではありませんでした。

どの点で実用に耐えなくなるか、一応説明しますが、このプロジェクトではネイティブライブラリの参照がこれくらい深くなります。今回はリファクタリング後のリポジトリ構成、aap-lv2からaap-fluidsynthモジュールをaap-lv2-fluidsynthという独立リポジトリに移動させた後の構成を見てください(断片的です)。

  • aap-fluidsynth.so (in aap-lv2-fluidsynth repo)
    • libandroidaudioplugin-lv2.so (in androidaudioplugin-lv2.aar, in aap-lv2 repo)
      • libandroidaudioplugin.so (in androidaudioplugin.aar, in android-audio-plugin-framework repo)
    • libfluidsynth.so (in aap-fluidsynth, in aap-lv2-fluidsynth repo)
      • libsndfile.so (in android-native-audio-builders repo)
        • libogg.so, libvorbis.so, libFLAC.so (in android-native-audio-builders repo)

こんな感じで、 だいぶ深い参照の連鎖になっています。前回のエントリで説明したとおり、Prefabは複数の依存関係での参照ができただけで対応できなくなるので、Prefabの生態系に乗り換える・コミットしてやっていくのは、現時点ではあきらめるしかありません。

Prefabが使えないとなると、androidaudioplugin.aarやandroidaudioplugin-lv2.aarに含まれるネイティブライブラリやそのソースのヘッダファイルへの参照をなんとかしないといけないわけですが、aap-lv2のプラグインに関しては、LV2のプラグインのバイナリのビルドにはAAPへの参照が必要ない(LV2のプラグインの仕組みに則ってlibandroidaudioplugin-lv2.soに含まれるlilvがホスティングを担う)ので、基本的にはLV2プロジェクトのソースにLV2ヘッダが入っていない等の事情でも無い限り、ヘッダの追加参照は不要です。AAPのヘッダも同様に不要です。

ただし、プロジェクトによってはAndroid assetからファイルをロードするように変更されているものがあるので(aap-fluidsynthなど)、その関係でAAPのヘッダが必要となっている場合はandroid-audio-plugin-frameworkのリポジトリから直接コピーしてきたものをaap-include-hackというディレクトリに適当に放り込んでいます。やや治安が悪いですが、そうそう変更されるものでもないので、現実的にはたぶん大丈夫です。LV2ヘッダも同様です。LV2 toolkitの他のライブラリはあくまでホスティング用に取り込まれたものであり、libandroidaudioplugin-lv2.soでのみ使われているので、気にする必要はありません。

一方でlibandroidaudioplugin-lv2をビルドするときにはlibandroidaudiopluginのヘッダが必要になるし、これは頻繁に変更が加えられるホスティングAPIをふんだんに活用しているので、ヘッダファイルのやっつけコピーは回避します。ヘッダファイルについては、まあsubmoduleのディレクトリを参照してもいいだろうということで、CMakeLists.txtから直接相対パスを指定しています。

問題はリンク時に-Lオプションで指定すべき共有ライブラリのパスですが、これはPrefabが来るまではまともな解決は無理だろうということで、libandroidaudioplugin-lv2のビルドにおいては、build/intermediates/merged_native_libs/debug/out/lib/${CMAKE_ANDROID_ARCH_ABI}をCMakeLists.txtでlink_directories()に指定しています((ちなみにAndroid SDKに含まれているCMake 3.10.2ではtarget_link_directories()はまだサポートされていません))。リンク先のlibandroidaudioplugin.soがandroidaudioplugin-lv2.aarにバンドルされる必要はありません。androidaudioplugin.aarにj含まれているので。

その他のLV2プラグイン移植プロジェクトでも、たまにlibandroidaudioplugin.soのAPIを参照しているものがあり(主にAssetManagerのやり取りのため)、これもビルド時に-lオプションで解決できる必要があるのですが、これも使用しているAPIのABIが変わることはそうそう無いだろうと判断して、GitHub上でリリース済みのandroidaudioplugin.aarを適当にダウンロードしてきて展開してそれを参照することにしました。これもやっつけポイントでPrefabがまともになれば不要なやつです。

参照をまともに解決する仕組みが無いだけでここまで面倒なことになるとは…という感じですが、これまでもネイティブコードのAndroid対応ビルドの状況は惨憺たるものだったので、とりあえず自分流でもレシピが出来ただけでまずは上々だと思っています。

ビルド時間短縮と開発作業が継続可能な構成

だいたいこのようなアプローチでaap-lv2リポジトリを分割し、参照を編成し直したことで、およそ以下のような手順で、整合性のあるビルドに基づいて変更の動作確認ができるようになりました。

  • android-audio-plugin-frameworkの変更はトップレベルでチェックアウトしたものをビルドしてpublishToMavenLocalでローカルのMavenリポジトリに置く。
  • aap-lv2やプラグイン側のプロジェクトではAndroid Studioなりgradlewでビルドするようにする(トップレベルのmakeはsubmoduleのandroid-audio-plugin-frameworkをビルドしてpublishToMavenLocalで上書きしてしまうので呼ばない)

これでおよそ技術上の課題がおよそ解決したようにも思えますが、実際にはそんなことはありません。リポジトリとパッケージが「適切に分割された」状態のままでは、LV2プラグインの移植をデバッガで追跡できる範囲が非常に限定的です。リファクタリングの成果は実際のコーディング作業を継続的に行える構成になっている必要があります。

Prefabでも挙がっていて、Android NDKまわりで永遠に進展が見られない課題のひとつとして「参照パッケージにソースを同梱しておいてそれらもデバッガーで追えるようにしたい」というものがあります。つまり、今はできません。

共有ライブラリになってしまっているものをデバッグできないというのであれば、ソースを参照できるかたちにする必要があります。そのためには、まずaap-lv2のandroidaudioplugin-lv2モジュールの参照を、project(...)形式で別途チェックアウトされているソースへの参照に置き換える必要があります。これでaap-lv2自体に含まれるソースはデバッグできるようになります。

実際には、これでもまだデバッグ作業はすぐ行き詰まります。というのは、serd/sord/sratom/lilvのソースに踏み込むには、android-native-audio-buildersでビルドされる以前のソースでもステップ実行できる必要があるためです。これはもうlibandroidaudiopliugin-lv2のCMakeLists.txtに手を加えて、これらをソースから直接ビルドするようにしています。そのために必要な手作業もおよそかき集めてスクリプトひとつで移行できるようにしました(戻すのは面倒なのでgit reset --hardで)。

実のところこのモードでLV2 toolkitの全てをフルビルドしてもそんなに時間がかからないので、いっそこっちをデフォルトに…と考えなくもないのですが、デバッグ作業だからABIひとつだけビルドすれば足りているのであって、同じことを4つのABIでやるとまあまあの時間になってCI待ちの体験は悪化するな…と思っています。まあとはいえ開発体験のほうは良いので迷い中です

その他のハマりどころ

…もいろいろ書こうと思ったのですが、もはや覚えていないこともいろいろあってまとめきれる気がしないので、とりあえず覚えている躓きポイントをいくつか書いておきます。

  • Android Studioのネイティブデバッグ: 基本的に4.1以前はC++ソースのデバッグは出来ない前提でいたほうがよい。Arctic Foxを使う。
  • CMakeLists.txtで参照しているファイルに変更を加えてもビルドがトリガーされない場合がまあまあある
  • 共有ライブラリが更新されないままデバッガが起動してデバッグ作業が妨げられることがままある。手動でアンインストールしてからデバッグするのが安牌っぽい。
  • デバッガが接続できないままタイムアウトすることがまれによくある。ASを落としてもダメなことが多い。プロジェクトの.ideaを消すとほぼ直る。
    • ASを落とす時はkillall -9 javaでGradleデーモンを全部殺す。

今後の課題

AAPの開発はリグレッションを起こしながらだとなかなか進みません。ホストの開発において安定的に動作確認に使えるプラグインは必須ですし、逆もまた真なりです。LV2でうまくいってもJUCEでうまくいかない、みたいなこともあってまだ治安が悪いので、今度はaap-juceの大規模改修に取り掛かる必要があるでしょう。とはいえ、aap-lv2までが少なくともCIで見えるところまでのレベルでは正常化できたのは大きなところです。

実のところCIが整備されただけでは不十分で、複数のリポジトリに散在するプラグインを全体的にテストする仕組みが必要なのですが、まだテスト自体ろくに出来ていない状態なので(そもそもホストとプラグインを入れないとテストできない)、テスト実行できるためだけのアプリケーションを作って、アプリもapkをリリースから引っ張ってきてインストールしてinstrumented testsを実行する、という感じでしょう。

その他の作業記録

AAPのLinuxデスクトップビルドの復元

AAPプラグインの移植・開発をAndroid上でやるのはしんどいので、デスクトップでも可能にするという前提でビルドできることにしているのですが、まずデスクトップ用のAAPコアのビルドを再整備して、ローカル環境に~/.local/lib/aap/などにインストールされているプラグインを列挙できるところまでは実現しました。

ただ、プラグインをホストと接続するためには、デスクトップ環境にはAndroid Binderがないので無理があります。GitHubではBinder for Linuxというプロジェクトが見つかるのですが、試した範囲では最新のforkでもUbuntu 20.04ではビルドできませんでした。まあこのアプローチだと、コードの再利用は楽になりますが、Linuxでしか動作しないことになってあまり将来性が無いですね。

というわけで、gRPCでも使って通信部分を抽象化することにしました。それで仕組みだけは実装してみたのですが、Android Serviceにintentを発行してソレに基づいてプラグイン側のアプリケーションのプロセスを立ち上げる部分はまだ面倒なので未着手で、結果的にまだ何も出来ていない状態です。気が向いたら先に進めるでしょう。

あとaap-lv2やaap-juceもデスクトップで使えるように実装対応する必要があるのですが、何もやっていないので、実態としてはまだ使えるプラグインがほぼありません。まあ使えるホストが先にほしいですね。

Jetpack Compose実験

ビルドのリファクタリングとは別に、AAPのホストアプリケーションのサンプルをJetpack Composeで組み直そうとしていたのですが、Jetpack Composeを利用する上で必須のバージョンとなるAndroid Studio 4.2(でしか使えないAndroid Gradle Plugin)…から2020.3.1となったArctic Foxがまだまだバグバグで、ろくにデバッグできない状態だったので、issuetrackerで報告するためにバグ再現条件を調べたり修正を待っていたら、12月も上旬が過ぎていました。

何やら最新版でもまだしんどい場面があるみたいですし、その間にリファクタリング作業がメインになったので、こっちはそのうち再開するだろうという状態です。

Jetpack Compose for Desktop実験

Jetpack Composeに手を出したのは、これならJetBrainsのCompose for Desktopで動作させればデスクトップでも同じホストやプラグインUが動作するんじゃないかと思ったからなのですが、ちょっと調べてみると、Architecture Componentsに依存しているうちは移植性が無いし、自分のアプリだけ対応すればいいとしてもJetpack Composeの典型的な参考資料がほとんど使えない(たとえばStateに関するドキュメントがいきなりLiveDataに依存している)という状況なので、今手を出すならAndroid専用ということにしよう…となりました。

特にCompose for DesktopプロジェクトをKotlin MPPで構成していると、IDEAでもcommonモジュールをデバッグ実行できないみたいですし。この方面ではまだFlutterを使ったほうが安牌っぽい気がします(とはいえKotlin用channelとC++ffiの口を用意するのも面倒なので多分使わないと思う)。

Compose for Desktopまわりで調べたことなどはTechBoosterの新刊Up To 11に記事として収録されています。ただホントにうっすらとした記事なので、いつもの「これ以上調べてる人はおらんやろ…!」みたいなノリではないです。ご了承ください(?)

techbookfest.org