KotlinクロスプラットフォームMIDIエコシステムの構築

android-audio-plugin-frameworkMIDIまわりの機能をいろいろ追加したいのと、いいかげん音楽制作のためにC#で作ってしまっていたツールをLinuxデスクトップでも開発できる言語環境で開発を再開していきたいという気持ちが、ずっと燻っていました(います)。しかしまだそのための基盤が足りない状況です。

そういうわけで、2月から少し準備していたのですが、3月には重い腰を上げて、これまでC#で作っていたクロスプラットフォームMIDIライブラリやツールをKotlinに移植していました。まだAPI的に安定したものでは到底ありませんが、ある程度目処が立ってきたので一度まとめておきます。

【目次】

ktmidiプロジェクト

ktmidiはmanaged-midiのKotlin移植というべきもので、もともとはfluidsynth-midi-service-jという(これまたXamarin.Androidから移行した)プロジェクトの一部としてSMFの読み書き部分などを部分的に移植してあったものに、ネイティブMIDIアクセスAPIMIDIプレイヤーの移植を追加しているところです。

もっとも、現時点ではKotlinエコシステムには.NET Standardのようなまともなクロスプラットフォームのプロジェクト構築手法がありません。Kotlin MPPプロジェクトはJVM/JS/Nativeのみ、KMMプロジェクトはiOS/Androidのみ、デスクトップとモバイルで全てを共通化できる仕組みは無し(Jetpack ComposeはAndroidとデスクトップJVMのみで個別に対応)、といったバラバラな状態です。そのため、これらの移植はまだまだ安定性を期待できるものでは到底ありません。

ktmidiにMidiAccess APIを追加する、もともとのアイディアでは、デスクトップJavaではjavax.sound.midiAPIを使って、Androidではandroid.media.midiAPIを使う構想になっていました。しかし現状ではktmidiが採用しているMPPのプロジェクト構成ではAndroid個別対応が不可能なので(jvmMainの他にandroidMainを置くことが出来ない)、Android側は別途プロジェクトを用意するしかないという本末転倒の状態です(未着手)。

仮想MIDIポート機能

Javaにはjavax.sound.midiがあるし、Androidで互換APIを実装することもできるし、そもそもクロスプラットフォームMIDIアクセスAPIを用意する必要はあるのか?という疑問が出てくる人もいると思いますが(まず自分がそうでした)、javax.sound.midiでは「仮想MIDIポートの作成」ができないんですよね。Windowsで実現できないので、Windowsが足を引っ張っている感じです。javax.sound.midiだけでなく、Web MIDI APIにもこの機能はありません。

従来のクロスプラットフォームMIDI APIにもやはりこの機能は無いのですが、C++では最近RtMidiの後継としてlibremidiというプロジェクトが誕生しており、これはEmscriptenを使ったWeb MIDI APIのサポートも追加しているのですが(わたしが去年juce_emscriptenのためにRtMidiで実装したのと似たような感じです)、これが仮想MIDIポートの作成に対応しています。JUCEのjuce_audio_devicesにもMidiInput::createNewDevice()のようなAPIがあります。

仮想ポートが作成できないということは、仮想MIDIキーボードのアプリを作ったときに、そのアプリをMIDI入力デバイスとして使用できない(使用するためにルーティング作業が発生する)ということです。わたしが日頃から使っている自作の仮想MIDIキーボードアプリではこの機能が使われていて、DAWに接続してソフトウェアからあたかもMMLを演奏しているかのようにDAWMIDIメッセージを送信できて便利なので、この機能は必須です。

alsakt

javax.sound.midiでは足りないところとして、もうひとつ最近知ったことなのですが、javax.sound.midiLinux実装はALSA rawmidi APIで取得できるデバイスにしか対応していません。TimidityやFluidsynthのようなソフトウェアMIDIのポートや仮想MIDIポートはALSA sequencer APIでしか取得できないので(BLEも同様でしょう)、完全に使い物にならないレベルです。

無いなら作るしかない、というわけで、ALSA Sequencer APIのJNIバインディングを作りました。

https://github.com/atsushieno/alsakt

managed-midiを開発していたときにもalsa-sharpというプロジェクトを作って、そこでP/Invokeまわりを全て面倒見ていたという経緯があるのですが、ALSAAPIから手作業でバインディングを作るのはしんどいので、自分で作っていたnclangというClangの.NETバインディングを使って自動生成していました。*1

Java/KotlinでもJNIバインディングを手作業で構築したくはないので、自動化機構を選定しました。以前にfluidsynth-midi-service-jを作った時はJNAとJNAeratorを使ったのですが、JNAeratorはもともとBridJと両輪で開発されてきたプロジェクトで、BridJともども既に死んでいるので、今回は今でも開発が継続しているJavaCPPを採用しました。JavaCPPはAndroidでも使えるし以前に同人誌に記事を書いたこともあるのですが、今回はALSA SequencerなのでJVMしかもLinuxデスクトップだけです。ほぼ忘れていたのでゼロから調べ直して作りました。

JavaCPPはJNAとは異なりバインディングのビルド時にnative glueを生成する仕組みになっていて、それはjavacpp-presetsというリポジトリでいくつも例示されているように、mvnから呼び出されるように作られています。が、2020年になってgradle-javacppというプロジェクトが誕生し、IDEAやAndroid Studioからもgradleでシームレスにビルドできるようになりました。今回はこれを採用することでビルド過程をあらかた既存のワークフローに合わせることができ、maven-publishプラグインと合わせてほんの2日で他のプロジェクトからmavenLocal()で参照解決して利用できるレベルまで辿り着けました。

ktmidiでは、MidiAccessクラスのLinuxデスクトップ向け実装として、このalsaktを利用したAlsaMidiAccessというクラスを用意しています。実態としてはmanaged-midiAlsaMidiAccessをそのまま持ってきたというのに近いです。

(もしネイティブコードで使おうと思ったらKotlin/Nativeを使うことになるわけですが、JNIはKotlin/Nativeでは使えないのでcinteropを使って相当部分を実装することになるでしょう。JNIよりは楽なのではないかと思いますが、やってみないとわかりません。)

mugene-ng

今回一番何とかしたかったのは自作のMMLコンパイラmugeneです。このMMLコンパイラは単なるおもちゃではなく、MIDIで楽曲データを作成できるレベルまでマクロシステムを作り込んであって、これを生き延びさせるのが今回の主目的です。この開発が継続できるようになれば、MIDI 2.0を使った楽曲データの作成も現実的になりますし(そのためにはktmidiでMidiPlayerのMIDI 2.0サポートも必要になりますね)。

mugeneの用途としては、現状では仮想MIDIキーボードとvscodium拡張に組み込んで使うのと、MMLプラグイン定義からTracktion Waveformの楽曲データ(.tracktionedit)を生成するもの(augene)と、AndroidアプリとしてMMLからコンパイルして演奏できるようにするというのがあります(fluidsynth-midi-service-jの前身であるXamarin.Android版のfluidsynth-midi-serviceではMMLから演奏する機能もデモアプリに付けていました)。今後オーディオプラグインなどに組み込んで使うことを考えると、ネイティブコードとしても利用できるようにしたいところです。これらを全て自然に満たせそうなのは、現状ではC++とKotlin、あとはDartくらいかなと思っています(開発環境がLinuxでまともに動作することは当然必須)。MMLコンパイラはリアルタイムでは動作できないのでGCがあっても問題ありません。

MMLコンパイラ自体の機能要件は単なる文字列処理でしかないのでどんな言語でもすぐできるはずなのですが、構文解析をLALR(1)のjayで実装していたので、なるべくそのまま移植したいところです。今回は最終的にantlr-kotlinで実装しましたが、一筋縄ではいかず(その紆余曲折は途中まで別途まとめてあります。気が向いたら続きを書くかも)、一度はANTLR4/Javaを使って生成したコードをIDEAのnj2kを使って手作業でKotlinに変換してその結果をさらに修正したり(ついでにnj2kのバグを直したり)で寄り道しつつ、最終的に文法定義のレベルで対応してantlr-kotlinを使い続けられるようにしました。

プロジェクトはとりあえずはKotlin MPPで作って、JVM以外にJSでもNativeでもいけるようになっています。これもデフォルトで組み込むMMLファイルの解決にAndroid固有の処理が必要になりそうですが、外部からプラットフォーム固有の実装を繋ぎこめば何とかなるレベルです。JSがいけるはずなので、いずれvscodium拡張も書き換えようと思っています(JSはまだ試していない)。

