音声信号処理勉強会の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バインディングくらいしかやっていなかったので)。

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

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

「DAW・シーケンサーエンジンを支える技術」第2版リリース

昨年M3 2019秋で刊行した「DAWシーケンサーエンジンを支える技術」の第2版を改訂版としてリリースします。第1版のPDFをお持ちの方は後述の方法ですぐ入手できます。

技術書典10でも新刊とみなされて販売されます(第1版の書籍を第2版としてアップデートしたためですね)。既に購入いただいている方はアップデート版を無償でダウンロードできるようになるはずです(もうなっているかも。わたしには確認できないので実態はわかりません…)。

techbookfest.org

boothで販売中の電子版も更新してあります。第2版のデータを追加したので、別途ダウンロードできるようになっているはずです。

xamaritans.booth.pm

第2版は72ページとしていますが、印刷と同じ数え方でいえば76ページ、前回の56ページから3割増といったところです。主な加筆部分はプラグインGUIのインタラクション、オーディオグラフの実装技術、サンドボックス機構の説明などです。どちらかと言えば少し散逸気味だった初版の構成を見直して、より体系的に内容を編成した部分が大きいです。文字が多くて理解が困難そうだった部分にもいくつか画像を追加しています。

今回は電子版のアップデートのみです。第2版を印刷して販売する予定はありません。かなり客層の狭いトピックを扱っていて、第1版は実のところまだ印刷版の在庫があって印刷コストを回収できていないくらいなので、これで第2版も印刷するというのはいくら何でも道楽が過ぎ経済に対して失礼すぎるというものでしょう。紙版は電子版のおまけ、いわばグッズなので古くても問題ないという方向けに引き続き初版をboothでも購入でき、紙版購入者の方には第2版のPDFをメッセージにてお渡しする運用とします。紙の旧版はこちら:

xamaritans.booth.pm

実はこれでもまだフォローできない層がありまして…紙の印刷版をM3会場なりboothなりで購入いただいた方には、個別にダウンロードリンクやそれが印刷されたQRコードをお渡ししていたので、簡単にアップデートができません(アップロードしたものも古い版を上書きできない/するのが適切とはいえないので)。やむを得ず、第1版PDFをお持ちの方ならmd5sumコマンドで生成できるハッシュをパスワードにしたzipファイルを公開します*1md5sum sequencer-book.pdf のように実行してその出力をパスワードとして使ってください。これでもよくわからない、あるいはどうもうまくいかなかった、という方は、mastodon.cloudなりTwitterなりfacebookなりでatsushienoを探してコンタクトしてもらえれば対応いたします。

https://drive.google.com/file/d/1gcT64EjGLwcg5p_E2GbrwayddYzjyUrI/view?usp=sharing

また、第2版で加筆・修正した部分を知りたいという方は、大変見づらいですがMarkdownのdiffを公開するので、そちらを参照してください。見づらいのはセクションを割とアグレッシブにまるっと移動したり、その結果として実質的に重複していた内容を削ったりなどしたためです…第1版よりはだいぶ読みやすくなったと思うので、diffの読みづらさはご容赦ください。

https://gist.github.com/atsushieno/11cf7e8ba46bfbfdce3638ff9ab91eac

なお、12/26から始まる技術書典10ですが、今回はこの本のほかに純粋な新刊を出す予定はありません。前回から3ヶ月程度しか経過しておらず、これで同人誌の執筆ばかりしていたら本来やるべきコーディングが進まないので、ソフトウェアの開発や創作がある程度満足できるところまで進まないうちは、それ以外の活動は気分転換になる時だけ && イベントの締切までに完成させることを絶対に目標にしないということにしました。書き物は完成した時が出せる時であり、イベントはそれに従うものです。宣言しておかないとまたイベントが近づいた時に同じことを繰り返しかねないので…

まあ前回のMIDI 2.0本は1週間くらいで8割方書いてたので、それくらい時間が取れれば十分なのですが、その時にネタがあるか、その時に無人の荒野を切り拓きたい気持ちがあるか(今はあんまし無いです。そうでなくても日々の生活が他人から断絶しているのでもうお腹いっぱい)、といった諸条件に依存します。

