2022年6月30日木曜日

Android版東方VGSでワイヤレスイヤフォンが使えない件の調査状況

ここ数日、Android版の東方VGSがワイヤレスイヤフォンを使うと再生できないという不具合の調査をしています。

https://github.com/suzukiplan/tohovgs4-android/issues/34

現象は モダンUI でのみ発生して RETRO UI では発生しません。

モダンUI と RETRO UI では内部的に音声の再生方法が異なります。

  • モダンUI = AudioTrack
  • RETRO UI = OpenSL/ES

という訳で、AudioTrack の使い方が誤っているのかも?と疑って調査をしたのですが、実装レベルでの誤りは見つかっていません。

「AudioTrackの初期化が失敗している」といった分りやすい事象が起こってくれれば良いのですが、そのような形跡はなく、疑わしいログ出力もありませんでした。

どうやら、ワイヤレスイヤフォンを接続するとAudioTrackが見た目上は正常に動くけど、バッファリングしたデータが勝手に破棄されるような挙動をしているようです。現象発生時の adb logcat を確認したところ、OS の bluetooth インタフェースと思しきものが一時停止(suspend)の状態になっている事が確認できす。

06-29 21:11:18.367  2320  2728 I btif_av : system/bt/btif/src/btif_av.cc:3098 btif_av_stream_suspend: btif_av_stream_suspend
06-29 21:11:18.368  2320  3010 I bt_bta_av: system/bt/bta/av/bta_av_api.cc:281 BTA_AvStop: BTA_AvStop: bta_handle=0x41 suspend=true
06-29 21:11:18.403  2320  3010 I bt_btif_a2dp: system/bt/btif/src/btif_a2dp.cc:131 btif_a2dp_on_suspended: btif_a2dp_on_suspended: ## ON A2DP SUSPENDED ## p_av_suspend=0x6e002963c0
06-29 21:11:18.403  2320  3010 I bt_btif_a2dp_source: system/bt/btif/src/btif_a2dp_source.cc:733 btif_a2dp_source_on_suspended: btif_a2dp_source_on_suspended: state=STATE_RUNNING
06-29 21:11:18.403  2320  3010 I bt_stack: [INFO:a2dp_encoding.cc(503)] ack_stream_suspended: result=SUCCESS_FINISHED
06-29 21:11:18.403  1123  1756 I BTAudioProviderSession: ReportControlStatus - status=SUCCESS for SessionType=A2DP_HARDWARE_OFFLOAD_DATAPATH, bluetooth_audio=0x0200 suspended

多分、アプリ → AudioTrack::write → AudioFlinger → bluetooth I/F → bluetooth earphone という流れで音声ビットストリームが流れていく筈で、suspend になった bluetooth インタフェースが音声ビットストリームを破棄しているから AudioFlinger が永久に stream end を検出できず、その結果 AudioTrack の PlaybackPositionUpdateLister が永久に発火しない(発火しないと音楽の再生が進まない)状況に陥っているのではないかと推理しました。

今回のテストで3000円ぐらいの激安ワイヤレスイヤフォンを調達したので、ハード(ワイヤレスイヤフォン側)の問題かも?と疑って、そのワイヤレスイヤフォンをiPhoneに接続してテストしてみましたが、iPhoneでは全く問題無く再生できました。

なので、ハードではなくソフト(アプリ or OS)側の問題であることは間違いありません。

幸い、OpenSL/ESは問題無く動作するので、モダンUI も RETRO UI と同様、OpenSL/ESを使うようにするという回避策もあるかもしれませんが、OpenSL/ES には別の問題があります。(2016年以前のAndroid版東方VGSでは長い間この問題に悩まされていました...)

OpenSL/ESだと何故か、バックグラウンド再生をすると音飛びが発生する仕様です。これはAndroid 8以降で発生するようになったOS側の仕様変更によるもので、Android 12でもまだ治っていません。(OpenSL/ES自体がマニアックなAPIなので多分治ることは無いと想定しています)

※RETRO UI のバックグラウンド再生を制限しているのはこれが理由です(RETRO UI は C言語で作られている関係で、C言語で使えるオーディオ・インタフェースを使わなければならないため、AudioTrackに置き換えることができません)

念の為、AudioTrack→OpenSL/ESに実装を書き換えてテストもしてみましたが、やはりバックグラウンドの音飛び問題は健在でした...

つまり、OpenSL/ESを使ってしまうと事実上バックグラウンド再生に対応できないため、対応したければAudioTrackを使うしかないというのが実情です。

なお、exo-player という Google が公開しているAndroid用動画再生ライブラリ(OSS)があって、その実装でも AudioTrack が使われています。

exo-player は Android版YouTubeでも使われているので、問題のワイヤレスイヤフォンを使ってAndroid版YouTubeで動作確認をしてみましたが、特に問題無く再生できるようです。

東方VGSとYouTubeの音声再生では、音声ビットストリームのエンコーディング形式に違いがあります。

  • 東方VGS: 22050Hz 16bit mono の raw PCM
  • YouTube: AACなど(動画のコーデックを見て切り替え)

bluetoothの帯域はかなり狭いので、リアルタイムに音声データを通信しようとすると raw PCM だと十分なバッファを確保できません。そのため、AAC等の圧縮形式でデータパケットを流す必要があるのですが、もしかするとAudioTrackはその辺を気にせず raw PCM でそのまま bluetooth にデータを流しているかもしれません。(Androidのベーシック層の設計思想は割とこういうパターンが多いので...)

その場合、AudioTrackのエンコーディングを raw PCM から AAC にして、MediaCodec で AAC エンコードしたデータをwriteしてあげれば何とかなるのかな?(OpenSL/ESの場合、そもそもraw PCM以外 buffer write できない仕様だから、bluetoothへ流す時にOS側でAACなりにエンコードしているから正常に動作している...という推測が成り立ちそうな気がします)

AudioTrackをAPI仕様通りに扱っていて正常に動かないので、本件はOS側の不具合だと言える気がするので、一旦この件の対策はせずにOS側の対策を待つしかないかもしれません。

まぁ、そんなこと言ってたらAndroidでプログラムなんか作れない訳ですが...

APIが必ずしも仕様通りに動くと思ってはならない

という訳で内心は早々にギブアップしたいところではありますが、もう少し粘って調査します。

追記: 無事回避策が見つかったので qiita で詳述してみました。(これで、同じ問題に躓く人が出てきた時、ググれば助かる...かもしれない)


 

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。

合理的ではないものを作りたい

ここ最近、実機版の東方VGSの開発が忙しくて、東方VGSの曲追加が滞っています。 東方VGS(実機版)のデザインを作りながら検討中。基本レトロUIベースですがシークバーはモダンに倣おうかな…とか pic.twitter.com/YOYprlDsYD — SUZUKI PLAN (...