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

JUCE vNext?に入りそうなMIDI 2.0サポートについて

JUCE Advent Calendar 2020 5日目が空いていたので、今週developブランチに追加されたMIDI 2.0 UMPサポートを実装するコードについて解説します。

https://github.com/juce-framework/JUCE/commit/9032f58

先月のADC 2020でJUCEブースに行って「MIDI 2.0をサポートする予定はある?」って質問してきたばかりだったので("We're working on it" みたいな感じでした)、もう出てきたか…!という感じですが、UMP仕様の正式版が決まってからもうすぐ1年になろうとしているので、順当にin a timely mannerで出てきた感じではあります。ADC 2019の頃に「そのうちリリースする」って言っていたMMA (MIDI Manufactures Association) のMIDI 2.0実装ライブラリはどうなったんだろう…

MIDI 2.0とUMPについて(復習)

MIDI 2.0 UMPについてはMIDI 2.0 UMPガイドブック (PDFのみ版)で詳しく解説したのでここで繰り返すことはありませんが、MIDI 1.0ではMIDIイベントとかMIDIメッセージと言われていたものに相当するMIDI 2.0の仕様です。「MIDI 2.0ノートオン」「MIDI 2.0ピッチベンド」「MIDI 2.0ノート別コントローラー」「MIDI 2.0システムエクスクルーシブ8」といったものが規定されています(そんなにMIDI 2.0ってprefixは付けないかもしれない)。

SMF(MIDIファイル、*.midファイル)の仕様や、特定のトランスポート(USBとかBLEとか)で転送する方式の仕様は、MIDI 2.0ではまだ(?)規定されていないので、今回のコードにも関係ありません。またMIDI-CIで新たに可能になった双方向のやり取りに関する仕様もUMPの範囲ではないので、今回のコードに関係ありません。

MIDI 2.0に対応「する」ハードウェアデバイスとしては、Roland A-88 MkIIが知られています。あと最近どこかがタッチパネル系のコントローラー(大昔にあったJazzMutant Lemurみたいなやつ)をMIDI 2.0対応させるって言ってindiegogoかなんかに出していた気がします(うろ覚え…)。MIDI 2.0 USBトランスポートは今年の6月に正式に規定されているはずなので、もう対応しているかもしれません。

プラットフォームAPIとの関係

これを書いている時点で、主要プラットフォームのAPIMIDI 2.0 UMPサポートが含まれているのはAppleの最新版OS上のCoreMIDIのみです。iOS 14.0、macOS 11.0, Mac Catalyst 14.0以降ということになります。Windows APIにもALSAにも対応するAPIはありません。

UMP仕様自体はパケットデータのフォーマットを規定しているといえるもので、この意味ではプラットフォームAPIとは無関係にAPIを規定できるものです。その意味ではプラットフォームAPIに対応するものが無いことは、ほぼ障害になりません。

一方で、プラットフォームAPIが無いうちは、そのプラットフォーム上に「有効なMIDI 2.0デバイス」を登録したりそれを発見したりする術がないので、特定のソフトウェア上のプロトコルに基づいてのみ、MIDI 2.0の機能が使えるということになります。たとえばUSB MIDI 2.0クラスは既に存在している(はずな)ので、USB接続から独自にMIDI 2.0デバイスを発見して読み書きすることは可能でしょう。ただしWinMMやALSAMIDIサポートは未だに1.0のみなので、それらのAPIを使用してデバイスとのメッセージをやり取りしようと思ったら、MIDI 1.0の範囲(精度)に丸めるしかありません。

CoreMIDIが追加されたAppleの各種OSについては、CoreMIDI APIMIDIエンドポイントに接続して、CoreMIDI APIのUMPパケット(MIDIMessage_64など)に変換して送信する(あるいはその逆で受信する)などの実装が追加されています(どちらかというと、既存のC++実装がObjective-C実装に書き換えられているので、MIDI 2.0とは無関係にひとつの大仕事っぽいというのが実態ですが…)。

MIDI 2.0 UMP仕様では、実のところ、MIDI 2.0 UMPをMIDI 1.0の範囲で表現できるデータに変換する方式が規定されており、今回JUCEに追加されたコードでもこれが実装されています。

オーディオプラグインAPIとの関係

これを書いている時点で、MIDI 2.0を直接サポートしているオーディオプラグイン規格はありません。

ただし、VST3.7がリリースされたときに、Steinbergは「MIDI 2.0で規定されている機能をVST3の機能でカバーすることは可能であるという」アナウンスを出していて、MMAはこれをもって「VST3はMIDI 2.0をサポートしている」とアナウンスしています。

(わたしはこれは不正確で不適切なメッセージだと思うので、常に正確なところを表現しようと思っています。実際VST3ではMIDIサポートが落とされた結果、VST2の頃に実現できていたマルチチャネル対応が不可能になっていたりします(具体例)。)

MIDI 2.0の機能をカバーしている」というのは、具体的にはノートエクスプレッションのサポート(MIDI 2.0ではノートの「属性」のサポート)、高精度のピッチベンドなどで、これは実際正しく、MIDI 2.0 UMPでMIDI 2.0デバイスから受け取ったメッセージをVST3のコントローラーメッセージに変換してVST3のフィルター(プラグイン)チェインに処理させることは、部分的には可能かもしれません(DAWMIDIレコーディング機能などと連動させるのは無理かも)。

