ものがたり(旧)

atsushieno.hatenablog.com に続く

Falplayer: infinite ogg vorbis player (alpha)

今北産業

ゴールデンウィーク中にMono for AndroidOgg Vorbisプレイヤーを作っていたのですが、何とか実用的なところまで出来上がったので、Android Marketで公開しました: https://market.android.com/details?id=name.atsushieno.falplayer

ソースコードgithubに置いてあります: https://github.com/atsushieno/falplayer-android

既存のメディアプレイヤーで、このFalplayerと同じ機能を実現している一般的なものは無いと思いますが、そのことも含めて、以降でこのアプリについて説明します。

はじまり

大震災の後、いまいちコード書きに集中できなかったこともあって、3月の途中から英雄伝説/空の軌跡SCで(今さら)遊んでいました。しかもオンライン販売されていなかったせいで、店頭まで買いに行ったらFC/SC/3rdのセットの方が安い…というわけでついうっかりセットで買ってしまったせいで、3rd.までクリアする羽目に…

[rakuten:murauchi-denki:27096651:detail]

で、Windows版には.oggファイルとしてBGMファイルが生で入っています。20世紀からの伝統として、FalcomのBGMはたいへんクオリティが高いので、これを聴きたがる人はけっこういまして、たとえばこんなWindowsアプリがあったりします: http://www.forest.impress.co.jp/docs/review/20100210_348148.html

これらの.oggファイル、ゲーム中ではコメントとして埋め込まれたLOOPSTART/LOOPLENGTHという値を使用して、うまいことループできるように作られていて(そこそこ技術が必要)、ループしない場合は適当に1曲として聴けるようにもなっています。

今回は、後述するようないくつかの理由から、Mono for Androidのサンプルを作るという意味合いも込めて、このoggファイルを永久ループ再生できるようなプレイヤーを作ろうということになりました。

アプリケーションの特徴

メディアプレイヤー、というと、既にPowerAMPやWinamp、Meridianなどポピュラーなプレイヤーが数多く存在しているので、今さら感があるように聞こえますが、今回必要なのは、Ogg Vorbis中のコメントを抜き出して永久ループさせてくれるものです。

実際にこれらのプレイヤーのソースコードが眺められるわけではないので、あくまで一般論になりますが、Androidのメディアプレイヤーは、APIとしては、android.media.MediaPlayerを使用します。これが既に一時停止や停止、シーク機能などを提供しているわけですね。

このクラスでは、oggファイルの永久ループはサポートされていません。

.oggを使って永久ループを実現しているゲームは他にもあって、たとえばContrastaAEなんかもそうなんですが、一般的な機能なんじゃないかと思われるかもしれませんが、実のところループを記述する方法は一意ではありません。ContrastaAEにはタグが無いようです。

そんなわけでMediaPlayerはあきらめます。音楽を再生するもうひとつの方法として、android.media.AudioTrackクラスがあります。今回はこれを利用しています。このクラスは、着信音など音声ファイルの再生とストリーミング再生を両方サポートするもので、合成した任意の生PCMを再生する等の場合には、これが一般的な解になるのではないかと思います。

このクラスに、デコードした.oggの生PCMを全部放り込めれば楽なのですが、AudioTrackで指定できるバッファの大きさには、明示されていない限度があるので、展開して数十MBにもなる.oggファイルの内容をバッファ再生することはできません。Ogg Vorbisの内容をデコードして、生PCMをストリーミング再生する必要があります。

また、AudioTrackにはループバック再生の機能が含まれているのですが、あくまでストリーミング再生でなくバッファ再生のためのものなので(ストリーミングで消えていったバッファにシークして戻れるはずはありませんね)、今回は使えません。このループバック機能は、着信音など、短いものをループ再生するために存在しているわけです。

Mono for Android

今回はP/Invoke以外は特に変わったことをしているつもりはありません。イヤホンを抜けばBroadcastReceiverがIntentを受け取るようにしているくらいで、AlertDialogの作り方も凡庸なものです。

ひとつ言及しておく必要があるとしたら、SharedPreferenceは実質的に使えませんでした。これは僕はMono for Androidのバグだと認識しているのですが、アプリケーションをビルドして転送するたびに、過去のアプリを消してしまうので、SharedPreferenceのようなものは消えてしまって使えない(はずである)のです。。

Ogg Vorbisのデコード

AndroidJava APIには、Ogg Vorbisをデコードするためのクラスが存在しないので、何らかの方法で代替する必要があります。

