fresh digitable

めんどくさかったなってことを振り返ったり振り返らなかったりするための記録

ViewPagerの中に入れたImageViewで拡大縮小やスクロールをするためにやったこと

UdonRoadではツイートに添付されている画像を表示させるため、ViewPagerの中にImageViewを入れて使っている。これまでは特に何も考えず表示させていたが、パノラマっぽい横長の画像やマンガの中の小さな文字などを詳しく見るために画像を拡大できるようにしたくなったので次のような処理を追加した。

github.com

ViewPagerは左右のフリックジェスチャを受け取ってページを切り替えるため、それと競合するような横方向のジェスチャを受け取って処理したいとなると工夫が必要である。具体的には次の二つ。

  • 真横方向のピンチイン/アウト
  • 真横方向のスクロール(ドラッグ)

タッチイベントを受け取った後の処理の流れを大雑把に説明すると次のような感じ。画像の拡大縮小、移動でMatrixを使っている理由は特にありません。

  • onTouchEvent
    • ScaleGestureDetector.onTouchEvent()MotionEventを渡してピンチイン/アウトのジェスチャを認識させる
    • GestureDetector.onTouchEvent()MotionEventを渡してスクロール(ドラッグやスワイプ)ジェスチャを認識させる
    • ピンチイン/アウトまたはスクロールイベントが起きたらスケールファクタや移動量を受け取り、"画像変換のためのMatrix“を更新する
    • ViewParent.requestDisallowInterceptTouchEvent(true)を呼ぶ
    • View.invalidate()を呼んでviewを更新する
  • onDraw
    • 画像の現在のMatrixImageView.getImageMatrix()で取得して"画像変換のためのMatrix“をMatrix.postConcat()で渡す
    • スケールの値が初期値より小さくならないよう補正する
    • 画像が縦横方向に移動しすぎてフレームアウトしないように(若しくは、はみ出すようなサイズのときに背景が見えないように)補正する
    • 補正したMatrixImageView.setImageMatrix()で渡す
    • “画像変換のためのMatrix"をリセットする

肝はViewParent.requestDisallowInterceptTouchEvent(true)で、ImageViewで消費したいイベントが起きた時にこれを呼ばないと親のView(ここではViewPager)にイベントを取られてしまう。

あとは細かい処理やハマりポイントとして、次の2点を挙げておく。

  • 隣のページに行きたそうなドラッグはImageViewで受け付けずに親に渡す(shouldGoNextPage()みたいな名前のメソッドで実装している)
  • ScaleGestureDetector.OnScareGestureListener.onScale()falseを返してもScaleGestureDetector.onTouchEvent()trueを返してくる。スクロールが起きているかどうかを知りたかったらScaleGestureDetector.isInProgress()を呼ぶ

この記事の全ての事象はNexus5X(Android 7.1.2)で確認した。

ExoPlayerのAudioProcessorを実装する

頭外音像定位のデモためのAndroidアプリを作っている。

github.com

akihito104.hatenablog.com

音に独自の効果を加えたいときは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つのスレッドを使って並列に処理している。

f:id:akihito104:20170601151729p:plain:h300:w300 f:id:akihito104:20170601151902p:plain:h300:w300

図(左)再現しようとしている音場、(右)計算のイメージ

また、時間領域における畳み込み計算は周波数領域における乗算で置き換えられることから、FFTとIFFTを使っている。FFTってそういえばちゃんと書いたことなかったな…と思い立って、今回は外部のライブラリを使わず自分で実装した。畳み込み計算をすると音データが長くなるので、元の長さの分だけ返すようにする。はみ出た分は次のフレームの計算結果に足しあわせる。

エフェクト無しの場合

エフェクト無しの場合は受け取ったデータをほぼそのまま返す。単にそのまま返してしまうとエフェクト有りの時との音量差が大きいので、ちょっと小さくしている。また、前のフレームがエフェクト有りだった時には畳み込み計算の結果がオーバーラップしてくるのでその分を足しあわせてやる必要がある。

ByteBuffer getOutput()

queueInput()で作ったByteBufferオブジェクトを渡す。一度渡したオブジェクトが2度と渡されないよう、AudioProcessor.EMPTY_BUFFERを使って置き換えておく必要がある。

ExoPlayerのAudioProcessorで頭外音像定位するオーディオプレイヤーを作っている

Androidのメディア再生のためのライブラリであるExoPlayerには、PCMになった音データを受け取って処理できるようにするAudioProcessorというインタフェースがある。これを実装したクラスのインスタンスを、プレーヤーインスタンスを作る時にいい感じに渡してやることでいろんな効果を掛けることができる。これを使って頭外音像定位を実現するオーディオプレイヤーを作っている。

github.com

主に頭外音像定位のデモ用に自分で使うために作ったものであるが、クローンしてビルドしたりその他ごにょごにょすることで手元の環境でも使うことができると思う*1。音量調整に難があり、たまにプチプチ鳴ることがある(2017/6/9 追記:あんまり鳴らないように修正した)。手元の開発環境はNexus5X(7.1.2)なのだが、これより動作の遅い端末では再生が難しいかもしれない。

頭外音像定位の原理などについては過去に触れたことがある。

akihito104.hatenablog.com

このアプリではいわゆるステレオ配置のスピーカシステム*2で音楽を聴く状況を再現しようとしている。イヤホンで音楽を聴いているのに、目の前にスピーカーが2つ置いてあってそこから音が聞こえるように感じたらいいな、ということである(弱気)。

デモでは普段イヤホンで音楽を聴くときの聴こえ方(頭内定位)と頭外定位の聴こえ方とを対比して体験してみてほしいので、効果を掛けるかどうかをスイッチで切り替えられるようにしている。

もし体験してみたいという方がいらっしゃれば、リポジトリのソースをビルドしたりその他ごにょごにょしていただいて手元の環境でお試しいただくか、リアルの私を捕まえてください。ちなみに私が持っている音源はアニソンか鼓童です。

追記

AudioProcessorでやってることについてちょっと詳しく書いた。

akihito104.hatenablog.com

*1:この記事を書いている途中で外部ストレージ読み取り権限をもらう処理を全然実装していないことに気がついたりした(小声) 2017/6/9 追記:実装した

*2:受聴者の正面から左右それぞれ30度(または45度)の位置にスピーカを置いて音を再生するシステム。このアプリでは30度。