Roland A-88 MkIIみたいなデバイスがVST3対応のプラグインを作ることも、やろうと思えばOSサポートとは無関係に独自にできるわけです(A-88 MkIIのためにどんなソフトウェアがいま提供されているかは知りません)。

AppleのCoreMIDIには、MIDI 2.0 UMPをあらわすAPIやそれをデバイスとやり取りするAPIは定義されていますが、AudioUnitにUMPを扱うAPIが追加された様子はありません。AudioUnitはVST3とは異なりMIDIを明示的にサポートするAPIが含まれているので、MIDI 2.0 UMPをサポートする段階になったらそのためのAPIが追加されるはず…というのがわたしの理解です(ただAUについてはそんなに頻繁・入念にチェックしていないので、見落としているかもしれません)。

とはいえ、UMPは所詮バイト列で表現できるデータフォーマットにすぎないので、CoreMIDIのMIDI-CI実装がMIDIバイスとホストアプリケーションの間のプロトコル情報のやり取りを適切に処理できていれば、アプリケーションはUMPパケットをバイト列で受け取るだけでMIDI 2.0のメッセージを受信できているかもしれません。

AudioUnitにMIDI 2.0固有のAPIが何も追加されていない現在、JUCEで独自にMIDI 2.0サポートがAudioProcessorなどに追加されていることもありません。VST3にプラグイン開発者が独自に対応するようにJUCEが独自に追加することはあるかもしれませんが、おそらくAudioUnitに正式にMIDI 2.0サポートが追加されるのを待つのではないかと思います。その際にはVST3用のAudioPluginFormatなどにもUMPサポートの実装が追加されることになるでしょう(現在は何も追加されていないはずです)。

UMPサポートAPIの詳細

そういうわけで、まだMIDIバイスにもオーディオプラグインにも十分な(もしかしたら「必要な」)サポートがなさそうなMIDI 2.0 UMPですが、バイトストリームからのUMPの解析とUMP構造の取得、あるいはUMP構造とUMPストリームの生成と仮想的な「送受信」は、今回追加されたAPIでも可能です。今回追加されたクラスはたとえば次のようなものです(全てを列挙はしません)。

  • Packet<numWords> - 1つのUMPをあらわす
  • Packets - add(), clear(), uint32_t* data() などUMPシーケンスをあらわす
  • Factory - makeJRClock(), makeNoteOnV1(), makeAssignableControllerV2() などPacket<T>を構築できる関数をもつ
  • Iterator - uint32_t*から生成され、UMP1つ1つを取得するためのiteratorとなる(個別の要素としてはViewoperator*()operator->()で返される)
  • View - uint32_t*から生成され、1つのUMPをあらわす(こちらにもbegin()end()が定義されているが、これは1パケット中の4バイト単位のデータをiterateするためのもの)
  • Midi1ToBytestreamTranslator - uint32_tのUMPシーケンスをMIDI 1.0のMidiMessagesに変換する
  • Midi1ToMidi2DefaultTranslator - MIDI 1.0入力をMIDI 2.0のメッセージに(精度などを)変換する
  • Receiver - MIDI 2.0のメッセージを受信する役割を担うインターフェース。開発者はpacketReceived(const View& packet, double time)を実装する必要がある。
  • U32ToBytestreamHandler - MidiInputMidiInputCallbackから生成して、pushMidiData (const uint32_t* begin, const uint32_t* end, double time)の呼び出しでUMPシーケンスを渡されると、それをMidiInputCallback.handleIncomingMidiMessage()に流すようになっている
  • U32ToUMPHandler - PacketProtocolReceiver&から生成して、pushMidiData (const uint32_t* begin, const uint32_t* end, double time)の呼び出してUMPシーケンスを渡されると、それをReceiver.packetReceived()に流すようになっている

これで何が出来るの?

これらのAPIだけでどんなことができるかは、具体的な実装なしで説明するくらいしか現時点ではできませんが(今その辺の作業をやっているわけではないので…)、たとえば現在Fluidsynthには部分的にMIDI 2.0の精度でピッチベンドなどを処理するAPIが追加されているので、UMPストリームを何らかの方法で生成して*1、UMPの内容を(サポートされている機能の範囲で)Fluidsynthに渡すようなReceiverを実装して、それをU32ToUMPHandlerで処理するようなアプリケーションを書けば、FluidsynthにMIDI 2.0のデータを演奏させることができるでしょう。あるいはSteinbergが「サポートしている」といっているVST3プラグインに処理させることもできそうです。

とはいえ、まだ正式に発表された機能でもないですし、APIが変更される可能性もあるので、そこは(試すにしても)気をつけて使ってみるのが良いでしょう。

*1:たとえばMIDI 2.0に対応したMMLコンパイラを作るとか! (SMF相当のフォーマットが無いけど)

JUCEで作られたOSSオーディオプラグインをかき集めてみた

JUCE Advent Calendar 2020 3日目のエントリーはゆる〜く自分がこれまでかき集めてきた「JUCEで作られたOSSのオーディオプラグイン」をいろいろ紹介します。

目次

Rationale

そもそも何でそんなのを集めているのか?という話ですが、単に趣味で集めていたわけではありません(いや、あるか?)。まずChrome*1でこのページを見てみてください(宣伝)。

juce-demos.atsushieno.dev