今回、Ogg Vorbisのデコードには、音声を再生できるAndroidバイスであればほぼ必ず含まれているであろう tremolo の共有ライブラリ (libvorbisidec.so) を、P/Invokeで呼び出して利用しています。tremoloは、モバイル環境向けに最適化されたlibvorbis/libvorbisfile、のようなものです。

以前に id:atsushieno:20090325:p1 で紹介した通り、MonoでOgg Vorbisデコーダを実装した(というかJorbisを移植した)ハッカーがいました。僕はそれをmoonlightで利用できるようにした(だけの)ものを作りましたが、実は今回このアプリを作る前に、同じコンセプトでogg vorbisプレイヤーが作れないかと画策していた時に、csvorbisをデコーダとして試してみたことがあります。残念ながらその時は到底まともに再生できる速度が得られませんでした。

(とはいえ、実のところ、今回のアプリでも、AudioTrackのバッファサイズの調整が無いと、まともな速度で再生できなかったので、デコーダの速度というよりAudioTrackの使い方がまずかったのかもしれません。この辺は要再検証。)

そんなわけで、今回はパフォーマンスの観点で問題が一番小さいと思われるtremoloを何とかして使うことにしました。tremoloのバインディングについては、後で少し詳しく書きます。

実のところ、JNIでmonoランタイムの呼び出し→マネージドコードでストリームデコード指示→tremoloにreadをP/Invokeで指示→tremoloからコールバック関数としてdelegateが呼び出される という流れは、それなりにパフォーマンスが悪いんじゃないかという気がしますが、とりあえずはそれなりに再生しているようです…

tremoloバインディング

tremoloのバインディングを作るのは、当初VS2010 (Mono for Android)とAndroidエミュレータ上で行っていて、デバッグで死ぬ思いをしたわけですがw、途中で気付いてLinux上で通常のmono環境で何とか作れました。

今回のアプリでは、ov_open_callbacks()を使って、マネージドコード上で開かれたStreamを再生しています。これを実現するために、ov_callbacksというlibvorbisfileの構造体に対応するC#の構造体を定義する必要があったわけですが、コレがハマりました。

マネージドコードのメソッドを関数ポインタにマッピングさせるには、delegateを使えば良いわけですが、このov_callbacksの中に含まれるポインタの示す先が、構造体に含まれるdelegateフィールドだと、たとえ GCHandle.Alloc (..., GCHandleType.Pinned) でポインタを固定していても、やがて消えて無くなってしまいます(そもそも.NETだとdelegateがpinnedに出来なかったりとか…)。仕方ないのでdelegateは別途保持しておいて、ov_callbacksに対応する構造体では Marshal.GetFunctionPointerForDelegate () でアドレスを保持しています。もしかしたらまだ直さないといけない部分があるかも。

ちなみに、当初はtremoloのヘッダに合わせて作成していて、emulator上では動いたのですが、手元のHTC Desireで試してみたら、どうやら一部の ov_int64_t の部分が32ビットになっていたりなどして、正しい値が返ってきませんでした。HTCが独自に手を加えているのかもしれません。

改善の余地

残念ながら、このアプリケーションは軽くありません。AudioTrackにマネージドコードで生成したバッファを渡している以上仕方ないのですが、そこにP/Invokeのオーバーヘッドが入り込んでいる部分は、可能なら削りたいところです(これはMono for Androidの代わりにJavaクラスで実装してJNIにしたところで大して変わらないというか悪化するかもしれないでしょう)。

現在はov_open_callbacks()を使用して、コールバックの中でSystem.IO.Streamからバッファを読み出すように実装しているのですが、これはov_open()でFILE *を直接渡すようにすれば、ひとつ無駄なP/Invokeが減らせそうな気はします。

ひとつの大胆な変更案としては、libvorbisidec の代わりに、libmediaplayer あるいは libmediaplayerservice に相当するネイティブライブラリに、LOOPSTART/LOOPLENGTHをサポートする機能を追加した上で、自前でビルドして、そのP/Invokeラッパーをやはり自前で作ってしまう、という方法があるのではないかと考えています。libmediaplayer / libmediaplayerservice はAndroid専用なのでLinuxgdbデバッグ出来ないのが難点ですが…。また、これは通常のAndroid NDKではビルド出来ないので、その辺にいろいろ手を加える仕組みが必要になります。が、これについてはまた別の機会に…

ちなみにリリースするようなapkをパッケージしたのもAndroid Marketとか登録したのも初めてなので、未だによく分かってないかもしれません。

どうでもいいこと

アイコンはflickrから適当に見つけてきたハーモニカの写真を適当にPaint.NETで加工したものです。その意図は空の軌跡/Ysシリーズをやったことある人なら分かってもらえるはず(!?)

…ふう、これでやっとクリアした気分になったw