あと、これは現状では全く使われていないのですが、MMLを書いてMIDIバイスを操作するのではなくAPIとしてプログラマブルに操作したいという場面で使えるように、mugeneのdefault_macro.mmlの命令に相当するAPIを実装したnotiumというライブラリを作ってあったのですが、これもついでにKotlinに移植しました。

あとaugeneも作り直したいところですが、現状のツールのアプローチが.tracktionedit全体を毎回生成する方式で、トラックが増えてくると毎回ロードするために数十秒待たなければならなくなって致命的に生産性が低いので、いったん全部仕切り直して、MML処理部分はKotlin/Nativeでネイティブライブラリ化してJUCEアプリに組み込むのが妥当かなと思っています。それなりに実装時間が必要になりそうなので氷漬けです。

kmmk

こっちはまだ全然出来ていない状態ですが、ALSA実装が組み込まれたktmidiを、Jetpack Composeで作り直し中の仮想MIDIキーボードkmmkで使っています。アプリケーションのUIはJetpack Composeで、モバイル前提の設計になるので(Androidでも動作しています)、キー入力イベントなども根本的に作り直しになる予定です。そもそもxmmkではボタンは単なる飾りでクリックにも反応しないので作り直しともちょっと違う…

仮想MIDIキーボードのアプリはいろいろあるのですが、今ほしい機能を仮想MIDI入力デバイス以外で挙げるとしたら…

  • 入力の記録(これはxmmkではMMLとして実装済
    • ついでにMMLからの簡易再生(xmmkでは実装済
  • 簡単に使えるプログラムチェンジ(xmmkではメインメニューで実装)
  • 多様なMIDIバイスの音色プリセットのサポート(xmmkでは実装済
  • 簡単に88鍵くらいにアクセスできるキーボード(PCキーボードに反応するのは一定範囲のオクターブだけで良い)
  • キースイッチ情報の適用

といったところでしょうか。キースイッチに関しては何も規格がない状態なので、動的に取得するのがまだ困難かもしれません(MIDI 2.0 Property Exchangeで取得できる時代になってほしい…)。いずれにしろ風呂敷を広げすぎても意味がないので、できるところから実装していこうと思っています。

*1:Microsoft/ClangSharpというプロジェクトが似たようなことをやっていて、当初はAPIが全然イケてなかったのですが、最近ではnclangのようなOOラッパーも追加してまともに使えるようになっていそうなので、nclangを使う必要はもう無いでしょう。

3月の活動記録(2021)

3月の開発記録です。長くなってしまったので分割して、こっちは「その他」編です。

VST3とSFZ探訪

2月にリリースされたWaveform 11.5がLinuxでVST3をサポートするようになりました。これによって、昨年10月で書いていた、プラグインをtracktion_engineとWaveform11.5の間で共有できなかった問題が、VST3によって解決したことになります。そういうわけで、3月はVST3プラグインをいろいろ試していました。これはまとまった時間ができたら何かしらアウトプットを出したいと思います。(まだ無い状態)

ただ、Waveform 11.5 (on Linux) はまだまだオーディオエンジンが不安定で、トラックが10本を超えた辺りから(多分あまりプラグインを問わず)頻繁にクラッシュするようになるので、打ち込み作業での実用性はまだまだ覚束ないところです。

3月にはもう一つ嬉しいニュースがあって、Linux版のReaperがなんとLV2プラグインを使えるようになりました。LV2はクロスプラットフォームで利用できるように設計されているので、これはいずれMacWindowsでも使えるようになるかもしれません。

2月にはVitalが公開されたこともあり、シンセプラグインはそれなりに選択肢が出てきたのではないかと思っています。サンプラーのほうもバラエティを広げておきたいところです。エンジンはsfizzが最有力候補だと思っていますが、こちらもVST3版の開発が進んでいて、Waveformからも直接ロードできるようになっています。半年くらい前からLV2 UIの開発が進んでいたのですが、これはvstguiで作られており(vstguiはGPLv3ではないので気軽に使えるのです)、VST3版でも自然に同じものが組み込まれています。

sfizzはv1.0まではSFZ v1を主なターゲットとしていますが、SFZ v2やARIA独自拡張命令なども一部実装されていて、最近のバージョンではkeyswitchなどもサポートされるようになりました。その結果、たとえばUI METAL GTXのようなkeyswitch前提の音源もロードしてある程度期待通りのサウンドが生成できるようになっています。実用性がどんどん上がっているので今後も楽しみなところです。

aap-lv2-dragonfly-reverb

Dragonfly-Reverbはgithubで公開されているリポジトリの中では人気の高いリバーブエフェクトで、LV2とVSTがサポートされています。dragonfly-reverbのもうひとつ大きな特徴としては、このプラグインDPFで実装されています。DPFはLV2をサポートしているので(そのために存在するようなものなので)、これをAAPに移植できればDPFもサポートできたということになります。DPFによるVSTサポートにはVST2SDKではなくVeSTigeが使われているので、2021年でも誰でもビルドできます。

VSTをいろいろ探す作業の一環として、利用可能なプラグインで楽曲のテンプレートを作っていたのですが、Dragonfly-Reverbはリバーブの中ではかなり有力な選択肢なので、Waveformで打ち込んだものをAAPでAndroid上で演奏しようと思った時に使えるようにしておきたいところです。

DPFはビルドシステムらしきものがMakefile前提で作られており、sfizzやfluidsynthのようにCMake前提でビルドすることはできません。そのため、mda-lv2やguitarixのようにandroid-native-audio-buildersリポジトリAndroid用にビルドするやり方で対応しました。ビルド自体はスムーズに実現できたのですが、プラグインの挙動自体がまだ期待通りではないので、いずれtweakが必要になるでしょう。

https://github.com/atsushieno/aap-lv2-dragonfly-reverb

AAP: Compose UI and audio preview in plugin apps

2月までの実装では、AAPのプラグインアプリに付属のMainActivityが立ち上がるとプラグインのリストと、それらを選択するとポートリストを表示する機能が伝統的なUIで実装されていました。3月はJetpack Composeのfirst betaがリリースされたこともあって、ComposeをAAPに適用するには悪くないタイミングです。というわけで、まずこのUIをJetpack Composeで実現するところから始めました。Composeはこの時点でmainブランチに取り込まれていなかったのですが、本格的にComposeにコミットしていく流れになったと思います。

もともとのUIも単なるプラグインとそのパラメーターのリストなので大したことはないので、ついでにホストアプリケーションのサンプルで実装していたオーディオ出力のプレビュー機能を、プラグイン側にも実装することにしました。もともとUIの大部分が再利用の産物だったので、ホスト側のオーディオ処理をモジュールとして独立させて共有してしまいました。

プラグインアプリ内で利用できる機能として実装したことで、デバッグ作業も少し楽になりました。(インプロセスでオーディオ処理できているわけではないので、あくまでデバッグが楽になった程度です。)

KotlinクロスプラットフォームMIDIエコシステム

これはまとめていたら長くなってしまったので別エントリにします。

2月の開発記録(2021)

今月は前のめりにまとめた完全自分用の開発作業記録です。今月は上旬にちょっとDSPまわりの勉強をしていた以外は、だいぶAndroidAudioPlugin関連の開発に時間をつぎ込めた感じです。

s:/aria2web/aqua/

割とどうでも良い前フリから。もう半年以上前ですが、aria2webというsfizzサンプラーARIA extension GUIをWebViewとしてロードできる仕組みを実現したLV2プラグインを作りました。

atsushieno.hatenablog.com

プロジェクト名がイマイチだったので*1、いずれAAPに取り込んでWeb UI化するときに名前を変えようと思っていたので、今月ようやく着手しようと思った時点でaquaに改名しました。

XML namespacesを適切に利用した拡張管理

そういうわけで、先月末にいよいよGUI統合に取り掛かりたいと書いていたやつですが、実は未だに何もない状態です。先に片付けるべき課題があったためです。具体的には、プラグイン個別のGUIに対応するより先に、GUIのないプラグインの汎用パラメーターコントローラーリストのようなUIを(特にWebUIで)作るべきだろう、と考えました。

そのために実際に必要になるのは、パラメーターの種別に応じたUIコントローラーであり(値の範囲が整数なのに小数値を入力したくはないわけです。あるいはそもそもLV2 Atomのように数値でない場合とか)、そのためにはまずパラメーターのプロパティが規定される必要がありました。LV2で言えばport propertiesというコンセプトです。LV2であればlv2:portPropertyとかunits:unitなどがこれに相当します。

ポートのプロパティは拡張可能でなければなりません。拡張可能な情報を素直に実装するとしたら、適切な名前解決が重要です。 さし当たって、それまでdefault, minimum, maximum属性として規定されていた<port>要素のプロパティはurn:org.androidaudioplugin.port名前空間に属することになりました。これまでAAPは自由に拡張可能としてきたわけですが、C APIレベルでの拡張性しか実装していなかったので、ポートのプロパティのようにメタデータaap_metadata.xml)を拡張するやり方が未整備でした。

この拡張を実装するにあたって問題になったのが、AAPの内部でtinyxml2を使ってメタデータを処理していたのですが、tinyxml2はXML namespacesを処理できないことでした。マジか…他に何が使えるんだ…と思って調べてみると、PugiXmlもRapidXMLもダメ、使えるのはexpat(APIが完全に古い), libxml2(glib依存が厳しい), xerces-c++(でかい)…といった感じでした。ここは20世紀か?という感じの古参ばかりですが、あまり深入りしたくなかったので、Android側でのメタデータ解析は全てAndroid APIでやることにして、デスクトップのみlibxml2にして回避しました。ちなみにJUCEに含まれるXMLAPIダメです

LV2みたいにTurtle文法を選択してパーサの選択肢が無くなるよりXMLにしたほうが絶対良いでしょ、と考えていた(いる)わけですが、C++がこんなにxmlnsをサポートしていないパーサばかりだとは思っていなかったので、自分がC#やらKotlinやら使ってコードを書いていたのはずいぶんスポイルされているんだなあ、となりました。しかし、うーん…さすがに2021年からC++XMLパーサを実装するのはどうかな…

ちなみに今月はこのポート プロパティの基盤を実装しただけで、それを反映するGUIの構築には着手していない状態です。

flatten LV2 dev. environment

aap-lv2が12月にリポジトリを分割して開発しやすくなったので、aria2webをaquaに変更する過程で気づいた、sfizzで期待通りにオーディオデータが生成できなかった問題に着手しました。が、中で使われているLV2 toolkit (serd + sord + lv2 + sratom + lilv)がバイナリ参照になっていて、デバッグ時に中まで追えずに苦労していました。その度にデバッグのためにリポジトリの構成を変更してLV2 toolkitを一緒にビルドしてはコミット前に戻す、みたいなことを繰り返していたのですが、こんなことをするくらいなら全部aap-lv2のリポジトリでビルドすべきだ、と考えるに至りました。

実際これを避けていたのは(1)公式のビルドスクリプトがwafだったので公式に可能な限り準拠したやり方にしたかったのと、(2)Androidの4アーキテクチャ分 x この数のネイティブライブラリビルドの増加はCIビルド時間に有意に悪影響が出ると考えていたからなのですが、wafをスキップして直接CMakeでまとめてビルドしてみると個々のライブラリのビルド時間はそれほど長くも無かったので、全部フラットにビルドすることにしました。これでデバッグ体験はかなり良くなっています(現状やるのはわたしだけだと思いますが)。

ちなみにこの作業自体は前述のaquaをAAPに取り込んでWebUIの実験台に使おうと思ってまずsfizzの動作確認…となってオーディオ側の問題に気づいて着手したのですが、前述の通りGUI統合に入る前にポートのプロパティを…となってこれも未着手です。

Hera (Juno 60 emulator) port to AAP

今月はRolandからJuno 60プラグインが出て話題になりましたが、ほぼ同時期にADLplugやsfizzの開発者がHeraという新しいJuno 60エミュレーターのJUCEのプロジェクトを公開していました。そういうつもりはないんだけど完全に開発者の追っかけみたいなムーブになっている…

github.com

既存のエミュレーターにJUCEのMPEサポートを加えて作ってみた、というコンセプトで作られているようです。RolandのほうはMPE対応はしていなさそうですね(featuresを見ているだけですが)。

f:id:atsushieno:20210226001343p:plain
Hera on Waveform 11.5

CMakeベースのJUCEプラグインだったので、ちょうどCMakeでInstrumentとして機能するやつがほしかったんだ…! と思って、その日のうちにAndroidに移植しました。

github.com

aaphostsampleで開くとちゃんと音が出るので、最近ではOB-Xdと並んで簡単に動作チェックする時に活用しています。移植作業もEqとChowPhaserを移植していたのを応用しただけです。Heraとは全然関係ないのですがChowPhaserの作者から「Androidに移植してるの?? 試せる??」みたいなメールをいただいて「すまん…JUCEのaudio inまわりがおかしいからまだだ…!」みたいなお返事を書くことに…

Helio workstation with AAP

AAPのプラグイン移植はずいぶんバリエーションが増えてきたのですが、ホスト側はさすがに使い回せるアプリがほとんど無く、自作のaaphostsample、JUCEのAudioPluginHost、あとtracktion_engineを使うaugeneが一度も演奏できたためしがない…という状況でした。

JUCEを使ったDAWのひとつにHelio Workstationというのがあるのですが、これがTabletのみを意識しているとはいえAndroidでも動作すると謳っているので、これにAAPサポートを追加したいというのがひとつのプロジェクト完成形の姿でした(任意のDAWで任意のプラグイン移植が使える状態といえるわけです)。

github.com

ただhelio-workstationのプロジェクトは、Android用のプロジェクトなどが既にProjucerで生成された後の構造をいじって作られているようなので、既存のaap-juce移植のモデルにはうまくフィットしませんでした。何回か失敗した後で、スクラッチからHelio向けに独自のアプローチでビルドプロセスを構築することで(といってもどっちも自分しかやっていないのですが)、ようやくAAPサポートを追加できました。

github.com

ほぼオリジナルのコードに手を加えること無く実現したので、無理にビルド手順を一般化しないほうがいいのかなあと思い始めています。昨日も公開されたばかりのVitalを移植しようとしたのですが、このプロジェクト構造も一般的ではないのでAndroidビルドを追加する時点でかなり無理っぽさが出ています。いずれやるとしても何日かかかりそう…(本家でやってほしさある)

ちなみに現時点で、プラグインの一覧が取得できるものの名前の表示がイマイチ(これはオリジナルがそう)、オーディオグラフの設定画面ではオーディオ出力ポートが出てこないので生成結果を一切音声出力できないっぽく見える、その割に実はプラグインインスタンスも生成できていてノートに合わせて音が生成できる、ただし出てくる音がノイズまみれの謎音…みたいな状態なので、実用性はまだ無いです。

f:id:atsushieno:20210226002110p:plain
Helio+AAP
f:id:atsushieno:20210226002135p:plain
Helio+AAP, plugin details

プラグインインスタンス生成の安定化

AAPで初期からずっと困っていた最大の不安要素のひとつが、プラグインインスタンスを複数回生成するとプラグイン側のサービスが落ちてホストも一緒に落ちる、という問題でした。これだとちょっと使うだけですぐホストが落ちるので使用体験を語れるほどのレベルにも至っていない感じだったので、port propertiesが落ち着いたところでがっつり取り掛かりました。

いろいろ試行してみてわかったのは、どうやらandroidaudioplugin.aarをbuild.gradle上でmavenLocal()から解決していると発生するが、デバッグするためにsettings.gradleをいじってプロジェクトの一部にandroidaudiopluginを追加してbuild.gradle上でproject(':androidaudioplugin')として参照すると発生しない(!)ということでした。

何でこの話を書こうと思ったのかというと、事実関係を確認する前提が割と罠だらけなので記録しておきたかったわけです(自分用メモなので!):

  • mavenLocal()でパッケージを解決する時は、ローカルでビルドして修正を加えているバージョンが参照されているかどうか、build.gradleの内容から念押しする必要がある(settings.gradleで追加しても自動的にproject(':...')で解決するわけではない)
  • CIでビルドしても通るようにsubmoduleのandroid-audio-plugin-frameworkmakeでビルドしているので(前述のaap-lv2リポジトリ分割について書いたエントリを参照)、別のソースツリーでandroidaudioplugin.aarやandroidaudioplugin-lv2.aarに手を加えたものをアプリで確認するなら、maven localのパッケージを上書きしないようにビルドする必要がある
  • debugビルドとreleaseビルドの両方でmaven-publishされる構成が変わるので、自分の期待通りのものがaarとして発行されているか、build.gradle内のpublishToMavenLocalタスク関連の記述をきちんと確認する

今回の問題は急場しのぎ的にreleaseビルドのaarをpublishToMavenLocal~/.m2/repository以下にデプロイしないように変更したら直りました。それ以上のビルドの細かいオプションのどこに違いがあったのかは、まあやる気が出たときに…。関連するネタとしては、JUCEプラグインAndroidに移植しているときにJNI_OnLoad()がreleaseビルドでだけ衝突する問題が発生したことがあって、その時はldに-fltoが渡されていたのが引き金になっていた、というところまで調べたりしました。これもそれ以上の原因は追及していなかったり…。

いずれにせよ、この処置を施した後は、プラグイン インスタンスの生成まわりで不定期的にクラッシュする場面はほぼ無くなったので、ようやく安心してある種のアプリでは使えるようになったんじゃないかと思います。

今後の方針

現時点で直しておきたいのは(1)Frequalizerを含むJUCEのエフェクタープラグインが全滅している問題(JUCEのバグ)と(2) HelioでプラグインをロードしたときにAudio Outが適切に認識されない問題(AudioPluginHostでは出てくるのでたぶんHelioのバグ)、の2点で、着手するならこの辺でしょう。

あとは今月ようやく2021年版ロードマップ(2021年中に全部やるとは言ってない)を作ったので、その中からおそらくGUIまわりを中心に消化していくことになるでしょう。

github.com

とはいえ4月にM3 2021春があってそっちに向けて何かしらまとまったものを作りたいので、開発はしばらく休止かもしれません。あと多分何かしら別の原稿を書くことになると思いますし。はーどうしよ…

*1:aria2webだとただの変換処理系みたいに見えるので。実際、最初はそうだったのですが…

音声信号処理勉強会のKPT

1年前にMusic Tech Meetup #1 (Night)を開催したのですが、その頃すでにコロナの足音が聞こえてきていて、オフライン勉強会の開催はギリギリ可能という感じでした。その後はとても同様の勉強会を開催できるふいんきではなくなってしまいました(夏頃ならできたかもしれませんが、いずれにしろ今は冬ですし)。

まだ日本だけは1年くらいコロナ禍が収束しそうにないし、オンラインでも勉強会を開催できるようになっておきたかったので、とりあえず一度やってみようと思って、音声信号処理勉強会というものを開催してみました(ここで告知してもよかったのですがまあライトにやりたかったものです)。参加してくださったみなさまありがとうございました。

connpass.com

というわけでそのKPTです。無理やりKとかPとかTとか付けたけど適当です。あ、参加していない人向けにどんな話題が上がったかをふいんきで分かるものとして、イベントページに資料集のgistがあります。

平均的にはおよそ10人くらいでの開催になりました。今回は無難な選択肢としてZoomを使ってみましたが、無料ユーザーなので40分に1度強制切断されながらの開催です(!) 多少のトラブルはありつつも続行は可能は可能でした(K)。ただ中断体験とかチャットログ全消滅はいまいちだった…んじゃないかな…と思うので(P)、今度やる場合はDiscordあたりで試そうかなと思っています。(T)

Discordのビデオチャットは、せいぜい4人くらいまでしかやったことがないので、同じくらいの人数でいい体験になるかは正直わかりません。上限はコロナ禍の間は50人らしいですが(これ日本だけ残り続けても米国が収束したら縮小するんだろうなあ…)、チャットの収拾がつかなくなるので、人数はほぼ変えないと思います。(K)

進め方ですが、基本的に雑談スタイルで進めて、その間に皆さんから資料を出してもらえたり、話題を提供してもらえたりで、まあ悪いというほどではなかったんじゃないかと思います(K)。ただアツシエノがファシリテーターとしてずっとしゃべり続けるのは、しゃべるほうも聞くほうも体験が悪いと思うので(P)、今度はLT的な発表を少し募って主にしゃべる人(というのが決まっているわけではないですが)を切り替えながらやってみようと思います。(T)

内容の方向性ですが、今回は特定の書籍の読書会というベクトルでまとめていて、これは割と良い選択だったと思います(K)。実際の話題があまり本に縛られないようにしたのも、悪くはなかったと思います(K)。本に縛られない方向性のほうがいいのかな、とも考えなくはないですが、それだと本書の内容についてわからないことを議論するのは難しいので、デフォルトで使える話題として据え置こうかなと思います(K)。具体的な書籍は変えたり増やしたりするかもしれません。(T)

…という感じで、また1ヶ月後くらいを目処にまた開催してみようと思います(確定ではないです)。

1月の作業記録 (2021)

恒例の自分用作業記録です。これが自分用でなくなる日は来るのだろうか。

making aap-juce hackable again

aap-juceandroid-audio-plugin-frameworkからJUCEプラグインの移植が無駄に大きくなりそうだったので切り離して作られたリポジトリですが、これも昨年末の時点でGitHub Actionsでビルドを全部通すのに3時間かかる有様でした。これ以上は増やせないし、それでも移植できていたプラグインはほんの5本くらいで、各種の機能のproof of conceptとなっていないといけないのに、これ以上は増やせないというのでは困るわけです。また全サンプルが同じリポジトリにあると、本体の変更を即時全てのサンプルに反映する必要があり、これが開発の妨げになっていました。

そういうわけで、この際、新しいプラグインを移植する作業をテンプレ化できるようにするというタスクの消化も兼ねてリポジトリを分割しました。

ある程度は新規プラグイン移植タスクをテンプレ化できていたので、リポジトリの分割はまあまあ早く実現できたのですが、GitHub ActionsでAndroid Studio Canaryが前提とするAndroid Gradle PluginとCMakeの相性が悪い問題などがまあまあハマりどころでした。ちなみに今でもGitHub Actionsの問題が原因でOSXビルドが出来ない状態です。

リポジトリを分割したら収拾がつかなくなるという状態はなるべく避けたかったので、aap-juce-worldというリポジトリを1つ作って、最終的にはそこでリリースバージョンのようなものをまとめることにしました。これが従来のaap-juceリポジトリに近いものになっています。ビルドが5時間くらい、ダウンロードできるapk群も700MBを超えるレベルなので、スケールしなさそうではありますが、そもそもOSSで公開されているJUCEプラグインもそんなに数多くは無いし、あるものを全て取り込むわけでもないので、まあしばらくはもつんじゃなかろうか…

あと、ビルド時間を短縮する手段として検討しているのが、JUCEのライブラリを共通化してCMakeでビルドするアプローチですが、そもそもCMakeサポートが実現できていなかったので、まずCMakeサポートを実装してみました…というところからスピンアウトしたのが前回のエントリーです。

とはいえ、そもそもCMakeを使っているJUCEプラグインがほとんど無いのが現状です。前回のエントリーを書いた時点でwitte/Eqを移植していましたが、1つだけだと移植作業のテンプレ化には結びつかず、汎用性が担保できなかったので、もうひとつChowPhaserを発掘して移植しました。

移植してみた感想としては、いったんやり方が固まってしまえばCMakeのほうがずっとシンプルです。Projucer版では、.jucerファイルも(少なくとも現状では)手作業で作り換えないといけないですし、Androidプロジェクト自体がProjucerで保存するたびに消えてしまうので、修正を加えていても安心感がありません。jucerベースで作られているプロジェクトもCMakeで書き換えて移植したほうが早いかもしれません。

最終的に1月には12件の新規リポジトリが生えました(めんどくさいのでgithubからコピペしてきた)。1つだけJUCEとは無関係のFirefoxアドオンで、もう1つだけaap-juceと無関係の前回書いたJUCE+Android+CMakeの実験用リポジトリです。

10件のaap-juce関連リポジトリのうち、odin2とFrequalizerはProjucerベースのプロジェクトの新規移植、witte/EqとChowPhaserはCMakeプロジェクトの移植(当然新規)です。

とはいえいずれも完動品というわけではなくて、むしろJUCEのAndroidまわりのissueがいろいろあって特にinput channelがまともに取れないのは割と厳しみある感じです。このレベルの問題に今さらぶち当たったのは、これまで移植してきたプラグインが全てInstrumentでEffectではなかったというのが理由なのですが、なぜEffectが増えなかったかというとまさにaap-juceに移植を増やせなかったせいだったので、順当に問題が出現している感じです。JUCEチームは明らかにAndroidには力を入れてないので、自力で直さないといけないことになりそう…。LV2のエフェクトは当然ふつうに使えるので、むしろLV2化して作ったほうが実現可能性は高いのかもしれません。JUCEの今後の出方次第ですね。

一方でodin2くらい大規模なプロジェクトが、さまざまなUI関連コードを殺しながらであっても一応ビルドが通ってデフォルト音色だけでもホストから音が出ている状態なので、ちゃんと本格的なやつも動かせそうなふいんきになってきました。まあ実際にはあの画面をそのままAndroidに持っていっても実用に耐えないので、モバイル向けにUIを作り変える必要はあるでしょう。

来月の方向性

ともあれ、LV2とJUCEの両方が最新のリポジトリ整理で継続的に開発可能な状態になったので、長らく手を付けられなかったUI統合に手を伸ばす機運が高まってきました。ちょうどChrome 87でAtomicsがAndroidでもサポートされるようになっていて、juce-demosに置いてあるプロジェクトがAndroidでも(オプションを有効にすれば)動作することが確認できたので、JUCE側もUIを再利用出来る可能性が十分にあります。今月はjuce_emscriptenの更新も取り込もうとしていたのですが、いまいち上手く行ったかどうかわかっておらず、完全でもないので(DemoRunnerで音は出せているという程度)、JUCEでやるならそこからです。まあJUCEより先にsfizzのARIA統合として作ったaria2webをLV2 UI取り込みの例として使ってみたい気もしています。

本当はUI統合より前に楽曲再生機構としてのaugene(tracktion_engineのフロントエンドプレイヤー)を動作させたいところなのですが(編集UIが出せても再生が出来ないのでは意味がない)、ローカル環境ではともかくGitHub ActionsでAndroid NDKのclang++が謎のクラッシュを起こしてビルドがコケる状態なので、手を付けられるところから手を付けたいと思います。いずれにしても、ここしばらく主な作業対象がCMakeとMakefileとビルドスクリプトばかりで、その前も原稿とか書いていたので、全然プログラムを書いている気がしなくて良くないので、来月こそはコードを書きたいですね。

JUCE+CMake+Android now works

JUCE6はCMakeに対応していますが、実はCMake対応の恩恵を受けられないプラットフォームが存在します。Androidです。

「は?」というのが多分一番正しいでしょう。何しろCMakeはだいぶ前からAndroid Studioでサポートされているわけで、むしろ一番恩恵を受けるべきプラットフォームです。Win/Mac/Linux/iOSのいずれもCMakeネイティブなビルド環境ではないのに、むしろCMakeがネイティブのビルド環境としてサポートされているAndroidがなぜかサポート外になっているのです。このままではいけない。

目次


Missions

2021年1月にリリースされたJUCE 6.0.6の時点で、AndroidはProjucerによってのみサポートされています。ProjucerはAndroid StudioからビルドできるようなAndroidアプリケーションのGradleプロジェクトを生成します。アプリ開発者がProjucerからAndroid Studioを起動してそこからデバッグ実行できる状態のものができています。JUCEアプリケーション部分はAndroid NDKを用いたネイティブコードのアプリケーションとなり、Projucerが生成するプロジェクトはこれを独自のView上にネイティブコードで描画し、UIイベントをAndroidフレームワークとネイティブコードの間で相互運用します。

Projucerで生成されるAndroidアプリケーションでは、AndroidManifest.xmlやbuild.gradleのさまざまな情報がProjucerのAndroidExporterで規定されたプロパティとしてカスタマイズ可能な項目となっていますが、これらはAndroidアプリケーション開発者が自前で調整できるほうが、自然でメンテナビリティの高い高品質なコードになります。Androidプロジェクト上で何がカスタマイズ項目であるかを知っているだけで設定できる方が、それに加えてProjucerの何というプロパティでどのように設定する必要があるかを知らないといけないよりも簡単であるのは自明でしょう。Projucerが生成するプロジェクトは不自然で、古臭く、アンチパターンに陥っている箇所もあります。

JUCEチームがAndroid開発のエキスパートをかかえていて、常に最新のトレンドにキャッチアップして適切なプロジェクトモデル生成をProjucerに実装できるなら話は別ですが、これが現実化することはまず無いでしょう。そもそもProjucerで全て生成するというのは筋が悪いです。Projucerは切り捨てて、CMakeのモデルに移行していくのが今後の望ましい姿です。

CMakeサポートに求められるのは、Projucerに代わるアプリケーション全自動生成機構ではありません。C++Android NDKをサポートするAndroidプロジェクトではCMakeサポートが組み込まれており、JUCE + Android + CMakeサポートに期待されるのは、Android Studio (Gradle) プロジェクトのCMakeサポートの部分に、いかに違和感なくJUCEのCMakeプロジェクトを適用できるか、にあります。

Projucerを使わないAndroidアプリケーションのビルド機構としては、アプリケーションのActivityから巡り巡ってJUCEアプリケーションのブートストラップ処理に入るネイティブコードのエントリーポイント関数を呼び出せれば、ミッション完了です。本当はそれに加えてAndroidのアプリケーション ライフサイクルに沿った状態管理なども必要になるのですが、どうせProjucerで生成されたアプリケーションでもそれなりにしか出来ていないですし、JUCEアプリケーションに固有の問題ではないので、ここではあまり気にしなくても良いでしょう。

これを実現するためには、ProjucerでどのようなAndroidアプリケーションのファイル群が生成されているのかを把握し、何をユーザー(Androidアプリ開発者)が作成し、どのようにJUCEアプリケーションを繋ぎ込むかを手順化して、実現可能なワークフローを確立する必要があります。

理想を言えば、「既存の」CMakeプロジェクトをそのまま取り込めれば、Androidサポートの可能性が格段に広がります。とはいえ現状では厳しいので、既存のCMakeプロジェクトとの差分を最小化する方向性のみを堅持していくのが良いでしょう。無理に既存コードをそのまま取り込めるようなtoolingの実装に開発コストをかけすぎると、そのツール自体のメンテナンス性が下がります。

以上のような前提で、今回は以下の2つを目標として設定しています。

完成品から(だけ?)見たい人は以下のリポジトリを見ると良いでしょう。

特に後者のプロジェクトには元のプロジェクトに対するパッチが含まれているので、どれくらいの差分でAndroid用ビルドが実現しているのかわかりやすいと思います。自作のandroid-audio-plugin-framework対応コードも含まれているので、実際にはさらに小さい差分で足ります。

分析編

このセクションでは今回のミッションを達成するために調べたことをまとめます。ソーセージの中身に興味がない人は飛ばして読むと良いでしょう。(何でこんなことをしているのか、を把握せずに読み進められるかな…?)

一般的なAndroid C++サポートの実現方法

Android StudioC++サポートの付いたプロジェクトを新規作成すると、この画像のような構成になっています。これが目指すべき状態ですres以下には大量のファイルがあるので折りたたんでいます。

vanilla Android C++ project

一方で、JUCEのAndroidプロジェクトはこうなっています。とはいってもこれは不完全なリストです。具体的には、C++のコードが表示されていません。このトップディレクトリの外側に存在しているためです。

vanilla Projucer Android project

いろいろ違うところはありますが、共通している部分が多いことも見て取れるでしょう。

ProjucerのAndroidExporterで生成されるファイル

この節ではProjucerが生成するAndroid Gradleプロジェクトの内容を読み解きます。

生成されるファイルは、GUI ApplicationやAudio Pluginなど、プロジェクト種別である程度は異なりますが、大枠では大差ないはずです。

build.gradle, local.properties, settings.gradle, gradlew(.bat), gradle/

これらは何も特別なところがなく、普通にAndroid Studioでプロジェクトを生成するのとほぼ変わりません。バージョン番号などが異なり、またAndroid Studioのプレビュー版などではMavenリポジトリが追加されていることがありますが、Projucerの出力はシンプルです。

app/src/debug/res, app/src/release/res

@string/app_nameを定義するstring.xmlだけが含まれています。しかしProjucerのAndroidリソースの生成はややいびつで、debugビルドとreleaseビルドでディレクトリを分けており、これは一般的ではありません。Projucerでビルド設定ごとに異なる内容を指定できるようにしているのを愚直に再現しているのが悪いので、ゼロから作るCMakeプロジェクトで配慮する必要はありません。

app/src/main/AndroidManifest.xml

WifiBluetoothなど、利用するモジュールによって必要になるpermissionなどのmanifest項目が増えることになります。この程度のことはアプリケーション開発者が自分で作業すべきでもあります。

あと、Manifestはマージできるので、JUCEモジュールごとにAARを構築してそれぞれにAndroidManifest.xmlを付けるという手がありますが、とりあえずそこまで求めなくても十分です。

app/build.gradle

一番特殊かつ意味があるのは、カスタムsourceSetsの追加部分で、JUCE標準モジュールの中に含まれるJavaのソースが追加されている部分です(これについては後述します)。基本的にはここで追加するのはあまり適切ではなく、削られるべきものもあり、残しておいてもまあ悪くないというレベルのものもあります。

他にも、signingConfigなど、一般的に必要ではないものが生成されており、これはProjucerにそういうオプションがあるのが悪いです。CMakeビルドでProjucerの負の遺産を引きずる必要は無いですし、productFlavorsもあえて生成する必要はありません。必要な開発者が自分たちの都合に合わせて自前で設定すべきです。

app/CMakeLists.txt

このファイルにはさまざまなオプションが使用しているJUCEモジュール次第で追加されます。

juce_audio_devicesモジュールが含まれていると、Oboeのビルドが追加されます:

set(OBOE_DIR "/media/atsushi/extssd0/sources/JUCE/modules/juce_audio_devices/native/oboe")
add_subdirectory (${OBOE_DIR} ./oboe)

これが必ず含まれていると思いますが、もしかしたらオプションかもしれません。add_definitions()の内容はオプション次第です。<...>/JUCE/modulesは(個人の環境になっていますが)グローバルパス設定から来ています。プロジェクトのUIDのようなものが含まれているプロパティもあるのですが、さすがにいらないようです。

add_library("cpufeatures" STATIC "${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c")

set_source_files_properties("${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c"
    PROPERTIES COMPILE_FLAGS "-Wno-sign-conversion -Wno-gnu-statement-expression")

add_definitions("-DJUCE_ANDROID=1"  "-DJUCE_ANDROID_API_VERSION=16"  "-DJUCE_PUSH_NOTIFICATIONS=1"
    "-DJUCE_PUSH_NOTIFICATIONS_ACTIVITY=\"com/rmsl/juce/JuceActivity\""  "-DJUCER_ANDROIDSTUDIO_7F0E4A25=1"
    "-DJUCE_APP_VERSION=1.0.0"  "-DJUCE_APP_VERSION_HEX=0x10000")

include_directories( AFTER
    "../../../JuceLibraryCode"
    "/media/atsushi/extssd0/sources/JUCE/modules"
    "${ANDROID_NDK}/sources/android/cpufeatures"
)
enable_language(ASM)

config次第でオプションが追加されますが、長いので省略します。ほとんどは標準のJUCE CMakeサポートが肩代わりして不要になります。

if(JUCE_BUILD_CONFIGURATION MATCHES  "DEBUG")
    add_definitions("-DJUCE_DISPLAY_SPLASH_SCREEN=1"  "-DJUCE_USE_DARK_SPLASH_SCREEN=1"  
        "-DJUCE_PROJUCER_VERSION=0x60005"  "-DJUCE_MODULE_AVAILABLE_juce_audio_basics=1"  ...
        "-DDEBUG=1"  "-D_DEBUG=1")
elseif(JUCE_BUILD_CONFIGURATION MATCHES  "RELEASE")
    add_definitions("-DJUCE_DISPLAY_SPLASH_SCREEN=1"  "-DJUCE_USE_DARK_SPLASH_SCREEN=1"
        "-DJUCE_PROJUCER_VERSION=0x60005"  "-DJUCE_MODULE_AVAILABLE_juce_audio_basics=1"  ...
        "-DNDEBUG=1")
else()
message( FATAL_ERROR "No matching build-configuration found." )
endif()

残りの大半はadd_library()でソースを列挙し、プロパティを設定しています。実際にはソース列挙だけで十分でしょう。

最後にtarget_link_libraries()などが記述されます。eglなどは多分juce_gui_basicsが無ければ不要でしょう。

target_compile_options( ${BINARY_NAME} PRIVATE "-fsigned-char" )

if( JUCE_BUILD_CONFIGURATION MATCHES  "DEBUG" )
    target_compile_options( ${BINARY_NAME} PRIVATE)
endif()
  
if( JUCE_BUILD_CONFIGURATION MATCHES  "RELEASE" )
    target_compile_options( ${BINARY_NAME} PRIVATE)
endif()

find_library(log "log")
find_library(android "android")
find_library(glesv2 "GLESv2")
find_library(egl "EGL")

target_link_libraries( ${BINARY_NAME}
    ${log}
    ${android}
    ${glesv2}
    ${egl}
    "cpufeatures"
    "oboe"
)

JUCE/modules/juce_core/native/javacore/init

init/com/rmsl/juce/Java.java というネーミングがアレなファイルだけが入っています。中身は短い。

package com.rmsl.juce;
import android.content.Context;
public class Java
{
    static
    {
        System.loadLibrary ("juce_jni");
    }
    public native static void initialiseJUCE (Context appContext);
}

この実体部分はjuce_coreモジュールの中でJNIで実装されています。このJNI呼び出しは存在している必要があり、JUCEモジュールの中に存在している必要はありません。同等のJavaクラスをKotlinで作ってしまえば不要になります。

JUCE/modules/juce_core/native/javacore/app

com/rmsl/juce/JuceApp.java というファイルだけがあります。これも短い。

package com.rmsl.juce;
import com.rmsl.juce.Java;
import android.app.Application;
public class JuceApp extends Application
{
    @Override
    public void onCreate()
    {
        super.onCreate();
        Java.initialiseJUCE (this);
    }
}

実のところ、これはベストプラクティスに反するので削除すべきです。AndroidアプリケーションでApplicationクラスから派生できるのは1つしかないし、この内容でその希少な価値を独占するのは悪です。現代ではJetpack App Startupを使うべきだし、使わないとしてもContentProviderにできる(すべき)案件です。ちなみに、削除するといっても、build.gradleのsourceSetsの列挙から外すだけです。JUCEのソースから削除する必要はありません。

JUCE/modules/juce_gui_basics/native/javaopt/app

ここには2つソースがあります。com/rmsl/juce/JuceActivity.javaは、Activityに実装することの弊害(AppCompatActivityなどを利用できない等)のほうが遥かに大きいので廃止すべきですが、JNIシグネチャーが絡んでいることもあるので、このレールから外れるやり方でappNewIntent()に相当する機能を呼び出せるか検証する必要があります。

package com.rmsl.juce;

import android.app.Activity;
import android.content.Intent;

//==============================================================================
public class JuceActivity   extends Activity
{
    //==============================================================================
    private native void appNewIntent (Intent intent);

    @Override
    protected void onNewIntent (Intent intent)
    {
        super.onNewIntent(intent);
        setIntent(intent);

        appNewIntent (intent);
    }
}

もうひとつ、com/rmsl/juce/JuceSharingContentProvider.javaのほうは、割と長い内容になっているので、そのままアプリケーションに取り込んだほうが良いでしょう。ContentProviderの独自実装であり、設計上も特に悪いところは無いはずです。(まあ、Javaで書かれていますが。)

ブートストラップ

AndroidでのJUCEアプリケーションのブートストラップは、次のような流れになっています。

  • GUIアプリケーションの場合、juce_gui_basicsに含まれるJuceActivitycom.rmsl.juce.Java.initialiseJUCE()を呼び出す
    • GUIアプリケーション以外は同様の手順をService.onCreate()などで踏む必要がある
  • Javaクラスはlibjuce_jni.soをloadLibrary()でロードする
  • Java.initialiseJUCE()JNI_OnLoad()によってjuce_JavainitialiseJUCE()JNIEnvregisterNatives()で関連付けられており、ネイティブのThread::initialiseJUCE()を呼び出すように実装されている
    • 何でわざわざそんな名前にしているのかは不明(デフォルトでJava_com_rmsl_juce_Java_initialiseJUCE()に関連付けられるはず)
  • Thread::initialiseJUCE()は最後にjuce_juceEventsAndroidStartApp()を呼び出す
  • juce_juceEventsAndroidStartApp()は、juce_getExecutableFile()で得られた実行中のアプリケーションの共有ライブラリのファイルを別途dlopen()でロードし、その中からdlsym()juce_CreateApplication()を取得して呼び出されている
  • juce_eventsモジュールにjuce_Initialisation.hで定義されたjuce_CreateApplication()が含まれている
  • juce_CreateApplication()はマクロJUCE_CREATE_APPLICATION_DEFINE(AppClass)で定義されるもので、プラグインフォーマットごとに規定されるが、Androidの場合はJUCEがサポートするプラグイン規格が存在しておらず、Standaloneのみ対応しており、その生成コードには含まれている。

実装編

Androidアプリケーションテンプレートとして作る

今回の目的を実現するために、まずは標準的なAndroidアプリケーションを作成して、そのapp/build.gradleで指定されたCMakeLists.txtがJUCEアプリケーションをビルドして、正しくロードできるように調整する、というステップで目標を達成することにします。

まずAndroid StudioC++アプリケーションを作成します。筆者はゼロからファイルを作ります(正確には、既存のアプリからコピペしてきます)が、簡単ではないでしょう。Gradle関連のファイルはそのまま使えます。筆者はAndroid Studio Arctic Fox (Canary)を使っているのでgradle 6.8-rc-1とAndroid Gradle Plugin 7.0.0-alpha04を指定していますが、多少古いバージョンでも問題ありません。

app/build.gradle

app/build.gradleには(部分的な内容ですが)次のように指定します。buildTypesやproductFlavorsなど不要なものをほとんど削ったので、Projucerが生成するものと比べるとかなり短い内容になっています。

defaultConfig {  
  applicationId "com.yourcompany.newproject"  
  minSdkVersion    16  
  targetSdkVersion 30  
  externalNativeBuild {  
    cmake { arguments "-DANDROID_STL=c++_static", "-DANDROID_CPP_FEATURES=exceptions rtti" }  
  }
}  
  
sourceSets { main.java.srcDirs += [
  "../JUCE/modules/juce_core/native/javacore/app",
  "../JUCE/modules/juce_core/native/javacore/init",
  "../JUCE/modules/juce_gui_basics/native/javaopt/app"
  ] }

sourceSetsは実のところandroidx Application Startupなどを使うことでもっと減らせますし、減らしたほうが適切なのですが、今回はそこまで説明しないことにします。(これを説明するとKotlinのコードも追加しないといけなくなるので。)

app/CMakeLists.txt

app/CMakeLists.txtには次のような内容を指定しています。ちょっと長いですが全部載せます。前述のjuce_cmake_vscode_exampleリポジトリから引っ張ってきたファイルに追記していったものなので、その名残がちょっとあります。

# Automatically generated makefile, created by the Projucer
# Don't edit this file! Your changes will be overwritten when you re-save the Projucer project!

cmake_minimum_required(VERSION 3.15)

PROJECT(JUCE_CMAKE_ANDROID_EXAMPLE
LANGUAGES C CXX
VERSION 0.0.1
)

# for clang-tidy(this enable to find system header files).
if(CMAKE_EXPORT_COMPILE_COMMANDS)
set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES})
endif()