わたしがちょっと手伝っていたjuce_emscriptenのデモとして、世に出回っているJUCE系アプリケーションをWeb Assemblyアプリとして動かしているものです。ファイル関連APIなどいろいろな機能制限もあるため、完全なかたちではないにしても、さまざまなアプリケーションが既に動いています。作ったのは今春くらいなのですが、juce_emscriptenも特に進化していないので*2これが最新版という感じです。

もうひとつ、Android用に自作しているオーディオプラグインフレームワークがあって、JUCEからの移植もサポートしていて、少し手を加えるだけでJUCEプラグインのオーディオ部分を鳴らすことができるものが少なくないので、気が向いた時に動かせそうなものを探しています。

github.com

もちろん、自分のLinuxデスクトップでも使えるオーディオプラグインは常に探しています(何しろ選択肢が少ないので)。わたしのようなユーザーにとっては、ソース付きで公開されているJUCE系のプラグインは良い狩り場(?)になるのです。大概はjucerファイルをちょっと加工すればLinuxでもビルドできるようになるので。

そんなわけで、JUCE系のプラグインやアプリケーションをいろいろ探しては試す癖がついていました。それなりに使えそうな知見になってきた気もするので、この機会にいろいろ紹介しようと思います。こういう契機なので紹介内容はOSS…少なくともソースコードのあるソフトウェア限定です。最近リリースされたVitalとか、(juce_emscriptenの原点である)Synthesizer Vとかは含まれません。

サンプラー → シンセ → FM/PSGエミュレーターエフェクター くらいの雑な順番になっています。

Birch-san/juicysfplugin

github.com

Fluidsynthを経由してサウンドフォント(SF2/SF3)をサポートするサンプラー音源です。MIDI音色に相当するインストゥルメントプラグインとして使うにはコレが手っ取り早くて助かります。SF2はかなり古いフォーマットですが、まだ使われることもあるようです(やや練度の足りない内容ですがこれも2年前にまとめてあります)。Fluidsynthは今でも開発が継続していて、最近ではMIDI 2.0 UMPサポートなども実装されつつあるようです。

移植性の観点では、まずfluidsynthのビルドを取り込まないといけないところで躓くことになるかもしれません。Androidビルドは公式にCIがあるので(わたしがAndroidサポートを追加したときに書いたビルドスクリプトをCI設定してくれた人がいたらしい)、そこから引っ張ってくるのが安牌です。また割と無邪気にFluidsynthを共有ライブラリとして参照解決するので(互換性のないバージョンを参照したり、共有ライブラリのロードパスの関係で問題を引き起こしたりするので、依存関係はなるべく静的に取り込んでビルドするほうが好ましいです)、ホストによってはロードできずに落ちたりすることもあります。

サウンドフォントでよく言及されるのはDebianなどで配布されているFluidR3_GM.sf2ですが、探すと他にもいろいろ見つかります。自分の場合は最近は musical-artifacts.com で見つけた https://musical-artifacts.com/artifacts/1057 を使うことが多いです。

osxmidi/SFZero-X

github.com

SF2ではなくSFZのサンプラープラグインとして作られているのがSFZeroです。osxmidi/SFZero-Xは今では開発されていないオリジナルのstevefolta/SFZeroからの派生版で、音源部分はSFZeroModuleという別リポジトリに含まれているsfzeroという非JUCEモジュールの実装です。

ただ、SFZフォーマットは現在でも継続的に開発されている現役のフォーマットなのですが、SFZeroの音源部分はもう5年近く開発されていないので、わたしがSFZサンプラーとしてSFZero-Xを推薦することはないです。サポートされていないSFZ opcodeも数多くあります。今一番アクティブに開発されていてopcodeのカバレッジも広いOSSsfztools/sfizzですが、これは今はVSTGUIでUIが作られているVST3/LV2音源なので、今回はSFZero-Xの紹介としました*3。どうしてもJUCEでやる必要がある/移植する必要がある場合はこちらでしょう。

TheWaveWarden/odin2

github.com

モダンでクールなGUIを備えた高機能シンセサイザーです。JUCE6で全プラットフォームVST3に対応しているほか、LV2 Porting Projectを使用したビルドにも考慮しています(README.mdで詳しく説明されています)。Surgeと同様Odin2もOne Synth Challengeで数多くの楽曲が作られています

プリセット音色もたくさん用意されているのですが、現状のビルドではバンクに相当するodsファイルが何もビルドされず、ユーザー定義音色の格納場所についても問題を抱えているので、プリセットを1つ1つ手作業で選択していく作業が必要になっています。この辺りが(多分)次のリリースで改善されるとだいぶ実用的になると思います。

f:id:atsushieno:20201202211921p:plain

artfwo/andes

github.com

オシレーター1つのシンプルで移植の難易度が低いシンセサイザーです。もっとも、シンプルとは言っても、このオシレーター自体はPerlin noiseをオーディオ処理に適用するという実験を実装したもので、Linux Audio Conferenceのサイトで論文(というのが適切なのかは分かりませんが)も提出されています。

移植作業の最初の一歩として試すには良い選択肢です。ただしPerlin noiseの処理にそれなりにコストがかかるのか、(自分がAndroid環境で試した限りでは)オーディオ処理はあまり軽量ではありません。素でglitch noiseが発生するほどではありませんが、組み込みデバイスなどで最初に試すのには向いていないかもしれません。

