Android 5.0 LollipopからMediaPlayerで曲のループがおかしい

投稿日:2015年7月3日

前回お話したMediaPlayerの再生開始直後にノイズが入るお話の続きです。

実はAndroid 5.0 LollipopになってからのMediaPlayerの挙動にはまだ嫌な点があるのです。
それは、ループ再生時の遅延なんですよね。

今までは

//再生ファイルの設定
MediaPlayer mediaPlayer = MediaPlayer.create(this, R.raw.bgm);

//再生ボリュームの設定
mediaPlayer.setVolume(1f, 1f);

//ループ設定
mediaPlayer.setLooping(true);

//再生開始
mediaPlayer.start();

こんな感じで再生すれば問題なく曲がループしていました。
ですが、Android 5.0 Lollipopでこのまま曲を再生すると、ループする際に500msぐらい?隙間が開いてしまうのですよね。
そのせいで、ループする前提で曲を作っていても、ループ端が目立ってしまい作成者としては気分が悪い!
そこで、それをどうにかしたいというのが今回のお話になります。

結論から言うと、こんな感じのクラスを作ってみました。

public class RunnableSoundLoop implements Runnable {
  MediaPlayer[] arrayMediaPlayer;
  long preTime;
  Timer soundLoopTimer;
  int[] musicLength;
  int playTime, playNumber;

  static final long FPS = 40;
  static final long FRAME_TIME = 1000 / FPS;

  public RunnableSoundLoop(MediaPlayer[] arrayMediaPlayer, long preTime) {
    this.arrayMediaPlayer = arrayMediaPlayer;
    this.preTime = preTime;
    this.musicLength = new int[arrayMediaPlayer.length];
    for(int i = 0; i < arrayMediaPlayer.length; i++) {
      musicLength[i] = arrayMediaPlayer[i].getDuration();
    }
    this.playTime = 0;
    this.playNumber = 0;
  }

  public void run() {
    soundLoopTimer = new Timer(true);
    TimerTask timerTask = new TimerTask() {
      public void run() {
        if(arrayMediaPlayer[playNumber] != null) {
          //再生時間を加算
          playTime += System.currentTimeMillis() - preTime;
					
          //再生時間が曲の長さを超えた場合
          if(playTime >= musicLength[playNumber]) {
            //再生時間をリセット
            playTime = 0;

            //次のMediaPlayerを再生する
            playNumber +=1;
            if(playNumber >= arrayMediaPlayer.length) {
              playNumber = 0;
            }
            arrayMediaPlayer[playNumber].start();
          }

          //現在の時間をシステム時間で取得
          preTime = System.currentTimeMillis();
        }
        else {
          //MediaPlayerが解放された場合はTimerを止める
          soundLoopTimer.cancel();
          soundLoopTimer.purge();
          soundLoopTimer = null;
        }
      }
    };
    soundLoopTimer.schedule(timerTask, 0, FRAME_TIME);
  }
}

でもって、呼び出し元のActivityに以下の様に書けば動きます。
(rawフォルダにあるbgm.ogg(mp3)を再生する場合)

public class ActivityMain extends Activity {
  MediaPlayer[] arrayMediaPlayer;
  Thread threadSoundLoop;

  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }

  protected void onResume() {
    super.onResume();

    //MediaPlayerの実行
    playMediaPlayer();
  }

  protected void onPause() {
    super.onPause();

    //MediaPlayerの停止
    releaseMediaPlayer();
  }

  public void playMediaPlayer() {
    //MediaPlayerの設定
    arrayMediaPlayer = new MediaPlayer[] {MediaPlayer.create(this, R.raw.bgm),
      MediaPlayer.create(this, R.raw.bgm)};

    //初回再生時のノイズ除去処理
    for(int i = 0; i < arrayMediaPlayer.length; i++) {
      arrayMediaPlayer[i].setVolume(0f, 0f);
      arrayMediaPlayer[i].setLooping(false);
      arrayMediaPlayer[i].seekTo(arrayMediaPlayer[i].getDuration() - 200 * (i + 1));
      arrayMediaPlayer[i].setOnCompletionListener(new OnCompletionListener() {
        public void onCompletion(MediaPlayer mediaPlayer) {
          mediaPlayer.setVolume(1f, 1f);
          mediaPlayer.seekTo(0);
          mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
            public void onCompletion(MediaPlayer mediaPlayer) {
              mediaPlayer.seekTo(0);
            }
          });
          if(threadSoundLoop == null) {
            threadSoundLoop = new Thread(new RunnableSoundLoop(arrayMediaPlayer, System.currentTimeMillis()));
            threadSoundLoop.setDaemon(true);
            threadSoundLoop.start();
            mediaPlayer.start();
          }
        }
      });
      arrayMediaPlayer[i].start();
    }
  }

  public void releaseMediaPlayer() {
    for(int i = 0; i < arrayMediaPlayer.length; i++) {
      if(arrayMediaPlayer[i] != null) {
        arrayMediaPlayer[i].stop();
        arrayMediaPlayer[i].release();
        arrayMediaPlayer[i] = null;
      }
    }
    threadSoundLoop = null;
  }
}