if (ANDROID)

# defs, some are specific to Android and need definisions in prior to `add_subdirectory(JUCE)`.
add_definitions(
    "-DJUCE_ANDROID=1" 
    "-DJUCE_PUSH_NOTIFICATIONS=1" 
    "-DJUCE_PUSH_NOTIFICATIONS_ACTIVITY=\"com/rmsl/juce/JuceActivity\"" 
    )

# Enable these lines if you use juce_audio_devices API
set(OBOE_DIR "../JUCE/modules/juce_audio_devices/native/oboe")
add_subdirectory (${OBOE_DIR} ./oboe)

# libcpufeatures

add_library("cpufeatures" STATIC "${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c")
set_source_files_properties("${ANDROID_NDK}/sources/android/cpufeatures/cpu-features.c"
  PROPERTIES COMPILE_FLAGS "-Wno-sign-conversion -Wno-gnu-statement-expression")
enable_language(ASM)
endif (ANDROID)

# build JUCE
add_subdirectory("../JUCE" ./JUCE)

# build App code (e.g. libExamplePlugin_Standalone.so)
add_subdirectory(src/main/cpp)

if (ANDROID)
add_library(juce_jni
    SHARED
    dummy.cpp
    )
target_link_libraries(juce_jni
    ExamplePlugin_Standalone
)
target_compile_options(ExamplePlugin PRIVATE "-fsigned-char" )
endif (ANDROID)