f:id:atsushieno:20201202212453p:plain

asb2m10/dexed

github.com

このはてなブログではちょいちょい登場していますが、YAMAHA DX-7のエミュレーターをなぜかGooglerがAndroid向けに趣味で(?)開発したもの*4を、外部の開発者がデスクトップでJUCEのGUIを付けて公開したものです。DX-7の音色バンクとなるCartのサポートなども追加されています。

音源側のコードであるmusic-synthesizer-for-Androidについては2年前に書いたのでそちらを見てください。

f:id:atsushieno:20201202212020p:plain

jpcima/ADLplug

github.com

これもこれまでちょいちょい登場しているFM音源(OPL/OPN/OPM)のエミュレーターです。実態はlibADLMIDIとlibOPNMIDIにJUCE GUIを付けたものと言えます。

これらのライブラリはさらに別のさまざまなFM音源エミュレーター(libOPNMIDIならNuken, MAME, Neko, PMDWinなど)を包括的にサポートしていて、エミュレーション結果が気にならなければ別のエミュレーターを試せるようになっています。

個人的には、今年はFM音源エミュレーターのコードにコントリビュートしてもsfizzのコードにコントリビュートしてもJUCE LV2のコードにコントリビュートしてもこの開発者が出てきて、この人強すぎでは…??ってなりました。

f:id:atsushieno:20201202212512p:plain

yokemura/Magical8bitPlug2

github.com

ゲーム向けのPSG音源を再現するインストゥルメントプラグインです。オシレーターはシンプルですが、ADSRのほかにLFOや自動ピッチベンドなどゲーム用途らしいパラメーターが用意されているのが、いかにもという感じの使いやすさをもたらしている音源です。

コードベースがJUCE 5.4.7と新し目なので、これも移植難易度が低く、実験には有用です。JUCE 5.4.7はアプリケーション次第では(?)そのままだとLinux gccビルドがコケる鬼門バージョンでもあるので、失敗したらCXX=clangでのビルドを試すようにすると良いでしょう。

f:id:atsushieno:20201202212112p:plain

ffAudio/Frequalizer

github.com

6点マルチバンドEQを実現するJUCEプラグインです。最初やや取っ付きにくいのですが、たぶん小一時間くらい使って慣れると、多機能でよく出来ていることがわかります。リアルタイムで入出力の波形が表示されるので、適用結果を視覚的に確認できるようにもなっています。

取っ付きにくい理由は主に(1)最初に6点それぞれのどこにコントロールポイントがあるのか完全にわかりにくい(細い縦線が見にくいのと、発見できた後もy軸の中央すなわち0dbの位置に点があるのが見えず丸型スライダーで値をずらさないと発見できない)のと、(2)制御点のそれぞれがPeak/Low Shelf/High Passなど10種類以上の選択肢があって、慣れないと意図した曲線を描けないのと、(3)初期値で6点は多分多すぎるためです。これさえ把握していれば多分何とかなります。点の多さはアクティブな点を減らすことで解決できるでしょう(Aボタンがたぶんactivateの意図なんですが、これで制御できます)。

ffAudioにはPluginGuiMagicというプラグインGUIをパラメーターから(たぶん)簡単に構築できるJUCEベースのGUIコンポーネントのコレクションもあり、Frequalizerはこれを使って構築されています。

f:id:atsushieno:20201202212151p:plain

GuitarML/SmartGuitarAmp

github.com

Googleのメディア/アート系プロジェクトに深層学習を応用するWaveNetを使ってギターアンプやペダルの既成品をシミュレートする GuitarML/PedalNetRT というプロジェクトがあるのですが、これをJUCEと組み合わせてオーディオプラグイン化したのがSmartGuitarAmpです。

紹介するのは同じpedalnetを使用している(そしてこのプロジェクトの元ネタでもある)damskaggep/WaveNetVAでも良かったのですが、新しいほうを選びました。WaveNetVAのほうはデモページでさまざまな適用結果を聞くことができます。

ただ、これは環境依存なのかもしれませんが、WaveNetVA同様、CPUコストが非常に大きく、まだまだリアルタイムでのまともな発声は無理だと思います。standaloneでビルドしたアプリでJUCEのオーディオテストがブツ切れになるレベルです。非リアルタイム用途では使えるかもしれない、といったところです。

関連: hacker newsのスレッド

surge-synthesizer/surge-fx

github.com

surgeは、旧くはVermber Audioから配布され今はOSSで開発されている多機能シンセサイザーです。surge-fxはそのエフェクト部分をプラグインとして抜き出してJUCE GUIを付けたものという位置づけになるようです。surgeはsurgeで独自にUIが構築されています。surgeは基本的にJUCEを使っていないようなので今回は紹介しませんでしたが、JUCEを有効にしたビルドオプションというものも存在するようです(patch_playerというコードもあってこれがJUCEアプリケーションになっているようですが、詳しくは未確認)。

surge-fxとして単独で使用したい場面がどれほどあるかわかりませんが、JUCE6にアップデート済なので、JUCE6を使用したビルドの動作確認には有用です。ただ、内部的に巨大なsurgeのコードを(全部ではないにしても)ビルドすることもあって、移植性は高くないかもしれません。*5

f:id:atsushieno:20201202212245p:plain

*1:もしかしたらChromium Edgeでも可かも

*2:春先に自分が手を入れたWebMIDIサポートが取り込まれたくらい

