AndroidのMediaPlayerで再生速度を変更する場合のノウハウ

# 概要

[MediaPlayer  \|  Android Developers](https://developer.android.com/reference/android/media/MediaPlayer)

Androidで音楽再生を行う際に、一般的にはMediaPlayerクラスと使用すると思います。このクラスには、Android 6.0以降から再生速度の変更ができる機能が追加されています。しかし、この機能は端末によってかなり動作が変わってくるため、機種毎の違いにかなり気を遣って実装する必要があります。ここでは、その実装方法について書いていきます。ここに記載するコードでは、必要最小限の部分のみを抽出し、エラー処理やバージョンの分岐などは省略してあります。
端末毎の違いをどう吸収するかという話なので、いわゆるバッドノウハウに近い話です。
ちなみに、ffmpeg等のライブラリを使っている場合は恐らく関係無いかと思います。

# 実装方法

ここでは、MediaPlayerで再生開始後に任意のタイミングで再生速度を変更するような処理を考えます。

一般的には次のようなコードになります。

```java:プレイヤーの再生開始
MediaPlayer mp = new MediaPlayer();
mp.setDataSource(context, mediaUri); // mediaUri: メディアデータのURI
mp.prepare();
mp.start();
```
```java:再生速度を2倍に変更
float speed = 2.0f;
mp.setPlaybackParams(new PlaybackParams().setSpeed(speed));
```

# Xperia Z3で発生した問題

上記のコードでは、一部機種でメディアファイルの種類によって再生速度が変更されない場合があります。具体的には、Xperia Z3 (SO-01G) で確認しています。ちなみに、MP3では可、AACやFLACでは不可という事象になります。

なぜ速度が変更されないかの具体的な理由はよく分からないのですが、対策は分かっていて、MediaPlayer#prepareメソッドを実行する前に一度再生速度を変更する必要があります。正直ただのバグだと思うのですが、なぜかそういう挙動になります。もしかしたら別に対処方法があるかもしれません。知ってたら教えて下さい。
再生中に再生速度を変更するような機能を実装する場合、次のようなキモいコードになりました。

```java:プレイヤーの再生開始
PlaybackParams params = new PlaybackParams();
MediaPlayer mp = new ediaPlayer();
mp.setDataSource(context, mediaUri); // mediaUri: メディアデータのURI
mp.setPlaybackParams(params.setSpeed(1.0001f)); // 一時的な速度変更
mp.prepare();
mp.start();
mp.setPlaybackParams(params.setSpeed(1.0f)); // 速度を戻す
```

prepareメソッド前の一時的な変更で1.0を指定しても無効だったので、float型の精度を超えない程度で1.0に近い値を設定してます。ちなみに、1.00001fは無効でした。設定する値は別に何でも良いのですが、何らの影響で意図しない再生が行われた場合の影響を最小に抑えています。startメソッド前に再生速度を1.0に戻すと、同様に速度変更ができなくなります。なので、再生速度を1.0に戻したい場合は、再生開始後に再生速度を変更します。

# setNextMediaPlayerの実装

Galaxy SシリーズおよびHuawei 端末のユーザから、まったく別の問題が報告されました。事象としては、再生が2重に行われる(Huawei Ascend Mate7)、再生を開始するとすぐに停止する（Galaxy S8、Huawei nova）といったものになります。

これは、MediaPlayer#setNextMediaPlayerを使ったアプリの作りに起因した問題でした。setNextMediaPlayerメソッドは、次に再生するMediaPlayerオブジェクトを設定することで、現在のプレイヤーの再生が終了すると、すぐに次のプレイヤーの再生を開始します。これにより、音楽の切り替わりで間を空けず継ぎ目無く再生するギャップレス再生を実現することができます。

問題が起こるのは、次のようなコードになります。

```java:次の曲を準備してプレイヤーを再生開始
nextPlayer = new MediaPlayer();
nextPlayer.setDataSource(context, nextMediaUri); // nextMediaUri: 次のメディアのURI
nextPlayer.setPlaybackParams(params.setSpeed(1.0001f)); // Xperia対策
nextPlayer.prepare();

curerntPlayer = new MediaPlayer();
curerntPlayer.setDataSource(context, currentMediaUri); // currentMediaUri: メディアデータのURI
curerntPlayer.setPlaybackParams(params.setSpeed(1.0001f)); // Xperia対策
curerntPlayer.prepare();
curerntPlayer.setNextMediaPlayer(nextPlayer);

curerntPlayer.start();
```

```java:再生速度を2倍に変更
float speed = 2.0f;
currentPlayer.setPlaybackParams(new PlaybackParams().setSpeed(speed));
nextPlayer.setPlaybackParams(new PlaybackParams().setSpeed(speed));
```

これは、再生速度の変更でcurrentPlayerとnextPlayerの速度を一緒にに変更することで、次の曲でも同じ再生速度を維持したまま再生させることを意図したものです。ところが、nextPlayer.setPlaybackParams(new PlaybackParams().setSpeed(speed));の行に来た段階で、端末によってはnextPlayerの再生が勝手に開始されたり（currentPlayerと2重に再生される）、Exceptionが発生したりします。

これは、MediaPlayerが再生停止中（prepareが実行された状態やpauseで停止した状態）に再生速度の変更をすると、件の事象が起こります。多くの端末では発生しないので、機種固有の問題かもしれません。

なので、これを解消するために、次のような感じのコードになりました。

```java:次の曲を準備してプレイヤーを再生開始

nextPlayer = new MediaPlayer();
nextPlayer.setDataSource(context, nextMediaUri); // nextMediaUri: 次のメディアのURI
nextPlayer.setPlaybackParams(params.setSpeed(1.0001f)); // Xperia対策
nextPlayer.prepare();

curerntPlayer = new MediaPlayer();
curerntPlayer.setDataSource(context, currentMediaUri); // currentMediaUri: メディアデータのURI
curerntPlayer.setPlaybackParams(params.setSpeed(1.0001f)); // XPeria対策
curerntPlayer.prepare();
curerntPlayer.setNextMediaPlayer(nextPlayer);
currentPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
    @Override
    public void onCompletion(MediaPlayer mp) {
        if (nextPlayer.isPlaying()) {
            nextPlayer.setPlaybackParams(new PlaybackParams().setSpeed(speed)); // 再生速度の変更
        }
    }
});

curerntPlayer.start();
```

```java:再生速度を2倍に変更
float speed = 2.0f;
if (currentPlayer.isPlaying()) {
    currentPlayer.setPlaybackParams(new PlaybackParams().setSpeed(speed));
    // nextPlayerの速度変更は、OnCompletionListenerで行う
}
```

分かりにくくて申し訳ないですが、currentPlayerにMediaPlayer.OnCompletionListenerを設定します。これは、再生が完了して次の再生に移行する時に発生するイベントで、これが発生した時点で次のnextPlayerが再生開始状態になっています。ここでnextPlayerの再生速度を設定するようにします。

# まとめ

今回、AndroidのMediaPlayerで速度変更機能を実装する苦労から得られた知見は、

* 再生速度は必ず再生開始(start)後に設定する
* Xperiaで再生速度の変更ができない機種があるので、prepare前に一度速度変更を設定する
* Galaxy、Huawei、Xperia辺りの機種は動作を確認した方が良い

といった辺りです。

この再生速度の変更機能はAndroid6.0で標準機能として実装されているにも関わらず、わりと機種依存度が高いようです。今回確認できた事象についてはとりあえず対処しましたが、他にも問題が起きている端末が存在する可能性があります。

もし他に具体的な対処方法をご存知の場合、教えていただければ幸いです。