if (ANDROID)からendif (ANDROID)まで囲まれた部分が2箇所ありますが、それ以外はデスクトップのCMakeLists.txtと変わりません。前半ではProjucerが生成する定数をいくつかそのまま指定しています。このアプリケーションではpush notificationを使っていないと思いますが、指定しないとビルドに失敗するので残してあります。

後半のポイントのひとつはtarget_link_libraries()で、今回はプラグインプロジェクトのStandaloneビルド(ExamplePlugin_Standalone)をリンクしています。Android用のプラグインプロジェクトとしてビルドできるのは(Shared Codeのビルドを除けば)Standaloneのみで、これはAndroid上ではexecutableではなくshared libraryとしてビルドされます。これがJUCEアプリケーションの本体になりますが、一方でアプリケーションのブートストラップではlibjuce_jni.soが名指しでロードされます。アプリケーションのCMakeLists.txtを書き換えて生成されるライブラリをExamplePluginからjuce_jniにしても良いのですが、なるべく元ファイルに変更を加えずにそのままビルドできるようにしたいので、libjuce_jni.soを別途ビルドするようにしています。

app/dummy.cpp

アプリケーションファイルには、もうひとつ追加が必要です。このCMakeLists.txtdummy.cppというファイルを指定していますが、これはadd_library()に何もソースを指定しないとCMakeがビルドしてくれないためです。空っぽのファイルで十分なので適当に作成しておきます。