要するに同じ曲のMediaPlayerを2つ作って交互に流す感じですね。

わざわざこんな面倒くさい事をしているのには理由があるのです。
実は、1個のMediaPlayerでこれを実現しようとすると、曲が終わった直後に

mediaPlayer.seekTo(0)
mediaPlayer.start();

と書きたいところなのですが、これだと上手く行きません。
曲が終わってMediaPlayerのisPlaying()がfalseを返すまでの間にseekTo()で再生位置をいじると、再生位置が飛びまくっているような不思議な音がし始めてしまうのですよね。
しかも、isPlaying()がfalseを返すまでの時間が結構長い・・・
よって仕方なく複製した2つ目のMediaPlayerを再生しているわけです。

それと、なんでgetCurrentPositionで曲の位置を見ずにわざわざシステム時間を自分でカウントしているのかですが、Android 5.0ではgetCurrentPositionの値がいまいち信用できないのです。
Android 5.0から曲終了後に再生位置をgetCurrentPosition()で取得すると値がおかしなこと(毎回違う)になっています。
(旧OSの場合はPosition = 曲の再生長になっています)
しかも、曲が終わった直後におかしな値を返すので、

if(getCurrentPosition >= mediaPlayer.getDuration()) {
  //ループ再生処理
}

なんて書いてしまおうものなら一生ループ再生処理にたどり着かなくなります。
(旧OSならこれでちゃんと動くのですけどね・・・)
もちろん、再生時間が80%を超えるまではgetCurrentPositionに頼って、そこからは自らカウントを取るなんていう合わせ技でも良かったのですが、そもそもgetCurrentPositionで得られる値ではループのタイミングが合わないのです!
・・・変ですよね、getCurrentPositionってミリ秒で得られるのに精度がないなんて。
まあそういうわけなので仕方なく自分でシステム時間をカウントして再生時間を見ているのです。

今のコードで、ループ時の繋がりがいまいちな人は適当にFPSの値を弄ると良いことがあるかもしれません。
(値を増やせば増やすほど精度は上がりますが、負荷がかかっちゃいます)
後、Timerで定期的に行っている処理の中にMediaPlayerに関する処理(isPlaying()とかgetCurrentPosition())など、処理時間が長い命令を同一スレッドで追加してしまうとplayTimeのカウント精度が下がってしまうので、可能な限り余計な処理は行わないほうが良いかもしれませんね。

しかしながら、本当は曲を途中停止出来るようにしたかったんですよ。
getCurrentPositionの値がもう少し頼りになれば曲停止時に

playTime = paramSound.mediaPlayer[paramSound.playNumber].getCurrentPosition();

なんていうのもありかな~って思ったんですけどね。悲しい!
さらに言えば、再開時にseekTo(playTime)で再生位置をカウントの方に合わせようとすると余計に酷くなっちゃいます。
もうどうしたら!!!
と言った感じで、曲の再開時は最初から流し直すことしかできませんでした。

まあ、というわけで華麗な対策というわけにはいきませんでしたが、参考になれば幸いです。

P.S.
今回ご紹介のものを応用すれば、nextMediaPlayerが使えないVersionでも、曲A→曲B(ループ)なんていうイントロ部分をもった曲なんかも再生できますよ~

ビジネス記事一覧へ

Studio POPPOをフォローしませんか?

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください