音を再生するのは簡単、停止と再開は闇というおはなし

この記事は Voicy Advent Calendar 2021 4日目の記事です。

Androidアプリエンジニアのぬまさんです。2020年9月にVoicyにジョインし早1年が過ぎました。仲間と切磋琢磨しながら開発しています。


はじめに

VoicyのAndroid収録アプリでどのように音を再生しているか、長時間音声を再生する対応を行った際得た知見を共有します。


音を再生する仕組み

Voicyではアプリで一つの配信を通常10分間でチャプターとして音声を区切る構成になっていますが、音声をWebでアップロードすることができ10分以上のチャプターを埋め込むことも可能です。

f:id:okayan_m:20211205010135p:plain
Voicyのいち配信を表すStoryとChapter

ThreadでAudioTrackを使い音を再生しています。 AudioTrackはバイトデータをストリーミングして再生するためのクラスです。

以下はAudioファイルをバイト変換 する処理です。

Webでアップロードされた長時間音声(時間は一定ではなく大体60分)を再生しようとすると、AudioファイルをByteバッファへの変換処理をする際、Out of memory が発生していました。 メモリ使いすぎですね。largeHeapを許可していてもおきてしまいます。

gist.github.com

Low Media API である MediaCodec + MediaExtractor + AudioTrackとIOスレッドを使ってバッファ変換及び再生を試みましたが、再生までは容易でしたが、停止・再開 する際 オフセット位置をバッファとして特定できず断念し、再生アプリで採用しているExoPlayerへ移行しました。

バッファ変換は色々数式が必要で理解するのに苦しく、オフセット位置の特定は私の技量では難しいと判断しました。 奮闘のあしあととして MediaCodec + MediaExtractor + AudioTrackでの実装を載せておきます。getStopPosition()はオフセット位置を特定したいメソッドですが特定できていないですw


ExoPlayerでMP3を再生

収録アプリと再生アプリは再生する音声のフォーマットに違いがあります。

再生アプリではモダンな配信プロトコルであるDASH・HLSを再生する必要がありました。 収録アプリではmp3ファイルを再生します。

HlsMediaSourceではなくProgressiveMediaSourceを使用します。seekTo関数を使用してシークできるようにしたいと思います。 一部のファイル形式では、setConstantBitrateSeekingEnabled関数を使用する必要があります。

Customization - ExoPlayer

こちらもシーク処理でハマり実現できるまで時間を要しました。 seekToメソッドを使用すると停止位置よりやや早い時間から再生されてしまいました。 DefaultExtractorsFactoryインスタンスにsetMp3ExtractorFlags()メソッドでフラグのFLAG_ENABLE_INDEX_SEEKINGを指定すると解決しました。

長時間音声に対応し、音声の再生はファイルを指定して投げればあとは勝手に再生できるようになってるが停止や再開やシーク処理はまだまだ私には超えられない壁だということがわかりました。

鬼滅の刃より:「悔しいなぁ 何か一つできるようになっても またすぐ目の前に 分厚い壁があるんだ すごい人はもっとずっと先のところで戦っているのに 俺はまだそこに行けない」

去年書いた記事で、ExoPlayerでコンテンツを保護するためのDRMといった技術を紹介しています。 numamyk.medium.com


読んでくださりありがとうございます。

voi-chord(ボイコード)というチャンネルで技術カンファレンスの振り返りや日々の開発にまつわるはなし、Voicyエンジニアの近況など音声で発信しているので是非聴いていただきたいです!

次回は @d_saito2110さんです! お楽しみに!