app/src/main/cpp/CMakeLists.txt

JUCEアプリケーション本体の部分(juce_cmake_vscode_exampleでいえばsrcディレクトリの内容)は、今回のプロジェクトではsrc/main/cppといディレクトリにコピーします。そしてこの中のCMakeLists.txtの内容を少しだけ追加してあります:

if (ANDROID)  
  
# dependencies  
find_library(log "log")  
find_library(android "android")  
find_library(glesv2 "GLESv2")  
find_library(egl "EGL")  
set(cpufeatures_lib "cpufeatures")  
set(oboe_lib "oboe")  
  
target_include_directories( ExamplePlugin PRIVATE  
  "${ANDROID_NDK}/sources/android/cpufeatures"  
    "${OBOE_DIR}/include"  
)  
  
endif (ANDROID)

target_link_libraries(ExamplePlugin PUBLIC
...
${log}  
${android}  
${glesv2}  
${egl}  
${cpufeatures_lib}  
${oboe_lib}
)

最初にAndroid固有の追加ライブラリをfind_library()で検索し、それらをtarget_link_libraries()で追加しています。

あと、juce_cmake_vscode_exampleではバイナリアセットとしてSVGファイルの追加も指定されているのですが、assetsディレクトリに置くとAndroid assetsと混同してしまうので、juce_add_binary_data()の呼び出しではjuce-assetsという別のディレクトリを参照するように微修正してあります。