*1:わたしに見える場所でmd5sumが公開されているのを発見したらその時点で削除してどこで誰によって公開されていたかを告知します

prefab: Android Studioの最新ネイティブライブラリ依存解決機構

Android Advent Calendar 202020日目はAndroid NDK方面で今開発が進んでいるPrefabパッケージ機構についていろいろまとめます。タイトルに書いている「ネイティブ」とはC/C++等のコンパイラが生成するCPUネイティブなコードの意味です*1

目次

Prefabとは?

Prefabとは、Android NDKを使ったネイティブコードを活用するアプリケーション開発の世界で新しく登場した、C/C++などのコードのビルドでサポートされるパッケージ解決のための仕組みです。Android Studio 4.0がリリースされた時にアナウンスされた新機能のひとつです。

android-developers.googleblog.com

その後Android Studio 4.1のリリースでもこれが大幅に改善されています。 ("Export C/C++ dependencies from AARs" のセクション)

android-developers.googleblog.com

Android Studioでは現在、(このPrefabに限らず)ネイティブコードサポートの機能がどんどん進化しており、現在Canaryビルドとして開発されているAndroid Studio 4.2 Canary…からArctic Foxこと2020.3.1 Canaryへと改名されたリリースでは、ついにCMakeを使っているプロジェクトでLLDBを使ったネイティブコードのデバッガーがちゃんと機能するようになりました*2。NDKを使っているアプリ開発者はArctic Foxを使うようにしたほうが多分ずっと開発体験が良いです(まあバグを踏む可能性はあります)。

AARパッケージの仕組みと、それだけでは足りない部分

Android SDKとKotlinでアプリ開発をやっている人は、日頃からAARやJARを使って(build.gradle上でdependenciesを指定して)開発していると思います(そうでない人は滅多にいないですよね…? *3)。これは全てMavenの仕組みに乗っかっています。

Mavenは元来JavaOracleなどのやつ)のJARパッケージの依存関係を解決するものでしたが、Androidのパッケージ依存関係も基本的には同様に解決できるので、Mavenが使われるようになりました。Androidのプラットフォームに近いJetpackなどのパッケージはmaven.google.comで指定しますが、他のライブラリはけっこうBintray jCenterから落としてきますね。

さて、これはあくまでKotlinあるいはJava言語のパッケージの話であり、Android NDKを中心とするC/C++その他のネイティブコードの世界に当てはまるものではありません。C/C++のビルド機構とKotlin/Javaのビルド機構は前提が全く異なります。

たとえば、C/C++ではヘッダファイルを#includeで指定する必要がありますが、これらは既存のAARには含まれません。AARにはjniLibsにネイティブ共有ライブラリそのもの(*.so)を含めることができますが、ヘッダファイルはアプリケーション開発者が別途何らかの手段で入手しないと、そのネイティブライブラリに依存するコードをビルドできません。また、共有ライブラリを参照してリンクする際にはその共有ライブラリのファイルも必要ですが、ライブラリのパスを適切に解決するやり方が、ndk-buildの仕組みから少しでも外れると存在しません。

Android NDKを使った開発はCMakeを使ったプロジェクトモデルにシフトしており、CMakeでまともにヘッダファイルやライブラリファイルのパスを指定する手順が無いことが、特にNDKで既存のC/C++資産を使う上での障害となっていました。そもそも、Android向けにライブラリのパッケージが存在するのであれば、clangに-Iだの-Lだのが渡されるようにアプリ開発者がいちいち指示せずとも、パッケージをdependenciesに指定するだけでよろしく解決してほしい、とアプリ開発者なら思うところです。

さすがにdependenciesを追加するだけで自動解決というのは無理(不適切)なのですが、dependenciesを追加してCMakeLists.txtにちょっと依存パッケージの記述を追加するだけで解決してしまう、というのが、今回のトピックであるPrefabの機能です*4

