[Android] wrong playback state when playing MIDI as Ringtone

The Problem

  1. play a MIDI file as Ringtone.
  2. check whether .isPlaying() after .play()
1
2
3
4
      Uri midiUri = Uri.parse( "/sdcard/some/path/to/example.midi");
      Ringtone ringtone = RingtoneManager.getRingtone(mContext, midiUri);
      ringtone.play();
      assertTrue("Couldn't play ringtone " + midiUri, ringtone.isPlaying());

The problems is that the last line always fails.

How Ringtone is played?

Playing Ringtone in Android is nothing special than play a ".mp3" files.

when invoking RingtoneManager.getRingtone() it actually create a Ringtone() instance, and call .setUri()

frameworks/base/media/java/android/media/RingtoneManager.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    public static Ringtone getRingtone(final Context context, Uri ringtoneUri) {
        // Don't set the stream type
        return getRingtone(context, ringtoneUri, -1);
    }

    private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType) {
...
            final Ringtone r = new Ringtone(context, true);
            if (streamType >= 0) {
                r.setStreamType(streamType);
            }
            r.setUri(ringtoneUri);
            return r;
    }

Ringtone class actually use MediaPlayer to play ringtone.

frameworks/base/media/java/android/media/Ringtone.java

1
2
3
4
5
6
7
8
9
    public void setUri(Uri uri) {
    ...
        mLocalPlayer = new MediaPlayer();
        ...
            mLocalPlayer.setDataSource(mContext, mUri);
            mLocalPlayer.setAudioAttributes(mAudioAttributes);
            mLocalPlayer.prepare();
        ...
    }

How MediaPlayer handles MIDI file?

The underlying Java, Android will allocate different player for different media types.

frameworks/av/media/libmediaplayerservice/MediaPlayerFactory.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SonivoxPlayerFactory : public MediaPlayerFactory::IFactory {
  public:
    virtual float scoreFactory(const sp<IMediaPlayer>& /*client*/, ...) {
        static const char* const FILE_EXTS[] = { ".mid",
                                                 ".midi",
                                                 ".smf",
                                                 ".xmf",
                                                 ".mxmf",
                                                 ".imy",
                                                 ".rtttl",
                                                 ".rtx",
                                                 ".ota" };
    ....
    virtual sp<MediaPlayerBase> createPlayer() {
        ALOGV(" create MidiFile");
        return new MidiFile();
    }
}

You can see that if file extension is "midi", it will use MidiFile() as the real MediaPlayer.

What is MidiFile?

Actually the cpp class MidiFile is just a wrapper for libsonivox, making it compatiable for MediaPlayerBase

frameworks/av/media/libmediaplayerservice/MifiFile.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  ▼ MidiFile* : class
      [functions]
      MidiFile()
      setDataSource( const sp<IMediaHTTPService> & , const char* path, const KeyedVector<String8, String8> *)
      setDataSource(int fd, int64_t offset, int64_t length)
      prepare()
      prepareAsync()
      start()
      stop()
      seekTo(int position)
      pause()
      isPlaying()
      getCurrentPosition(int* position)
      getDuration(int* duration)
      release()
      reset()

Note that it also implement the interface isPlaying().

How it check isPlaying()?

It use a state called EAS_STATE_PLAY which defined in libsonvix.

1
2
3
4
5
6
bool MidiFile::isPlaying()
{
    ALOGV("MidiFile::isPlaying, mState=%d", int(mState));
    if (!mEasHandle || mPaused) return false;
    return (mState == EAS_STATE_PLAY);
}

Let's see the log

1
2
3
4
5
6
7
01-01 20:05:16.335 V/MidiFile( 2832): MidiFile::setLooping
01-01 20:05:16.335 V/MidiFile( 2832): MidiFile::start
01-01 20:05:16.335 V/MidiFile( 2832):   wakeup render thread
01-01 20:05:16.335 V/MidiFile( 2832): MidiFile::render - signal rx'd
01-01 20:05:16.335 V/MidiFile( 2832): MidiFile::isPlaying, mState=0
* 01-01 20:05:16.336 E/MediaPlayer( 5515): internal/external state mismatch corrected
01-01 20:05:16.337 V/MidiFile( 2832): MidiFile::render - create output track

line 5, it print that the mState=0, which means EAS_STATE_READY.

external/sonivox/arm-fm-22k/host_src/eas_types.h

1
2
3
4
5
6
7
8
9
/* EAS_STATE return codes */
typedef enum
{
    EAS_STATE_READY = 0,
    EAS_STATE_PLAY,
    EAS_STATE_STOPPING,
    EAS_STATE_PAUSING,
    ...
} E_EAS_STATE;

That is to say, MediaPlayer still stay in "Ready" state after .start(). It's a state mismatch, line 6 also print this error.

1
01-01 20:05:16.336 E/MediaPlayer( 5515): internal/external state mismatch corrected

frameworks/av/media/libmedia/mediaplayer.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool MediaPlayer::isPlaying()
{
    ...
    if (mPlayer != 0) {
        bool temp = false;
        mPlayer->isPlaying(&temp);
        if ((mCurrentState & MEDIA_PLAYER_STARTED) && ! temp) {
*            ALOGE("internal/external state mismatch corrected");
            mCurrentState = MEDIA_PLAYER_PAUSED;
        }
        return temp;
    }
...
}

How the state mismatch happens?

In MidiFile::start(), it actually set mState here, instead, it try to wakeup a thread call render. The real stuff is handled there.

frameworks/av/media/libmediaplayerservice/MifiFile.cpp

1
2
3
4
5
6
7
8
9
status_t MidiFile::start()
{
    ALOGV("MidiFile::start");
    ...
    // wake up render thread
    ALOGV("  wakeup render thread");
    mCondition.signal();
    return NO_ERROR;
}

Let's have a look at what happens in render().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int MidiFile::render() {
    ...
    while(1) {
    ...
        while (!mRender && !mExit)
        {
            ALOGV("MidiFile::render - signal wait");
*            mCondition.wait(mMutex);  // wait request here.
            ALOGV("MidiFile::render - signal rx'd");
        }
        ...
*        EAS_State(mEasData, mEasHandle, &mState);  // state changed here
        mMutex.unlock();

        // create audio output track if necessary
        if (!mAudioSink->ready()) {
*            ALOGV("MidiFile::render - create output track");
        ...
}

Recheck ths log, we can see thread already been wakeup, at the same time, isPlaying() request came.

It seems that state changed calling is after isPlaying().

There is a concurrency problem here, isPlaying() calling might get wrong state, before it changes to right value.

1
2
3
01-01 20:05:16.335 V/MidiFile( 2832): MidiFile::render - signal rx'd
01-01 20:05:16.335 V/MidiFile( 2832): MidiFile::isPlaying, mState=0
01-01 20:05:16.337 V/MidiFile( 2832): MidiFile::render - create output track

How to fix it?

This bug seem deep rooted in binary library, you can't fix this in all Android devices. However, a small delay can be used as a workaround to avoid this concurrency problem.

1
2
3
      Ringtone ringtone = RingtoneManager.getRingtone(mContext, midiUri);
      Thread.sleep(100); // add some delay to workaround this problem.
      ringtone.play();

留言