app/src/main/AndroidManifest.xml

アプリケーションに加える最後の変更はAndroidManifest.xmlです。<manifest>要素の内容にいくつか変更を加えます。

<supports-screens android:smallScreens="true" android:normalScreens="true" android:largeScreens="true"
   android:anyDensity="true" android:xlargeScreens="true"/>  
  
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>  
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-feature android:glEsVersion="0x00030000" android:required="true" />

<application android:label="@string/app_name" android:hardwareAccelerated="false">  
 <activity android:name="com.rmsl.juce.JuceActivity" android:label="@string/app_name"
   android:configChanges="keyboardHidden|orientation|screenSize"  
   android:screenOrientation="userLandscape" android:launchMode="singleTask" 
   android:hardwareAccelerated="true">  
   <intent-filter>  
     <action android:name="android.intent.action.MAIN"/>  
     <category android:name="android.intent.category.LAUNCHER"/>  
   </intent-filter>  
 </activity>
</application> 

ApplicationやActivityのクラスがJUCEのもので固定になるのが特徴です(筆者のリポジトリではJuceAppは取り払っています)。一応<supports-screens><uses-feature>をProjucerが生成したままの内容で残してありますが、無くても動作するでしょう。<uses-permission>は必要に応じて追加します。

JUCE本体の修正

