ExoPlayerのAudioProcessorを実装する
頭外音像定位のデモためのAndroidアプリを作っている。
音に独自の効果を加えたいときはAudioProcessor
を実装する。AudioProcessor
は前段の処理で展開された音データをPCM形式で受け取り、これを変換して返す。返したデータはその後、ビデオとの同期を取ったり再生可能な形式に変換されたりして、AudioTrack
に書き込まれる。今回のアプリで実装した主なメソッドは次の通り。
boolean configure(int, int, int) throws UnhandledFormatException
渡されるPCMデータのサンプリング周波数、チャンネル数、エンコード形式の種類を受け取る。処理可能なら渡された値を覚えておいて、設定が変わったらtrue
、変わらなかったらfalse
を返す。もし処理できない形式だったら、UnhandledFormatException
を投げる。このアプリでは44100Hz, 2ch, 16bitの形式だけ受け付けることにしている。
int getOutputEncoding()
出力するエンコード形式の種類を返す。使用可能なエンコード形式は@C.Encoding
な定数として定義されている。このアプリではC.ENCODING_PCM_16BIT
を返している。
int getOutputChannelCount()
出力するデータのチャンネル数を整数で返す。このアプリでは2を返している。
void queueInput(ByteBuffer)
音データのPCMをByteBuffer
で受け取って処理する。処理したデータはここではなく後述するgetOutputBuffer()
で返す。最終的な計算結果は適当なByteBuffer
なフィールドに入れておく。このアプリはエフェクトをかけるかどうかのスイッチを持っていて、これを切り替えることでエフェクト有り/無しの音を聞き比べられるようにしている。
エフェクト有りの場合
エフェクト有りの場合はLチャンネルとRチャンネルのそれぞれにHRTFをかける。左側の図のようなステレオ音場を再現するためには、右側の図の計算イメージで示すように4回の畳み込み計算を行わなければならないのだが、この処理が結構重いため、1つの畳み込みを1つのスレッドに割り当てて、4つのスレッドを使って並列に処理している。
図(左)再現しようとしている音場、(右)計算のイメージ
また、時間領域における畳み込み計算は周波数領域における乗算で置き換えられることから、FFTとIFFTを使っている。FFTってそういえばちゃんと書いたことなかったな…と思い立って、今回は外部のライブラリを使わず自分で実装した。畳み込み計算をすると音データが長くなるので、元の長さの分だけ返すようにする。はみ出た分は次のフレームの計算結果に足しあわせる。
エフェクト無しの場合
エフェクト無しの場合は受け取ったデータをほぼそのまま返す。単にそのまま返してしまうとエフェクト有りの時との音量差が大きいので、ちょっと小さくしている。また、前のフレームがエフェクト有りだった時には畳み込み計算の結果がオーバーラップしてくるのでその分を足しあわせてやる必要がある。
ByteBuffer getOutput()
queueInput()
で作ったByteBuffer
オブジェクトを渡す。一度渡したオブジェクトが2度と渡されないよう、AudioProcessor.EMPTY_BUFFER
を使って置き換えておく必要がある。
ExoPlayerのAudioProcessorで頭外音像定位するオーディオプレイヤーを作っている
Androidのメディア再生のためのライブラリであるExoPlayerには、PCMになった音データを受け取って処理できるようにするAudioProcessorというインタフェースがある。これを実装したクラスのインスタンスを、プレーヤーインスタンスを作る時にいい感じに渡してやることでいろんな効果を掛けることができる。これを使って頭外音像定位を実現するオーディオプレイヤーを作っている。
主に頭外音像定位のデモ用に自分で使うために作ったものであるが、クローンしてビルドしたりその他ごにょごにょすることで手元の環境でも使うことができると思う*1。音量調整に難があり、たまにプチプチ鳴ることがある(2017/6/9 追記:あんまり鳴らないように修正した)。手元の開発環境はNexus5X(7.1.2)なのだが、これより動作の遅い端末では再生が難しいかもしれない。
頭外音像定位の原理などについては過去に触れたことがある。
このアプリではいわゆるステレオ配置のスピーカシステム*2で音楽を聴く状況を再現しようとしている。イヤホンで音楽を聴いているのに、目の前にスピーカーが2つ置いてあってそこから音が聞こえるように感じたらいいな、ということである(弱気)。
デモでは普段イヤホンで音楽を聴くときの聴こえ方(頭内定位)と頭外定位の聴こえ方とを対比して体験してみてほしいので、効果を掛けるかどうかをスイッチで切り替えられるようにしている。
もし体験してみたいという方がいらっしゃれば、リポジトリのソースをビルドしたりその他ごにょごにょしていただいて手元の環境でお試しいただくか、リアルの私を捕まえてください。ちなみに私が持っている音源はアニソンか鼓童です。
追記
AudioProcessor
でやってることについてちょっと詳しく書いた。
VideoViewとその周辺のコードを読んだり調査した #m_android_fcr
今作っているツイッタークライアントが、N(Android 7.0)になった辺りからContext
をリークさせるようになってしまったのでいろいろと調査した。
コードーディング会にもお邪魔して集中して取り組んだ。主催者の@operandoOSさん、会場を提供してくださったメルカリさん、ありがとうございました。
背景
このあたり
UdonRoad/MediaViewActivity.java at master · akihito104/UdonRoad · GitHub
UdonRoad/VideoMediaFragment.java at master · akihito104/UdonRoad · GitHub
MediaViewActivity
はViewPager
を持っており、FragmentPagerAdapter
を通して動画を再生するためのViewMediaFragement
をViewPager
にセットする*1。ViewMediaFragment
ではlayout resourceからViewGroup
をinflateしているのだが、ここでinflateしたVideoView
からContext
が漏れている様子。leakcanaryのスタックトレースは次の通り。
* GC ROOT android.media.PlayerBase$1.this$0 (anonymous subclass of com.android.internal.app.IAppOpsCallback$Stub) * references android.media.MediaPlayer.mSubtitleController * references android.media.SubtitleController.mAnchor * references android.widget.VideoView.mContext * leaks com.freshdigitable.udonroad.MediaViewActivity instance * Retaining: 59KB.
コードを読んで調べたこと
次の順で追いかけると、スタックトレースの3段目にあるSubtitleController.mAnchor
はVideoView
自身であるということがわかる。
Cross Reference: /frameworks/base/core/java/android/widget/VideoView.java
=> Cross Reference: /frameworks/base/media/java/android/media/MediaPlayer.java
=> Cross Reference: /frameworks/base/media/java/android/media/SubtitleController.java
スタックトレースの一番上のやつはPlayerBase
のコンストラクタの中でAppOpsService
にセットされているのだが、
Cross Reference: /frameworks/base/media/java/android/media/PlayerBase.java
リソースをリリースする時にこれがなぜか解放されないためにリークしているのではないか、と理解した。
Cross Reference: /frameworks/base/media/java/android/media/PlayerBase.java
PlayerBase
はNで追加されたクラスであるようす。
ぐぐったこと
同じようなリークの現象で困っている人がいたのだが、ButterKnifeのUnbinder.unbind()
をしていないからだと指摘されており、これを適用していい感じになったよありがとう!というやりとりで閉じられている。
ここにきて、僕が気づいていないだけで、僕も同じように参照をうまく解放できていないんじゃないかと思い始めてしまいふりだしへ。
その他のVideoView情報
M(Android 6.0)より前のVideoView
はAudioManager
がContext
を掴んで離さない(強参照している)ためにリークする。応急処置のワークアラウンドとしてつぎのようなコードが紹介されている。
Mの間だけは平穏だった様子。
ちなみに7.0以前のコードにもお茶目があったようで、これは7.1で修正された*2。issue trackerをみればわかることだが気が向いた人は探してみてほしい。
まとめ
SurfaceView + ExoPlayerで君だけのVideoViewを作ろう。根本的によくないコードを書いているんじゃないかという気もしていて、単に取り替えただけでは勉強にならないのでもう少し取り組むことにする。何か分かったら続きを書くかもしれない。