*3:以前はsfizz-juceというものがあったのですが

*4:初期チェックインの後はメンテナンスされていません

*5:半年ほど前にjuce_emscriptenでビルドしようとした時は、xmmintrin.h依存が解決できなくてあきらめたのですが、emscriptenではサポートされている雰囲気もあるので、がんばればビルドできるかもしれません。

M3 2020秋 サークル参加情報

今回も直前まで書かずに来てしまいましたが、明日10/25のM3 2020秋にサークル"ginga"として参加しています。割と最近(数週間くらい)に気づいたんですが、同人音楽サークルとしてgingaって名前だとgingaレーベルと紛らわしいんですよね…*1

追記: 素で書き忘れていましたが(!)第一展示場G-22です。Web会場も銀08としてLV2開発本のみ委託販売します*2

頒布物

音楽技術同人誌3冊の紙版を部数少なめに持っていきます。全部で50冊無いくらい。いずれもイベント価格¥500で頒布します。原価割れライン…! ダウンロード版も会場でお渡しします。

※boothにリンクしていますが、全て電子版へのリンクです。紙版はM3終了後に残った部数を設定して販売する予定です。

xamaritans.booth.pm

技術書典9で発表したMIDI 2.0 UMPガイドブックです。私の知る限り世界初です*3。それなりに売れたのですが発注部数より多く刷られていたので*4、M3でも販売できる運びとなりました。

xamaritans.booth.pm

同じく技術書典9で発表したLV2オーディオプラグイン開発者ガイドです*5Linuxデスクトップで使われている代表的なオーディオプラグイン規格の解説書として、おそらく世界でも類を見ないものだと思うのですが、これは予想通りだいぶニッチだったのでまだまだ在庫があります。*6 展示コーナーとも合わせてたのしめる本です。*7

xamaritans.booth.pm

M3 2019秋に発表した、DAWの仕組みを探るためにソフトウェア構成部品を理解しよう!という本です。これも他に類を見ない書籍として書いたつもりです。こちらは紙版も既にboothで購入できます(上記リンク先から探してください)。

xamaritans.booth.pm

2019年3月の幻想音楽祭でリリースした音楽作品です。自作コンパイラMMLとTracktion Waveform10で打ち込んだデータもお渡ししています。

展示コーナー

今回、新譜を作るつもりだったのですが、ソフトウェアが間に合わずとても着手には至りませんでした…。その話は後で書きますが、取り急ぎ代替となる展示物として、今回の頒布物であるLV2オーディオプラグインをその場で体験できるPCコーナーを設置します。*8

またPCに触れなくても、LV2プラグインにどんなものがあるのか、気軽に立ち読みできるパネル的なものを、書類ケースに入れて置いておきます。

また、これらのプラグインのいくつかはAndroidにも移植しているので、Androidエミュレーター上で試せる範囲で遊べるように環境を用意しておきます。

併せて、Ardour6, QTractor, Zrythmなど、ホスト/DAW側も、ほぼ最新のバージョンをお見せできるようにビルドしてあります。

今回展示できなかったもの

今回は、ほんとうはtracktion_engineを使いつつWaveformなしで最後まで打ち込んでAndroid上で再生できるところまで作りたかったのですが、Android上ではAudioPluginHost上でプラグインから音が出るところまでだけ出来ているという状態です…ここ2週間くらいのAndroid Studio 4.2-alpha(13, 14あたり)でネイティブコードのデバッガーが壊れていてデバッグ作業が困難なので(最近気づいたのですが、Android StudioC++サポート機能はプロプラエタリでAOSPに含まれていないんですね。原因を追求できませんでした)、いつ開発作業が再開できるのかわからない状態です。

Androidから離れて、Linux環境ではMML…とプロジェクトXMLファイル…からコンパイラとtracktion_engineだけで打ち込みができるところまでは出来ています。tracktion_engineにjlv2を追加してLV2プラグインとVST3プラグインを混ぜて利用できるようにしたので限りなく便利です。もっとも、Waveform11 on LinuxはLV2もVST3もサポートしていないので、DAWで微調整が一切できない楽曲になってしまいます(!?)

ひとつクリティカルな問題として、MMLからSMFにコンパイルして、それをJUCE AudioPluginHostのfiltergraphファイルと合わせてtracktioneditファイルにコンパイルして、それを再生するというツールチェインのパイプラインになっているため、プレイヤーが毎回オーディオプラグインをメモリ上にロードするのが重くて制作ワークフローとしてはイマイチです。これはtracktioneditコンパイラとプレイヤーを統合して、MIDIシーケンスの部分だけをhot reloadするような仕組みに作り変えれば改善する見込みです。そのためには.NETアプリケーションであるところのコンパイラをJUCEベースのコードに作り直す必要があるのでお手軽ではなく、今回は先送りにしました。

あと、前回audio plugin portabilityの問題として書きましたが、通常のオーディオプラグインはオーディオプラグインフレームワークを跨いでstateデータを再利用できるように構築されていないので、その解決策を模索していて時間切れになったこともあります。最初から移植性のあるstate情報・パラメーター情報を保存する仕組みを作ってそこに載せないとダメそうですが、これは日を改めて考察をまとめたいと思います。

この辺の話はいろいろ出てくるので、当日会場に来られる方で興味ある方がいたらぜひお話ししましょう。混雑するブースになる予定はありませんし*9