ここまででアプリケーションはほぼ完成しているのですが、このままビルドして実行しても、何も表示されないブランクActivityが起動するだけです。これは、ブートストラップのセクションで説明したjuce_CreateApplication()をJUCE本体がアプリケーションの共有ライブラリから発見できないのが原因です。

JUCEのモジュールは、どうやらODR (one definition rule)を維持する目的で、全てPRIVATEでリンクされており、これは-fvisibility=hiddenが指定されているのと同等です。juce_CreateApplication()はビルドされたライブラリにコードとして含まれてはいますが、隠蔽されているのでdlsym()で発見できません。JUCEはこの場合JUCEアプリケーションループを開始しないので、単に何も起きずにブートストラップ処理が終了します。この問題は次のone liner patchで修正できます。

https://gist.github.com/atsushieno/7da120ef87826c9d8fdf8ad6542a16f6

この程度の変更で、AndroidでもCMakeで構築したJUCEアプリケーションが実行できるようになります。

vanilla C++ project

既存のJUCEプラグインアプリケーションを移植する

https://github.com/atsushieno/aap-juce-witte-eq には、witte/EqというCMakeで作られたプラグインのプロジェクトを取り込んでビルドしています。このアプリをsubmoduleで指定して、それに対するパッチを当てた上で、ここまで説明してきたテンプレート…に少し手を加えたもの…をCMakeLists.txtからadd_subdirectory()で追加しています。パッチファイルを見ると分かりますが、基本的にはここまでで説明してきたfind_library()の追加などの変更を加えたものです。対象のプラグインのビルドにStandaloneが含まれていなかったのを追加していますが、これも前述の通りAndroidではJUCE本家でサポートされているフォーマットが他に無く、これをshared libraryとして参照する必要があるためです。

