Playing music with an intro and a loop in Android

I just added background music for Codemon. It consists on 2 tracks: On for the Codiseum and one for everything else. Each track consists of an intro and a loop.

The main track plays the intro only once and the one for the Codiseum restarts from the beginning each time you enter the battle arena.

It turns out, this is not as trivial as it should be, but I won’t spoil the fun.

Initial solution (almost good)

To play long music tracks on Android, we have the class MediaPlayer, and the easiest way to do an intro+loop is to use 2 MediaPlayers and play one after the other. We will create a utility class IntroAndLoopMusicPlayer to handle it for us.

Below is the code for creation, load and unload for the utility class, which has an intro player and a loop player as members. This class loads the music from the assets directory given the path of the 2 files.

public IntroAndLoopMusicPlayer(AssetManager assets,
   String introMusicPath, String loopMusicPath)
{
   mAssets = assets;
   mIntroMusicPath = introMusicPath;
   mLoopMusicPath = loopMusicPath;

   mIntroPlayer = new MediaPlayer();
   mLoopPlayer = new MediaPlayer();

   load();
}

private void load()
{
   mLoopHasStarted = false;
   AssetFileDescriptor afd;
   try {
      afd = mAssets.openFd(mIntroMusicPath);
      mIntroPlayer.setDataSource(afd.getFileDescriptor(),
         introfd.getStartOffset(),afd.getLength());
      mIntroPlayer.setLooping(false);
      mIntroPlayer.prepare();

      afd = mAssets.openFd(mLoopMusicPath);
      mLoopPlayer.setDataSource(afd.getFileDescriptor(),
         afd.getStartOffset(),afd.getLength());
      mLoopPlayer.setLooping(true);
      mLoopPlayer.prepare();

      mIntroPlayer.setOnCompletionListener(this);
   }
   catch (IOException e) {}
}

@Override
public void onCompletion(MediaPlayer mp) {
    mLoopPlayer.start();
    mLoopHasStarted = true;
}

public void unload() {
    mIntroPlayer.stop();
    mIntroPlayer.release();
    mLoopPlayer.stop();
    mLoopPlayer.release();
}

Simple enough, the intro player is not looped, and when it completes, the loop player starts, which is looped. Only missing part is to just call start on the intro player to get the music started.

Next step: Since we want 2 music tracks, we need to be able to pause and resume each one of them, so let’s add some code to handle this

public void pause() {
    if (mIntroPlayer.isPlaying()) {
        mIntroPlayer.pause();
    }
    if (mLoopPlayer.isPlaying()) {
        mLoopPlayer.pause();
    }
}

public void start() {
    if (!mLoopHasStarted) {
        mIntroPlayer.start();
    }
    else {
        mLoopPlayer.start();
    }
}

Note: It is important to check if the player is playing before calling pause. In case you try to pause the intro after it has finished, or the loop before it has started the MediaPlayer will yield an error and the music will stop. Yes, seriously.

As mentioned, we want to restore the tracks either to the beginning of the loop or to the beginning of the track, so we need 2 more methods

public void restoreLoop() {
    mLoopPlayer.seekTo(0);
}

public void restoreIntroAndLoop() {
    if (mLoopHasStarted) {
        mLoopPlayer.seekTo(0);
    }
    else {
        mIntroPlayer.seekTo(0);
    }
    mLoopHasStarted= false;
}

And finally, from the sound manager, we can pause or start the music. Whenever we start/resume one track we will restore the other its the initial point.

public void pauseBgMusic() {
    mMainPlayer.pause();
    mCodiseumPlayer.pause();
}

public void resumeBgMusic(BGMusic bgMusic) {
    if (bgMusic == BGMusic.Main) {
        mMainPlayer.start();
        mCodiseumPlayer.restoreIntroAndLoop();
    }
    else {
        mCodiseumPlayer.start();
        mMainPlayer.restoreLoop();
    }
}

Note: I call pauseBgMusic inside onPause of all activities and call resumeBgMusic on both onCreate and onResume. This is the seamless way of playing music among several activities I have found so far, including pausing the music when the game goes to the background.

So this all looks fine. What’s the problem then?

The problem: Forward compatibility

This solution works… until you try it on a phone with Jelly Bean on it. Then you will notice that the music stops for almost a second after the intro finishes and before the loop starts. Note that this does not happen on older versions such as Gingerbread. Yes, I was quite puzzled.

It turns out that JellyBean has introduced a new method to have a seamless continuation of playing: setNextMediaPlayer. Which, as a side effect, makes the previous method useless.

So, forward compatibility, here we go. We have to replace this:

      mIntroPlayer.setOnCompletionListener(this);

With this:

      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
         mIntroPlayer.setNextMediaPlayer(mLoopPlayer);
      }
      mIntroPlayer.setOnCompletionListener(this);

And then, onCompletion has to be updated to look like this:

@Override
public void onCompletion(MediaPlayer mp) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
    {
        mLoopPlayer.start();
    }
    mLoopHasStarted = true;
}

We have yet another problem: When the loop player starts via setNextMediaPlayer, the intro player gets released automatically.

The solution is to reset and reload the intro player, but then another weird side effect happens: When you reset the intro player, the next media player gets reset as well.

The problem is that MediaPlayer is very hard to debug:

  • Documentation is extensive, but incomplete.
  • It just yields cryptic errors when you call an invalid method for the current state.
  • There is no way to get the current state.
  • Whenever there is an error, all music stops.

It took me a lot of trial an error to discover in which state the players were after each call. Once you know that, the fix is fairly straight forward: Reset and reload both of them.

public void restoreIntroAndLoop()  {
    if (mLoopHasStarted) {
        // For JB this also resets the next player...
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
        {
            mIntroPlayer.reset();
            mLoopPlayer.reset();
            load();
        }
        else {
            mLoopPlayer.seekTo(0);
        }
    }
    else {
        mIntroPlayer.seekTo(0);
    }
    mLoopHasStarted= false;
}

I also tried using a single MediaPlayer that seeks to the beginning of the loop whenever it completes, but has the same ‘glitching’ problem as the original solution.

I hope you find this useful and that it saves you the headaches I had to go through.