ちなみに、これまではじゃあどうしていたのかというと、ヘッダーファイルを直接リポジトリに取り込んだり、Androidでもビルドしやすいようにビルドスクリプトをndk-buildやAndroidのCMakeサポートでもビルドできるように書き換えたり、AGP (Android Gradle Plugin) が展開するビルド中間ファイルbuild/intermediates/... 以下のパスをCMakeLists.txt中でlink_directories(...)で指定したり(!)していたわけです。当然ながら本家がバージョンアップすると動かなくなることもよくあるわけで(ビルドエラーで済めばまだいいけど実行時に初めて気づくような問題になったりとか…)、メンテナンス性が下がるわけです。

Prefabツールによる状況の改善

prefabメタデータとprefabツールの役割

Prefabは、アプリケーションあるいはライブラリであるDerivedがライブラリであるBaseに依存するとき、BaseのAARに含まれるネイティブコードBase-nativeに依存するライブラリDerived-nativeを、その他の外部のファイルに依存せずにビルドできるようにします。そのためには、Base-nativeC/C++ヘッダーはDerived-nativeのコードに記述する#include ...を解決するためにほぼ間違いなく必要になるので、BaseのAARの中にバンドルします。

AARをビルドするAGPとしては、Base-nativeでビルドされるファイルのどれがDerived-nativeのビルドやパッケージングの際に必要となるかわかりません。ヘッダファイルにも、公開APIを定義するものとそうでない内部実装むけのものがあり、機械的には判別できません。内部実装用ヘッダーはよくincludeではなくsrcディレクトリに含まれているものですが、あくまでパターンであり、公開機能と非公開機能など他の分け方になっていることもあります。またpkg-configをサポートしているライブラリは、利用する側のライブラリなどでもpkg-configでパッケージを解決する前提でlib/pkgconfig/*.pcファイルを含める必要があります。つまりC/C++ヘッダーだけを含めれば良いというものでもありません。

そういうわけで、これを指示するprefabビルド用メタデータBaseのビルドのために必要となります。これはJSONで記述されるもので、その内容は、 https://google.github.io/prefab/ で詳しく説明されています。ただし、このgithub pagesで書かれている情報のほとんどは、prefabコンソールツールの使い方やメタデータのフォーマットに関するものであり、われわれアプリ開発者にとっては無関係ですアプリ開発者はむしろ(後述する)Android Studio 4.1以降でサポートされているbuild.gradleの書き方を知っている必要があります。今回ここでprefabのJSONフォーマットについて説明することもありません。

PrefabはAGPでサポートされていますが、prefabツールは独立して呼び出してビルドシステムが必要とするファイルを自動生成するように作られています。このツールにおける「ビルドシステム」とはCMakeとndk-buildのことであり、生成されるのはCMakeのfind_package()で参照できるパッケージの*.cmakeファイルであったり、ndk-buildでAndroid.mkからインクルードできるスクリプトであったりします。

AGPは、build.gradleに記述されているprefab関連のセクションの内容をもとに、cmakendk-buildを呼び出すより前にprefabツールを呼び出して、生成されたファイルをこれらのツールで参照できるようにコマンドライン引数などを調整します(cmake -DFIND_ROOT_PATH=... など)。

Prefabアーカイブの汎用性

ちなみにPrefabパッケージはAARですが、ディレクトリ構成としてはトップレベルにprefabというディレクトリが一定の内容・構造で存在し、必要なメタデータテキストさえあればよい、という程度で、JARファイルすら存在する必要がありません。実際、GoogleのオーディオI/O用ライブラリOboeのパッケージにはJavaやKotlinのライブラリコードが含まれません。

Prefabパッケージの配布形態はAARであり、AARは単なるzipなので、prefabツールなどを用いずに自分でヘッダー、ライブラリのバイナリ、メタデータなどをアーカイブしても良いです。例えば、Prefabを具体的に使っている例としておそらく最もポピュラーなのはOboeだと思いますが、Oboeのprefabパッケージングはシェルスクリプトで行われています。

https://github.com/google/oboe/blob/master/prefab_build.sh

また、Microsoft/vcpkgにはPrefabパッケージをビルドする機能も追加されています。

vcpkg.readthedocs.io

一方で、パッケージを消費する側を担うprefabツールはndk-buildとcmakeのみを対象としていますが、Prefabパッケージのファイル構成やメタデータの仕組みそのものは特定のビルドシステムを前提としない作りになっているので、prefabツールと同様にメタデータから各ビルドシステムが期待するパッケージ解決ファイル等を生成するツールを用意すれば、他のビルドシステム(IDE固有のものなど)からも利用できるかもしれません。

モジュール

1つのPrefab AARパッケージに複数のネイティブライブラリを含めることもできます。Prefabパッケージ1つには、1つ以上のモジュールが含まれ、この1つ1つがネイティブライブラリ1つ1つに対応します。パッケージのユーザーは、全部ではなく一部のモジュールのみをCMakeのfind_package()などで依存関係として使うことができます。

複数のライブラリの依存関係をスマートに解決するには、1つのPrefabパッケージの中に複数のモジュールを無用にバンドルしないほうが良いでしょう*5。たとえばGNOMEちほーで使われているglib/atk/pango/gtkを全部まとめて1つのパッケージとして配布するのは多分アンチパターンです。一方でglibとしてひとまとめで配布されているものをgobjectとgmoduleglibと…のように共有ライブラリ1つ1つに分けて配布するのもやり過ぎでしょう。

ちなみに本稿執筆時点でglibのPrefabパッケージは存在しません。glibをAndroid向けにビルドするだけでもそれなりに大変なので…参考までに、5年前に書いた記事を貼っておきます。(この頃からNDKとライブラリのパッケージ解決は問題だったという問題意識の共有も含めて…)

qiita.com

Android StudioでPrefabパッケージを使う・作る

現在のAndroid Studioの安定版であるAndroid Studio 4.0以降では、Prefab AARパッケージに含まれるヘッダーファイルやライブラリを展開してアプリケーションから(別途ヘッダーファイルを用意したり不安定なintermediatesのパスを指定したりすることなく)合理的なスクリプトでビルドできるようになっています。またAndroid Studio 4.1以降では、AndroidのライブラリプロジェクトをPrefabとしてビルドできるようにもなりました。

以下に、Prefabによるビルドを有効にしたい場合にbuild.gradleに追加すべき内容を説明します。Kotlin scriptで書いている場合は適宜読み替えてください。(1-a) (1-b)のいずれかと、(2)が必要になります。

(1-a) AGPでPrefabパッケージを使いたい(ネイティブコード依存関係を解決したい)場合は、buildFeaturesの一部としてprefabオプションを有効にする必要があります。

android {
    ...
    buildFeatures {
        prefab true
    }
}

これを有効にしない場合でも、アプリケーション側にネイティブビルドでこのライブラリに依存する部分が無いのであれば、そのprefabパッケージのAARにjniLibs含まれていれば特に問題なくビルドできます。この条件が後で割と問題になってくるのでですが、今はとりあえず気にしないでおきましょう。

(1-b) AGPでPrefabパッケージを作りたい(ライブラリを提供する側としてビルドしたい)場合は、今度はprefabPublishingという機能を有効にする必要があります。そして、prefabセクションでモジュールごとセクションを作り(以下の例ではmylibrary)、そのモジュール名をname、ヘッダーファイルを含むディレクトリをheadersとして指定します。

android {
    ...
    buildFeatures {  
        prefabPublishing true}  
    }
    prefab {
        mylibrary {
            name 'foobar'
            headers '../../external/foobar-master/include'
        }
    }
}

(2)そして、もう1つ実質的に必要になるのでやっておくべきこととして、CMakeビルドのオプションとしてANDROID_STL=c++_sharedを指定します。ライブラリ提供者と利用者の両方で行う必要があります。

android {
    ...
    externalNativeBuild {  
        cmake {  
            arguments "-DANDROID_STL=c++_shared"  
        }
    }  
}

パッケージの作成と参照の解決はこれで出来るようになりました。参照する側はさらにCMakeLists.txtAndroid.mkに修正を加える必要があります。Android StudioでCMakeサポートが正式に含まれるようになっている現在、ndk-buildを使う意義はほぼ無いので、ここではCMakeでのみ説明しますが、追加で必要になる記述はこの程度です。

find_package(mylib REQUIRED)
add_library(myapp SHARED app.cpp)
target_link_libraries(myapp mylib::mylib)

PrefabとAGPのPrefabサポートの現在の課題

AGPのPrefabサポートは、まともな問題意識と解決のためのアプローチを採用している側面があり、一見魅力的なのですが、現時点ではPrefab機構の設計に影響するレベルでいくつか問題を抱えており、安定版に含まれている機能だからといって全面的に依存できる気がしない…というのが、現時点でのわたしの評価です。

以下に代表的な問題点をいくつか出しておきます。全て、自分のプロジェクトをPrefabに移行しようとして直面した問題です。あくまで本稿執筆時点での問題なので、近い将来に解決する可能性があります。(解決してほしい…)

build.gradleでヘッダーディレクトリが1つしか指定できない

prefabPublishingのモジュール別のheadersプロパティは単一の文字列で、複数のディレクトリを指定することができません。これはUnix系ライブラリでいえば「利用者に提示できる-Iオプションを1つしか指定できない」というのに等しい機能制限です。

Prefabパッケージとnon-Prefabパッケージの排他的関係

現在、AGPのNDKサポートでは、あるファイル名をもつネイティブライブラリが複数のAARに含まれていると、内容の競合であるとしてビルドエラーとする仕組みになっています。

現在、Prefabパッケージには、prefabディレクトリ以下にCPUアーキテクチャ別に格納された共有ライブラリと、従来型のjniLibsディレクトリの両方に、同じファイル名をもつ、内容のリンクのされ方が少し異なるものが存在しています。実は、prefabbuildFeaturesで有効にしたアプリケーションでこのAARを参照すると、これらの2つのディレクトリそれぞれの内容が参照解決時に競合となってビルドエラーとなるという(割と信じられないレベルの)問題があります。

この問題を解決するには、build.gradleでjniLibsに一切共有ライブラリを含めない設定にする必要があります。

android {
    ...
    packagingOptions {  
        exclude '**.so'
    }
}

jniLibsに共有ライブラリが含まれないとどうなるのか? 当然ながらPrefabを有効にしていないアプリケーションでUnsatisfiedLinkErrorが起こることになります。あるいはビルド時に何らかの方法で-Lのパスを指定していたらリンク時にundefined referenceになります。

この問題は一見ささいな実装不足ですが、どう解決するのが妥当なのか…と考え出すと、割と面倒な問題でもあります。あと開発チームがNDKチームとAGPチームで分かれて全然問題意識が共有されていなさそうでもあります。絶賛放置されていますし。

全てのネイティブライブラリで同一のlibc++_sharedを使う必要がある

前項の問題と関連する…というよりはこちらのほうが問題の出発点だったのですが、Prefabを使う場合はSTLについてlibc++_sharedを使うことを前提として共有ライブラリをビルドすることが(実質的に)求められます。というのは、AGPlibc++_staticを使ってビルドされたライブラリを全てサポート外として却下するからです。なぜlibc++_staticがサポート外とされるかというと、簡単にC++ODR (One Definition Rule)に違反するような競合を引き起こすからです。

STLはCMakeの場合は-DANDROID_STL=...オプションで指定できます(externalNativeBuild.cmake.arguments)。ちなみにデフォルトの値はc++_staticです。つまり明示的にc++_sharedを指定する必要があります。*6

android {
    defaultConfig {
        externalNativeBuild {  
            cmake {
                arguments "-DANDROID_STL=c++_shared" (, ...)
            }
        }
    }
}

Android NDKにおけるSTLの扱いの詳細についてはこのページを見ると良いです。

developer.android.com

これはおそらくクロスコンパイルによってアプリケーションをビルドする開発環境では多かれ少なかれ同様の問題があるのだと思いますが、Prefabの場合は全てのAARにlibc++_shared.soが(packagingOptionsで排除しない限り)含まれることになるので、"one STL per app"のルールに照らして、1つのlibc++_shared.soを「選ぶ」必要が生じてきます。しかし、NDKのリビジョンが変わる度に別々のlibc++_shared.soが含まれてきて、それがAARにバンドルされてくるとなると、厄介な問題になります。

ライブラリはさまざまな開発者が提供しさまざまな状況のアプリ開発者が利用することになるので、何かしらの包括的な手当てが必要だと思うのですが、現状そういう手当ては何も実装されていません。このリンク先のドキュメントには「ReLinkerを使うとよい」とあるのですが、それならAGPのNDKサポートのビルドステップとして実装しておいてほしい…

staticビルドがサポートされない vs. JNIを利用するコードはstaticでビルドしなければならない

Prefabはlibc++_sharedを前提として設計されているのに、JNIを使っているライブラリではこれを使ってはならないという制約があるようです。

Caution: JNI libraries distributed with Java AARs must not use the shared runtime to avoid conflicting with other libraries and the app. The warnings below still apply. See the documentation for Middleware Vendors for more information.

もう完全にどうしたらいいのかわからない。

ndkports

この他に、GoogleではNdkPortsというプロジェクトで既存のUnix系ライブラリを移植して蓄積したいという目論見があるようです。ndkportsはPrefabの公開時点でAOSPで公開されており、jsoncpp, openssl, curlの3つが含まれていました。そして現在は…

https://android.googlesource.com/platform/tools/ndkports/+/refs/heads/master/ports/

jsoncpp, openssl, curlの3つが公開されています。つまり…開発が進んでもいないし、誰も貢献していません。これ、冒頭のAndroid Developers blogの投稿で「パッチを送ってください」って書いているくらい開放していて、わたしも一度これでglibをビルドしてみようとしたのですが、自分でコマンドを呼び出す以外で出来ることが何もなく、まともに依存関係が解決できなさそうだったので、これは忘れてもよさそうです。Googleは以前にもCDepというプロジェクトでポシャっています。

build.gradleで project(...)で参照している同一プロジェクト内のモジュールへの参照解決が実装されていない

Prefabモジュールを書いて、そのサンプルアプリのモジュールを同じプロジェクト上に作るのはとても一般的で、その際にはproject(...) でライブラリを参照することになりますが、現状のAGPではこのprefabの展開が実装されていません。つまりサンプルプロジェクトがまともに作れないわけです(!)

https://issuetracker.google.com/issues/175291074

こんな根本的な問題がなぜ放置されているのか理解に苦しむレベルですが、要はこれまでのネイティブコードのプロジェクトでは(多分)すでに力技で解決してきているので、破壊的に変更されているわけではないからでしょう。

補足: Prefabトラブルシューティング

…は、書ききれなかったので、この辺の知見は、もしちゃんとまとまったら技術書典10なりzennなりで小冊子にでもして出したいと思いますが、とりあえず困ったらbuild/intermediates/cmake/debug/obj/armeabi-v7a/prefab_stderr_cmake_armeabi-v7a.txtみたいなファイルを(自分のABIに合わせて)読んでみると良いでしょう。

*1:FlutterやXamarinなど一部界隈でKotlinやJavaのことを(プラットフォーム中立なバイトコードと対比する意味で)「プラットフォーム・ネイティブ」などと呼んでいるのとは全然違います。

*2:多分これ以前はCMakeLists.txtを辿ってデバッグ対象のソースファイルを発見することが出来なかったんじゃないかと想像します。

*3:Bazel使いとか、クロスプラットフォームSDK使いでビルドスクリプトをGradle無しで自作する必要がある人とか? 自分がかつて部分的にそうだったわけですが…

*4:Prefabは、prefabコマンドツールの名称でもあり、Android Gradle Plugin(やAndroid Studio)がサポートする機能の名称でもあるのですが、前者の意味で使われる場面はめったに無いので、以降は基本的に機能の名称と思っておけば大丈夫です

*5:複数の応用ライブラリが別々のモジュール依存関係のツリーを構築して、ライブラリのユーザーの手元でバージョンの異なるものが競合しやすくなります

*6:こんなデフォルト値に何の意味が??と思われるかもしれませんが、今デフォルト値を変えたら従来のアプリのビルドが阿鼻叫喚になるわけで…