*1:うちは2004年にはてなに引っ越してくる前に使っていた名前なので多分こちらのほうが先なのですが

*2:これはWebイベントの設計が良くなくて、当日の販売状況に応じて販売数を調整できないようになっているためです

*3:最近洋書で他にも出たっぽい話を見ました。既刊本の改訂のついでのようでしたが。

*4:一般的にあることです

*5:もともとM3向けに計画して書き始めていたやつですが、技術書典9のほうが先に来たので

*6:MIDI 2.0本も原価割れなのですが、面倒なので価格を統一した感じです

*7:といっても会場で読む人はいないと思うので、展示を思い返して楽しむ感じになるかと思います

*8:広いスペースがあるわけではないので、そこはご理解ください

*9:これフラグになんねーかなー(

Q3〜10月前半の開発記録

7月の作業記録は別途書いてあって、8月の作業記録はMIDI 2.0本の告知がだいぶ持っていってしまったのですが、他のこともちょろちょろとあったので、最近までの自分用開発記録をまとめておきます。時系列は順不同です。

ADLplug/VST3 on LinuxとJUCE6+LV2

JUCEが 6.0でようやくLinux対応したので、ADLplugのようなJUCEベースのオーディオプラグインがまた本家JUCEだけでもビルドできるようになったのですが、ADLplugは独自にLV2対応を進めていたこともあって、JUCE6へのアップデートは行われていません。

このままだとVST3版は永久に出てこないことになって、LV2版を使っている自分としては特に困らない予定だったのですが、いま自分のaugeneプロジェクトでMML + tracktion_engineを使った打ち込み環境に全面移行を図っていて、そこではまだLV2サポートをホスト側で統合できていないので、VST3版が必要になってしまいました。

本家JUCEに切り替えた上でLV2サポートを切り捨ててビルドしても良いのですが、毎回ビルド切り替えのためにJUCEブランチを切り替えるのも面倒なので、とりあえずADLplugで使われているLV2サポート付きのJUCEをJUCE6にアップグレードしました。

github.com

これをもとに更新したADLplugも公開してあります。

github.com

ひとつ問題になっているのが、JUCE6にアップグレードした段階でVST2 SDKの代替として機能するVeSTigeが、既存のコードだけではJUCEのVST2 Wrapperを十分にビルドできなくなってしまったため、VST2サポートが切り落とされてしまっていることです。VeSTige、もともとはLMMSの一部なのですが、いろんなところで改良版?が作られているので、どこに使えるバージョンがあるのかよくわからないんですよね…。そういうわけでコレを使う限り & 本家VST2 SDKを使わない限り、VST2ビルドは失敗します。

あとVST3版にするとマルチチャンネルで使う時にプログラムチェンジが正しく処理されないという問題があって、これはアプリの問題ではなくVST3の仕様レベルでの問題っぽいというのが現時点での評価です。この問題があるのとJUCE6アップデートに上記の副作用があるのとで、本家にはマージされない可能性が大いにあります。作者もFM音源からは割と足を洗ってsfizzに集中しているそうですし。

fmbank

OPNplugは音色を定義して使えないとあまり意味がありません(プリセットで満足するタイプならそれでも良いのですが、わたしは他人の作ったFM音色を探すことに意義を見出せないので…)。そういうわけで、大昔にFM音源をいじっていた頃に自作していたOPNのFM音色をつかうわけですが、OPNplugなどで使えるフォーマットが全然違うので、まずはOSSでよく使われていそうなopm形式のテキストデータにしてgithubで公開しました。

github.com

FM音色データとして流通?しているものにもいくつかのフォーマットがあって、OPNplugでサポートされているのはwopnというバイナリのフォーマットのみです。この辺のフォーマットの変換にはOPN2BankEditorというプロジェクトが便利です。古いMMLデータ形式から新しいmml2vgm形式(やmucom88形式とかも)、それと各種FM音色エディタの形式がサポートされています。

github.com

ただ、エディタ上にコピペしてペーストするような作業になるので、大量に音色がある場合はコンバーターを頑張って作ったほうが良いかもしれません。わたしはコンバーターを書く時間のうちに手作業で変換が終えられてしまいそうなので手作業でやっつけました…。あとちょっと変換処理に問題があったので修正して反映してもらったりしていました。

この辺のツールの使い方は時間が無限に使えるようになったらmusilあたりに書いていきたいと思っています。

sfizz on AAP

sfzサウンドフォントをオーディオプラグインとして使えるsfizzを、Android assetsから読み込むアプローチをとりあえず一旦放棄して、ローカルファイルとして格納するようにしたら、無事プラグインとしてロードして動作するようになりました。ただなぜかホストに依存するようで、JUCE AudioPluginHostからだとどうもうまくいかないのでまだ検証が必要なやつです。

最近この辺をちょいちょいいじりながら本家のバグを見つけてプルリクなどを送ったりDiscordの開発コミュニティに顔を出したりしていたら、大して貢献しているわけでもないのにcontributorsリストに名前が載ってしまったので、もうちょっとsfzが広まるようにやっていくか…みたいなモードになっています(安い釣り針だ)。ARIA UIの実装実験もAndroidプラグインも既に彼らに知られていたので、その辺が大きかったのかもしれません。協力できるところは協力していこうと思っています。UI Standard GuitarのKSOPとかUI Metal GTXが使えるところまではもっていきたいですし(そこまで手を出せる技術知は今のところありませんが)。

ちなみにsfizzではlibsndfileに依存している部分を置き換える作業も行われていたりするので、これが通るとファイルシステム依存部分が減ってassetからのロードが無理なく実現できるようになるかもしれないので、現時点では今のコードに無理にassetサポートを追加しない方針で様子見です。(libsndfileはFluidsynthでもsf3サポートのために使われているので、libsndfileのファイル名を指定するAPIに依存しないアプリケーションコードを書くことも可能なはずではありますが。)

aria2webのスクショを見てわかる人はわかると思いますが、sfizzはKontaktの次の時代を作るOSSとして発展できるポテンシャルは十分にあると思います(SforzandoがOSSになってLinux対応できればそれでもいいのですが)。Kontakt、Komplete 13でもVST3版が出ることはなかったですし、この辺は世代交代を期待したいところです(とはいえVST3には前述の問題もあるのでVST2が使われ続けることになるのかも…それはそれで不幸な話なのですが)。現状sfzをサポートするOSSはopcodes実装がSforzandoに遠く及ばないとされていたのが、sfizzがえらい勢いで進化しているので、しばらく経つとこの辺の勢力図が変わってくるのではないかと思います。

tracktion_engine app on Android

しばらく前からaugeneというMML => MIDI + AudioPluginHost (filtergraph) => tracktion(edit) という流れのツールチェインを構築していて、最近ようやくMMLからOPNplugやsfizzでギター音源sfzの奏法をMMLで指定して演奏できるところまで実現できたので、次はこれをAndroidに移植する作業を進めていました。

tracktion_engine自体はStepSequencerDemoを動かしたことがあるので*1、基本的には移植で困ることは多分ありません。多分、というのは、StepSequencerDemo以外で使われている機能については使ってみないとわからないためです。実際、Editをファイルからロードして演奏する処理については、Android用にassetsからロードする仕組みはNDKにもJUCEにも無いので、sfizzと同様にローカルファイルストレージにいったんコピーしてからロードするというやっつけ工程が必要になりました。

あとJUCE6がCMakeに対応したので、外部ライブラリに依存しているaugeneもCMake化したほうが楽だと思って移行したのですが、Androidサポートについては、Projucerで行われているプロジェクトファイル生成はCMakeでは全くサポートされていないので、自前でテンプレートを用意するかどうかで試行錯誤した結果、とりあえず従来のProjucer方式でプロジェクトを生成しています。aap-juceのビルドシステムがProjucer前提で組まれているのが最大の理由です。CMake前提のビルドに書き換えようとも思ったのですが、移植対象のアプリがそもそもCMakeに書き換わっていない(そして古いアプリもあるので多分書き換わらない)ので、10月中は無理だと判断しました(この辺をいじっていた頃はM3出展を視野に入れていたのです)。

audio plugin portabilityの問題

Linuxデスクトップ上でMMLで打ち込んで生成したtracktioneditと、Androidに移植したtracktion_engineアプリがあれば完結するかと思われたこの課題ですが、本当の(そして現在進行系の)課題は、複数の環境をまたいで編集・再生できるポータブルな楽曲編集環境を構築するところにあります。

AAPはVST3をサポートできないので(そもそもvst3sdkがまだAndroid NDKでビルドできないので)、VST3でビルドした音源を使った楽曲がAndroid上でシームレスに演奏できることを期待するのはちょっと無理があります。正確には、音源そのものはJUCEでビルドされていてaap-juceでビルドできるのですが、AudioPluginHostで編集したfiltergraph上にはプラグインのstateが生バイナリをbase64変換して保存されており、この生バイナリのデータが他のプラグインフォーマットとは共有できません。つまり、ユーザーが打ち込んだデータのクロスプラグインフレームワーク互換性の問題です。

これは別に自分のプロジェクト固有の問題ではなくて、オーディオプラグインの世界全体の問題です。たとえばRoland Sound Canvas VAをVSTで打ち込んでいたものをAUに置き換えても互換性が無いことになっています。

本当にstateデータを共有できないのかどうか試してみたのですが、JUCEのVST3サポートの実装ではVST3固有の情報を出力に含めていて無理でした(OPNplugの実装でいえば、stateのXMLのroot要素判定に"VST3PluginState"が渡される)。手作業で回避コードを書くこともできるのですが、課題はADLplugという個別のプラグインを動かすことでもなければJUCEベースのプラグインのみパッチ対応することでもないので、そういう対応は現状考えていません。

現状で一番現実的に可能そうなのはtracktion_engineをLV2サポート付きのJUCEでビルドしてAudioPluginHostもaugeneもLV2を使って制作して、それをaap-lv2でビルドしたプラグインを使って再生する、といったところです。JUCEのLV2サポートもホスト側(juce_audio_processors)について実装されている必要があり、現状ではjlv2というモジュールしか見当たりません。ADLplugのLV2サポートなど大半のLV2サポートブランチはjuce_audio_plugin_clientのみなのです。

この問題をどう解決するか、については、そもそも音楽制作環境におけるクロスプラットフォーム・クロスプラグイン環境をどう実現すべきか?という課題について整理して別の機会にまとめたいと思っています。進展があればgithubの自分のプロジェクトである程度まとめていくかもしれません。

*1:ちなみにStepSequencerDemoはjuce_emscriptenを使ってWeb上で動作していて、tracktion_engineの開発者にも知られています

MIDI 2.0 UMPの中にメタデータを埋め込む

今日SMFに近い演奏データのデータフォーマットをMIDI 2.0 UMPベースで実装しようとしていて気付いた小ネタ。いろいろ増やしてMIDI 2.0 UMPガイドブックの改訂版を出す場合はその時に取り込もうと思っています。(改訂版は技術書典サイトとboothではアップデートを出せるので、新たに購入する必要がないようにするつもりです*1。)


MIDI 1.0におけるSMF(MIDIファイル)には、可変長のデータを保存する命令としてSYSEXとメタイベントが利用できます。SYSEXはF0h〜F7hに、メタイベントはFFhから指定長のデータを保存できます。メタイベントにはメタイベント種別があり、MIDIイベントとしては存在しない次の各命令に使えます。

メタイベントID 意味
0 シーケンス番号
1 テキスト
2 著作権表示
3 シーケンス名またはトラック名
4 楽器名
5 歌詞
6 マーカー
7 キューポイント
0x20 MIDIチャネルプレフィックス指定
0x2F トラック終端指定
0x51 テンポ
0x54 SMPTEオフセット
0x58 拍子指定
0x59 調指定
0x7F シーケンサー固有イベント

ところでメタイベントの識別子になるFFhには、実はMIDIイベントが存在しており(システムリセット)、自由に使える領域ではありません。SMFでこれを使えるのは、単に概念として演奏データファイル中にシステムリセットが出現することがあり得ないためです。

MIDI 2.0でメタイベントを表現することを考える場合、MIDI 1.0イベントとMIDI 2.0 UMPは根本的に前提が異なる形式になっているので、半ばゼロベースで構造を考えなければいけません。MIDI 1.0時代のSMFの構成は次のようになっています。

  • SMFヘッダ
    • フォーマット指定 (format 0, 1, 2)
    • デルタタイム分解能指定 (division)
  • SMFトラック
    • MIDIメッセージ配列: 各MIDIメッセージの構造は
      • デルタタイム (7bit-encoded variable length)
      • MIDIイベント (2 / 3 / nバイト)

MIDI 2.0にはUMPとしてJR Timestampメッセージがあるので、デルタタイム指定の代替として機能するかもしれません。ただしJR Timestampのタイムスタンプ値は16ビットで1/31250秒単位のフレーム数を指定するので、たかだか2秒くらいしか表現できません。全音符・全休符などを表現するには複数のJR Timestampメッセージが必要ということになります。これはいまいちなので別の表現形式のほうが適切なのかもしれません(未検討)。

話を少し本題に戻しましょう。SMFにしか存在しない概念であるメタイベントをMIDI 2.0 UMPの上に構成するにはどうすればよいでしょうか? MIDI 2.0には次の3通りの可変長データ表現があります。

SYSEXはあくまでSYSEXなので、わかりやすく区別するためにはMDSを使うのがシンプルな解に思えます。しかしMDSにはSYSEX7やSYSEX8に無い特性があり、メタイベントには適していません。なぜでしょうか? その答えはMDSが分割送信できるとされていることにあります。

MDSはMIDIケーブルを通じてファームウェアアップデートを実行するような場面で活用できるように設計されたものですが、分割送信できるデータはいつ終端が届くかわからず、先頭と末尾の間にJR Timestampが含まれる可能性もあります。テンポ変更や拍子指定などのイベントでは正確なタイムスタンプが重要ですが、MDSではデータを最後まで受信しないと完全なMDSを取得できません。UMPをストリーミング処理していると、MDSを最後まで受信した頃にはもう処理時間が変わってしまっている可能性があります。UMPをバッファリング処理しないとメタイベントを正しいタイミングで取得・処理できないというのは、効率的ではありません。

あとは、UMPで予約されている領域を活用する方法もありますが、UMPの基本設計のひとつとして、可変長データを固定長データのフォーマットに変形する仕組みがあります。これを崩さずに可変長データを送信するのであれば、8ビットをフルに使えるSYSEX8を使うのが一番無難でしょう。適当にManufacturer IDを使う手もありますが、ユニバーサルSYSEXでSMFのシステムリセットのようにファイルへの保存があり得ないような命令の領域を使えば、無難にメタデータを格納できるのではないでしょうか。MMAのユニバーサルSYSEXのリストを眺めてみると、non-realtimeの7Bh〜7Fhの辺りは問題なく使いまわせそうです。

そういうわけで、SMFにおけるメタデータを表現するなら、おそらくSYSEX8を使ってユニバーサルSYSEXの一部領域を乗っ取りつつ、既存のパケットフォーマットを使い回すのが良さそうだというのが、現時点での自分の理解です。

*1:約束するというものではなく、債務者たるわたしが進んでこれを履行するときは債務の履行になる特殊な債権債務関係()と理解してください

技術書典9で買って読んで良かった新刊リスト

技術書典9の気になった新刊リストその2…を書くタイミングではないので、実際に買って読んだ本について一昨日くらいに書いた感想ツイーヨの配列を返します。手抜きでアレですが読まれてほしい〜

あと感想をmentionにしてしまったので出し方が変になっちゃうけどこちらも:

まだあと6時間くらいは買えるので(電子版の販売はその後どこかしらで復活するはずですが)、今のうちにどうぞ…!