Xamarin Advent Calendar 15日目は、Android 6.0で追加されたMIDI APIを経由して、OpenSLESサポートを組み込んだFluidsynthをP/InvokeしてXamarin.Androidから音を鳴らす方法について説明します。
Xamarinを使ってはいますが、一番手間と時間がかかっている部分はXamarinほぼ関係無いです。でもまあXamarinでなければ正直やりたくない作業でもあります。
今回の内容は概ねこのリポジトリで作っているものについてです。(とは言っても、このリポジトリのsubmoduleとそのsubmoduleがメインではありますが…)
https://github.com/atsushieno/fluidsynth-midi-service
Android MIDIに関心のない人は(99%くらいはそうかと思いますが)、おまけでXamarin.AndroidからNDKでビルドしてP/Invokeで呼び出されるネイティブライブラリをgdbでデバッグする方法について書いておきましたので、そこだけ読まれると良いと思います。
Android MIDI APIとは?
Android MIDI APIについては、(別のところでも書いたとおり)今年の夏コミ(C88)でTechBoosterが出したAndroid Masters!という冊子に詳しく寄稿したので、それを見ていただくのが一番なのですが、ここでも簡潔に…
Android MIDI APIは、Android 6.0に新しく追加された、AndroidとMIDI楽器を接続してメッセージを送受信するためのAPIです。機能的にはデバイスリストの取得と入出力だけをサポートする、低レベルMIDI APIと呼ぶに相応しいもので、ALSA Raw MIDIやWinMMのMIDI API、あるいはWeb MIDI APIが近い内容でしょう。ALSA sequencer APIみたいな抽象レイヤーはありません。
2015年にもなってMIDIを使うことがあるのか?と思われるかもしれませんが、MIDIメッセージは汎用性が高く、単なる楽器の接続以外でも、IoTなどのメッセージング機構として使われたりします。また、昨今ではBluetooth接続が可能になったことで、楽器とPCやモバイルデバイスをハンディに接続できるようになって、使い勝手が向上したということもあります。iOSではサポートされていて、音楽ソフトではAndroidはオーディオ・レイテンシーなどの問題から大きく出遅れているので、何とかリカバーしたいということもあるでしょう。
Android MIDI APIには、MIDIデバイスに接続するクライアントAPIと、自分でMIDIデバイスを公開する場合に使用するデバイスサービスAPIがあります。MIDIアプリケーションの開発者は、その99%が単にクライアントAPIを使用するだけでしょう。今回はこのクライアントAPIの話は(ほぼ)しません。これを読んでる皆さんには最先端を行っていただきます。
さて、デバイスサービスAPIとは何でしょうか? Android MIDI APIの実装では、USB-MIDIとBluetooth MIDIがデフォルトでサポートされていますが、これに加えて、仮想的なMIDIデバイスを実装することもできるようになっています。デバイスサービスAPIは、このために存在しています。独自のデバイスを開発している人や、バーチャル シンセサイザーを公開したい人は、このAPIを実装することで実現できるというわけです。
今回は、このデバイスサービスAPIを応用して、FluidsynthをAndroidのMIDIデバイスとして使用できるようにしたいと思います。
Fluidsynthとは?
Fluidsynthは、主にLinux環境で使われている(けどクロスプラットフォームの)バーチャルMIDIシンセサイザーです。
http://www.fluidsynth.org/
バーチャルMIDIシンセサイザーは、その音色をどこかから調達しなければならないわけですが、Fluidsynthの場合は、「サウンドフォント」と呼ばれる音色定義ファイルの仕組みを活用しています。これはサンプリングデータを音色のバンクとして集めてアーカイブしたもので、sfzやsf2という形式でWebのさまざまな場所で公開・配布されています。音色バンクはGM準拠のものもあれば、そうでないものも数多くあります。MIDI音色用に作成されているというよりは、特定の楽器として作成されVSTのように使用されているほうが多いです。
サウンドフォントのライセンス(というのはまた実体があやふやなものではありますが)がMITなどOSS準拠のものもあり、UbuntuではFluidsynthのパッケージと同様、サウンドフォントのパッケージも公式リポジトリに含まれています。(FluidR3_GM.sf2)
FluidsynthはCで開発されており、概ねクロスプラットフォームなのですが、Androidはサポートされているプラットフォームに含まれていませんでした。AndroidでFluidsynthをビルドしている人は何人かいるようなのですが、Fluidsynthがサポートする音源の「ドライバー」までAndroidでPCM再生ができるところまで手を加えた人はいないようです。Fluidsynthには「ファイルに書き出す」機能があるので、それだけ使っていたりするのかもしれません(その程度なら既存のソースをビルド出来るところまで書き換えれば出来ますし)。
cerbero - Fluidsynthをビルドする(完成版)
以下、しばらくXamarinとはほぼ無関係な話が続きます(というか、ここまで1ミリも関わってない)。libfluidsynth.soがAndroid上に存在していることに何の疑問も感じない・感じる必要がないという人は、マネージドコードから操作する部分まで飛ばし読みすると良いでしょう。
さて、このfluidsynth、LinuxやMacOS Xであれば、cmake で普通にビルドできるのですが(のはず)、Androidではそうはいきません。そこで、Android用にはcerberoというビルドシステムを活用します。cerberoは今やや唐突に登場しましたが、ここは http://qiita.com/atsushieno/items/331b76b08b7f9b4aca0f で書いた内容とかぶるので、詳しくはそちらを見て下さい。ここでは、cerberoを使うことは前提として話を進めます。)
cerberoを使う最大の理由は、glibを問題なくビルドしてくれることです。Android用にglibをビルドするのは大変面倒なので、cerberoに全力で丸投げします。
ちなみに、上記エントリを書いた時は、gstreamer-sdkのサイトでリンクされていたcerberoのリポジトリをチェックアウトしてビルドしていました。このリンクが実は古い内容を指していて、わたしがコードをハックし始めた時は今年の2月くらいから更新が無かったのですが、実はgstreamer-sdk/cerberoとは別にgstreamer/cerberoというリポジトリがあって、こっちは毎日のように更新されていて、わたしがまともなAndroid対応のために加えた修正がほぼ重複していて、自分の苦労は一体何だったんだろうと…愚痴は見なかったことにして話を進めましょう。
cerberoに、多少ビルドシステムに手を加えた上で、fluidsynthのビルド「レシピ」を追加したものは、以下で公開してあります。(gstreamerで使わないモジュールを追加したものなのと、多少やっつけが入っているのと、Android以外で確認していないのとで、現状では本家から単にforkしています…)
https://github.com/atsushieno/cerbero
(古) まともに使えるlibfluidsynth.soをビルドする
ここは多分少し情報が古くて、おそらくもう必要ないのですが(全て上記gstreamer-sdkのサイトの情報が古かったのが悪い!)、cerberoでビルドされる共有ライブラリ(*.so)は、通常のLinux環境でautotoolsを使用してビルドして出来る共有ライブラリと同様、.soファイルはバージョン番号付きの実体(*.so.1など)へのシンボリックリンクになっていました。これは実はAndroidのライブラリローダーと相性が悪く、シンボリックリンクが解決されずにそのまま読み込まれてエラーになってしまいます。
これを解決するには、gccのオプションで正しい"SONAME"を使用するように指定するのが筋なのですが、cerberoをそれなりにきちんと理解しなければならないのと、glibのビルドをきちんと理解しなければならないのとで、面倒だったので、とりあえずso.Xを含むSONAMEと、それを参照しているであろう部分を全てバイナリハックで書き換えて対応しました(やっつけです)。これは思いつきでやったのではなくて、同じことをやっていた先人がいたためです。
https://github.com/MysticTreeGames/Boost-for-Android/issues/44
最新のcerberoを使用したAndroidビルドでは、SONAMEにバージョン番号が入らないような修正が加えられているようなので、このステップは多分不要です(libiconvやlibcharsetは未だにバージョン番号が付いていたので、そこはかとなく不安がありますが)。
ndk-buildを併用して、シングルバイナリの共有ライブラリをビルドする
glibの面倒な点は、実体となるライブラリがlibglib-2.0.soだけでなく、libgthread.soなど多岐にわたる点です。これは面倒なので、出来ればlibfluidsynth.soという1つのファイルに全てまとめてしまいたいところです。
というわけで、少し方針を変えて、fluidsynthはcerberoでビルドせずにndk-buildを使用することにしました。そのためにはAndroid.mkを手書きする必要があったわけですが、fluidsynthのソースツリーの構造は、ソースをいじっていて概ね理解していたので、あまり違和感なく書けました。
https://github.com/atsushieno/android-fluidsynth/blob/master/jni/Android.mk
このAndroid.mkのキモは、PREBUILT_STATIC_LIBRARYを使って、cerberoでビルドしたglibを参照として追加することで、必要な部分だけを最終的なlibfluidsynth.soに含めることができた、という辺りでしょうか。
ただ、ndk-buildでarm用バイナリをビルドしようとしたら、エラーが出て完成できなかったので、arm用には今でもcerberoのアウトプットを利用しています(共有ライブラリも全部apkに取り込んでいます)…
OpenSLESを使用して、Android用オーディオドライバーを作成する
さて、ここまで既に十分苦労してビルド出来たlibfluidsynth.soですが、この中にはAndroid用のオーディオドライバーが何も入っていません。オーディオドライバーが存在しないと、音楽を鳴らしたつもりでも、音が出ません。音が出なくても、合成したPCMをwavファイルなどに出力することはできるわけですが、それではMIDI楽器としては使えません。fluidsynthには、WinMM、CoreAudio、ALSAなど、プラットフォームに合わせたオーディオドライバーが存在しているわけですが、AndroidはLinuxベースのOSではあっても、ALSAもOSSもpulseaudioも存在しないので、自作してやらなければなりません。
ここはひとつ、何とかしてオーディオドライバーを追加するべきでしょう。というわけで、Android用のlow latencyオーディオのためのAPIであるOpenSLESを使用してやっつけました。
https://github.com/atsushieno/fluidsynth/blob/master/fluidsynth/src/drivers/fluid_opensles.c
このOpenSLESですが、参考になる情報が非常に少ない上に、どの資料を見ても最終的にこのブログにたどり着くので(Android NDK ネイティブプログラミングですら、サンプルコードを見る限りだいたいこれです…)、これを読み込んでどうにかするしか無い、というのが実態です。OpenSLESはAndroid2.3から存在しているのですが、仕組みだけlow latencyだけど実物のlatencyが大きすぎる(or, 大きすぎた)ということもあって、あまり使ってもらえなかったということかもしれません…あとインターフェースの初期化とか煩雑ですしね…
ともあれ、まだ実のところ動きはかなり怪しいのですが、一応「何とか音が出る」ところまではもっていけたようです。
Fluidsynthをマネージドコードから操作する
さて、ようやくfluidsynthがAndroid環境でも使えるようになりました。後は、これをマネージドコードから呼び出せるようにすれば、Xamarin環境で使えるようになります。簡単ですね!(白目)
fluidsynthはCで書かれており、fluidsynth.hで公開されているAPIをP/Invokeで呼び出すのは割と簡単です。ただ、fluidsynthは主にLinux環境用のソフトウェアであり、たとえばpinvoke.netなんかでは見つからないわけです。
というわけで自作します。実のところ、これは順番が逆で、fluidsynthを.NETから呼び出すnfluidsynthというものを、だいぶ前から公開しています。今回はこれを使い回します。
https://github.com/atsushieno/nfluidsynth
nfluidsynthの開発はわたしのデスクトップ環境、すなわちLinux上で行われています(いました)。ネイティブライブラリのデバッグは、モバイルのremote targetで行うよりもデスクトップ環境で行ったほうがはるかに効率が良いです。gdbも簡単に使えますし。Android Studio 1.5にはNDKサポートが追加されたはずですが(追加されては消え、を繰り返しているような…)、CLionを買ってデスクトップでも違和感のないネイティブ開発環境を手に入れたほうが良いのかもしれません。(わたしは今のところ滅多にC/C++をいじらないので不要ですが。)
Android NDKのgdbを使ったデバッグの方法については後でざっくり触れたいと思います。(今回Advent Calendarの記事らしい部分はここだけかもしれない。)
ちなみに、NFluidsynthのAPIについては、ここでは説明しません。それなりに素直なバインディングなので、サンプル等を見てもらえれば直感的に分かると思います。重要なのは、AndroidでCベースのネイティブライブラリをP/Invokeするのは簡単だ、ということです。Javaでやろうと思ったら、P/Invokeに相当するnativeメソッドを定義して、さらにJNIブリッジになるネイティブコードをfluidsynth側に追加してやらなければなりません。それは面倒なので、Javaで使いたい人のcontribution待ちということにしています。
nfluidsynthをXamarin.Androidで使う
nfluidsynth経由でマネージドコードからfluidsynthを呼び出す環境が確立できたら、次はいよいよXamarin.Androidから使ってみます。(ちなみに、この「使う」は、まだ「fluidsynthの(ラッパーの)APIを使う」という意味なので、そういう低レベルな部分はちょっと…という人は、ここも読み飛ばしたほうが良いでしょう。)
nfluidsynthにはAndroid用のソースは用意していないので、自前で新規にプロジェクトを作成する必要があります。今回はgit submoduleで追加した後、ソースファイルにリンクしています(書いていて気付きましたが、fluidsynth内部でshared projectとして作っておいても良かったかも…)。PCLは使いません。P/Invokeが必須であり、うまいことXamarin環境に合わせて使えるプロファイルが無かったためです。プラットフォーム別実装を作っても良いでしょうが、今回そこまでする価値は無いでしょう。
ちなみにAndroid用に新規プロジェクトを作ってファイルをリンクで追加するのも良いですが、ソースファイルは*.csproj上に<ItemGroup>の中に<Compile> itemとして追加してしまえば良いだけなので、テキストエディタで手作業でやったほうが早いかもしれません。(ますますshared projectでやれという話だ…)
Android用NFluidsynthは、P/Invoke部分は問題なく動作しますが、ひとつ問題があります。AndroidはLinuxのディストリビューションと異なり、ローカルファイルシステムにサウンドフォントをパッケージでインストールしておいて、それをデフォルトとして参照できるような仕組みはありません。サウンドフォントは明示的にロードしてやる必要があります。assetなどでインストールすれば良いわけですが、標準のAPIからassetの物理パスは取得できないので(たぶん)、apkからassetを取り出していったん展開するなどすれと良いでしょう。(わたしは開発中は/sdcard以下に適当に放り込んだものを読んでいます。)
また、Fluidsynthのコンフィグレーション パラメーターとして、一部特殊な設定値を追加しているので、それを使う必要もあるかもしれません。これはサンプルに上がっているソースを見てもらえればと思います。
Android MIDI APIのデバイスサービスとして実装する
いよいよ大詰めに差し掛かってきました。次は、このNFluidsynthをベースに、Android 6.0で新規追加されたMIDI APIのうち、デバイスサービスAPIであるところのAndroid.Media.Midi.MidiDeviceServiceを実装します。
(Android 6.0のAPIのドキュメントが無いなんて気のせいじゃないですかね、きっと…)
MIDIデバイスサービスは、MIDIデバイスの入力、出力ポートのいずれも実装できますが、fluidsynthの機能は、MIDIメッセージを受信してサウンドを合成して出力する、というものなので、MIDI出力を実装することになります。MIDI出力は、MidiReceiverというクラスを派生させて実装します。このクラスは以下のメソッドを実装すれば足ります。
public override void OnSend (byte[] msg, int offset, int count, long timestamp)
ここで、メッセージの内容に応じて、fluidsynthにNoteOn()
, NoteOff()
などを送ってやれば良いわけです。
MidiReceiverが実装できたら、今度はこれをMIDI OUTPUTのデバイスとして返すMidiDeviceServiceの派生クラスを作成します。そして、MIDI出力ポートを返すメソッドを実装します。
public override MidiReceiver[] OnGetInputPortReceivers ()
は? Input port? Output portだろ? と思った方。アナタはマトモです。まともなのですが、これはつまり視点の違いの問題で、デバイスの気持ちになって考えると、MIDI出力ポートはむしろ入力ポートであり、メッセージは受け取るものだ、ということです。ともあれ、このメソッドで先のMidiReceiverを返せば、このクラスの実装は終わりです。
ちなみに、MIDI出力ポートを「利用」する時は、MidiManagerというシステムサービス(Context.GetSystemService()
とか呼び出すアレです)を経由して、このMidiReceiverから派生したMidiInputPort
を取得して使用することになります。この辺のAPI設計は、端的に言えばメチャクチャですね。クソAPIだと思います。こればかりはわたしが決めたAPIではないので、将来のバージョン、API Level 24あたりでandroid.media.midi2などを作って仕切り直してもらいたいと思っています。camera2のように。禁則事項です
さて、MidiDeviceServiceクラスの実装は簡単に終わりましたが、MIDIデバイスサービスをデバイス内に「公開」して、他のアプリケーションから呼び出して使えるようにするためには、多少のメタデータをAndroidManifest.xmlに追加してやる必要があります。具体的には、intent-filterとmeta-dataが必要になります。内容についてはandroid.media.midiパッケージの説明を参照して下さい。また、これとは別にデバイス情報を定義したXMLが必要です。これも同ページを参照して下さい。
というわけで、最後はほとんど「ドキュメントを読め」になってしまいましたが、これがAndroid 6.0の最先端のMIDI APIの活用、ということになります。
Android MIDI APIを使わない、という選択肢
鋭い読者の方はお分かりかもしれませんが、このMIDI APIを使わなくても、Android版NFluidsynthだけでMIDI操作は可能です。実際、筆者はクロスプラットフォーム、クロスドライバーなmanaged-midiというAPIを作っているのですが、これを使うことでminSdkVersionを2.3まで下げることが可能です(OpenSLESが2.3以降にしか存在しないので、これが最低レベルです)。
Android MIDI APIなんてクソだから使いたくない、他のMIDIデバイスを活用する予定は当面無い、という筆者のような立ち位置の人は、このmanaged-midi APIを使うのも良いでしょう。ちなみに.NETの世界にはまともなクロスプラットフォームのMIDIライブラリが無く、他は全てWinMM依存です。managed-midiは、内部的にはportmidi、rtmidiといったクロスプラットフォームのMIDI APIのラッパーをドライバーとして使用しています。
将来的にはXamarin.AndroidにAndroid MIDI API「を使う」ドライバーを追加するかもしれません。(今すぐしてもいいレベル。でもこのAPIを使うってことは、MIDIメッセージをintent経由で送りつけるってことだし、確かbluetoothやUSBは内部実装的にはバッチで送られているとはいえ、それって遅延はどうなってるの?って話でもあるし、絶対in-processで動かしたほうがいいような…)
未完成
さて、ここまで書いてきた内容は大体出来ているわけですが、実のところOpenSLESオーディオドライバーの実装にはまだ重大な問題があって、自分の使いたい用途で使えるようになるにはまだ修正が必要なところです。まあ、とりあえずこのエントリを書くためには十分なところまではできているかと思います。
おまけ: Xamarin.Androidアプリケーションをgdbでデバッグする
さて、ここまでほぼXamarinと無関係な話が続いてきましたが、以上で終わりです。無駄に目を通して頂いてありがとうございました。
…で終わってしまうと本当に頃されそうなので、最後に少しだけ誰も書かなそうなお役立ちtipsを書いておこうと思います。
Android NDKを使用して書かれたネイティブコードは、gdbでデバッグすることができます。NDKにはndk-gdbというツールが用意されているので、これを使う、ということも出来るでしょうが、今回は(かなり前の方で書きましたが)開発過程でndk-buildを使っていなかったということもあって、ndk-gdbではなく、Android NDKに含まれるgdbserver
を使用してデバッグしたので、そのやり方について説明します。(いずれにしろ、多くの部分がndk-gdbを使うやり方と重複するでしょう。)
ちなみに、これから説明するやり方は、以下のページで見つけたものをXamarinに適用したものです。
https://github.com/mapbox/mapbox-gl-native/wiki/Android-debugging-with-remote-GDB
モバイルアプリケーションをデバッグする時は、開発機側でデバッガーのクライアントを、アプリケーションが動作しているターゲット側でデバッガーのサーバーを、それぞれ使用することになります。gdbserverは、このサーバーということになります。クライアントはAndroid NDKに入っているgdbを使うと良いでしょう。実際には標準的な&&プロトコル非互換でないgdbなら何でも良いと思います。(え、Windowsを使っている? cygwinなりmingwなりでいけると思います。未確認)
実際には、単純なクライアント・サーバーのみではなく、デスクトップ側では、ADB接続をforwardするTCPサーバーを立ち上げて、そのサーバとの間でgdbがメッセージをやり取りする、3実体間の接続になります。
(1) まず、gdbserverをアプリケーションにバンドルします。gdbserverは、Androidターゲット上で動作するものですから、Android NDKに含まれているものを使います。{android-ndk}/prebuilt/android-*/gdbserver
を、gdbserver.soに名前を変えた上で、Xamarin.AndroidアプリケーションにAndroidNativeLibrary
として追加します。先のブログにもありますが、Android OS上では、.soが付いていないとgdbserverが正常に起動できないようです。
(2) アプリケーションをビルドし、実行します。実行したアプリケーションPIDが必要になるので、この順番です。最初の画面が出てくる以前に実行されるコードのデバッグを行いたい場合は、IDE上でどこかしらにブレークポイントを設定して、実行を止めておくと良いでしょう。
(3) デスクトップとAndroidの間でADB接続を確立します。
adb forward tcp:5039 localfilesystem:/data/data/your.app.package.name/debug-pipe
-
adb shell run-as your.app.package.name /data/data/your.app.package.name/lib/gdbserver.so +debug-pipe --attach {PID}
# PIDには実行したアプリケーションのPIDを指定- 正常に接続できたら
Attached; pid = {PID} Listening on sockaddr socket debug-socket
のようなメッセージが返ってきます
- 正常に接続できたら
(your.app.package.nameは適宜AndroidManifest.xmlで定義したパッケージ名に置き換えて下さい。)
最後のコマンドは終了せず開きっぱなしになります。以降の操作は別のターミナルで行います。
(4) gdbを起動して、TCP forwarderに接続します。
android-ndk/toolchains/{arch}-4.9/prebuilt/{host}-x86_64/bin/{arch2}-gdb
adroid-ndk/prebuilt/{host}-x86_64/gdb
2016/06/30補足: Android NDK r11cの時点では、gdbはprebuiltに移動しています。
{arch}, {arch2}の部分には、arm-linux-androideabi
などが入ります。ターゲットAndroidに合わせて適切なものを選びます。({arch}と{arch2}は少し違います。それぞれ該当する、有るものを使いましょう。)
{host}には、デスクトップ環境に合わせて、linux
やdawrin
などが入ります。(おっとXamarinはLinuxをサポートしていないんだった。じゃあlinuxがここに入るはずはないな…本来なら…!)
(5) gdbをリモートサーバーに接続し、共有ライブラリ検索パスを設定します
target remote :5039
-
set solib-search-path {xamarin_app_dir}/Libs/{arch}
-
info sharedlibrary
を使うと、共有ライブラリのデバッグシンボル読み込み状態が分かります
-
正常にシンボルが読み込めた状態だと、以下のような出力になります。
0x9d6e1dd0 0x9d75ace0 Yes /svn/fluidsynth-midi-service/NFluidsynth.Android/Libs/x86/libfluidsynth.so
No /system/lib/libOpenSLES.so
---Type <return> to continue, or q <return> to quit---
No /system/lib/hw/gralloc.goldfish.so
(gdb)
ここまで上手く行けば、後はいつも通りgdbを使えば良いでしょう。
(gdb) b fluid_opensles_audio_run
Breakpoint 1 at 0x9d7097d2: file jni/../build/sources/android_x86/fluidsynth-1.1.6/fluidsynth/src/drivers/fluid_opensles.c, line 218.
わたしの手元ではこれでgdbが使えているようですが、もし何か上手く行かなければ、この節の冒頭で紹介したリンク先に、より正確な手順があるので、場合によってはそれが参考になるかもしれません。この辺りのトピックは、英語圏でもそれほど情報がない領域なので、自ら藪を切り開く覚悟をもってチャレンジしていくと良いと思います。
というわけで
以上で本当に終わりです。それではまた来年(?)をお楽しみに。