とはいえ、この移植は自作のAndroidプラグインフレームワーク向けのプラグイン化したものであり、そのために必要なJUCEモジュールの追加などもこのパッチの中で行っています。

CMakeで作られたOSSのJUCEアプリケーションはまだそんなに無いのですが、他のアプリケーションもこんな感じで移植できるのではないかと期待されます。

witte/Eq on Android

2020作業記録総括

2020年はあまりアウトプットに結びつかない1年(当社比)だったような気がしたので、そもそもどういう流れで今に至ったのかを振り返ってみました。(android-audio-plugin-frameworkに関連する話題がメインなのでMusic Tech Meetupなどの話は書いていません。)

Q1: JUCE_emscripten gallery: 自作プラグインのUIを動作させるのに一番手っ取り早いのはJUCEプラグインUIのwasm化ではないかと思って、GUI ApplicationだけでなくAudio Pluginプロジェクトをビルドできるようにしたり、Web MIDI API統合を実装したりしていました。肝心のUI統合自体は他のことをやっていて進んでいない状況です。UI統合より先にやるべきこと・やりたいことがある感じなのと、AndroidChrome (WebView)でAtomicsがサポートされていないので意味なかった(これは近いうちに実装が載るはず)という話があって棚上げ状態です。そういえばJUCE6対応もやっていない…(Dreamtonics社が先にやるかもしれない)

この頃からメインマシンだったHP Spectreが頻繁にキーボード/トラックパッドが死んでは修理が必要になるようになって、結局7月に買い換えるまでに4回修理に出し、その間まともに作業できない状態が続きました。買って1年も経っていないのに4回は多すぎる。

Q2: オーディオプラグインUI統合まわりでは別のシナリオを考えることにしました。JUCEからの移植は未来の案件ということにして、フルスクラッチのHTML UIのものをとりあえずPoCで作ろうと思いました。それで、(UIとは別の観点で)今年はOSSKontakt/nkiの代替を目指せるようなフォーマットとしてSFZ形式のサンプラーに注目するようになっていたので、当時GUIの無かったsfizzにARIAっぽいGUIをWebViewで付けてみようと思って、LV2 UI拡張をwebkitgtkで作ってみたりしました。sfizzは成長が早くて、今はvstguiを使ったGUIが付いている状態です(ARIA的なものではありませんが)。

sfizzはファイルシステムではなくassetからデータをロードするような変更も加えようとしたのですが(Fluidsynthでは実績があるので)、そこまで綺麗にローダーができていなかったのと、libsndfileまでパッチを当てないと無理っぽかったので、この計画は(この時点では)ポシャりました。

Web UIがちゃんと機能する感じになってきたので、こっちの作業はここで一区切りとしておいて、それまでmda-lv2くらいしか無かったLV2移植を拡充することにしました。sfizzとguitarix、後からfluidsynthの独自LV2プラグイン版が追加されました。これを実現するには、LV2 toolkit(serd/sord/sratom/lilv)と同じようにAndroid用のネイティブバイナリをビルドしないといけないので、ここでcerberoをアップデートして対応する作業をこなしました(毎回NDKの仕様変更が大きくて割と面倒なやつ)。

あとQ2に入った頃にAndroid NDK方面でPrefabが出てきたのでいろいろ試行錯誤して、だめだこりゃってなったりまだまだいけるか…?ってなったりしていましたがやっぱりダメでした(というのが今のステータスです)。割とこれで時間が溶けたはず…

Q3: 台湾のCOSCUPで初めてAAPの話を公にしたり、LV2開発者ガイドを書き始めたり、ついでにMIDI 2.0本を書いたりして(コレはホントにオマケのつもりで始めたやつですが完成度も割と満足しているし一番売れてます)、割とnon-coding taskに時間を費やしていた気がします。MIDI 2.0 UMPを扱うライブラリが一向に出てこなかったので、cmidi2という軽量ライブラリを作って、ついでにLV2とMIDI 2.0を繋ぐものがまだ無かったので、lv2-midi2という拡張機能も作りました。ただまだ使う場面が無いです。AAPには組み込んだのですが、MIDI 2.0に対応しているプラグインが無い状態です。

そして本当はM3 2020秋に向けて作品を作りたかったのですが、完全にインフラが足りませんでした。インフラというのは、MMLと音源設定だけでtracktion_engineベースのプレイヤーに変換できるような仕組みを構築しきれていなかったというところです。Tracktion Waveform 11はVST2のみ、tracktion_engineのプレイヤーはVST3のみのところにがんばってLV2対応を追加…といった感じでちぐはぐで、創作に耐えるシチュエーションではありませんでした。(別に以前にやったような構成でMIDIだけで完結させてからDAWに持っていってもよいのですが、もともと創作が目的ではないし得意でもないので…)

あとsfizzで使える音色と使えない音色があって、たとえばUnreal InstrumentsのKSOPの類は残念ながら全滅だったので、FM音源でも使ってお茶を濁そう…とか思ってFM音色を自分のデータから引っ張ってきたり、SSG音源もほしいよなあと思ってayumiをLV2化したりそれをAndroidで動かしたり、あとJUCEアプリだったOPNplugをAndroidに移植したりとかやっていました。Androidで作品を作るつもりだったのではなくて、この辺なら「Androidでも鳴らせます」っていうデモができるかなーと目論んでやっていたのですが、プラグイン機構が上記のような感じで平仄が合っていなかったのと、プラグイン機構の違いからstateのBLOBに互換性がない問題などが露見して、「そもそもポータビリティのある音楽データが実現できていないのでは…?」みたいな、この業界の課題に直面することにもなりました。

tracktion_engineも複雑過ぎるのか演奏開始するなりクラッシュする状態で、デバッグを続行するにはJUCE統合まわりのビルドも複雑でLV2サポートもビルドが不安定で…みたいな状態で、ダメだ…腐ってやがる…早すぎたんだ…と反省して、ビルドと開発の基盤を整備しよう、となりました。

Q4: M3が終わってから引っ越したりADC 2020に参加したり、Jetpack Compose / for Desktopとかいろいろ調べたりしていて大したことはやっていなかったのですが、今は開発を継続できる体制を整備する作業が進んでいるところです。前回書いた感じの作業ですね。1年前に比べるとやはりだいぶ違うソース構成になっていて、いくつかのプラグインがそれぞれ要素としては稼働していることを考えると、もうしばらく続けるとだいぶtoolchainとしては期待通りのかたちになるんじゃないかという気はします。てか1年前にはJUCEプラグイン移植が何も存在しなかったのか…(独自プラグインフレームワーク用のJUCEモジュール開発については2月に書いていて、プラグインはその後に一気に移植しています)。

今年の積み残し課題は2つあって、GUI統合と音楽プレイヤーなのですが、特に後者と関連する音楽データのポータビリティは大きな課題なので、来年もいろいろ模索しながら進めることになると思います。

パブリックな活動はほとんどしていなかったと思うのですが、LV2仕様やLV2 toolkitにはちょいちょいcontributeしていて、LV2 Wikiにもなぜか自分のプロジェクトやら同人誌(!)やらがチマチマと載り出したので(多分ほとんどがめっさ日本語を解するZrythm DAWの作者氏によるもの)、「何か見覚えのあるやつ」くらいのポジションにはなったような気がします(それまではlilvの.NETバインディングくらいしかやっていなかったので)。

とりあえず振り返りはこれくらいですね。来年の方針については来年考えてもよさそうです。災禍の時勢なのでのんびり考えようと思っています。