Not able to achieve Gapless audio looping so far o

2019-01-07 08:37发布

问题:

I have tried almost every method but I've failed to achieve gapless audio playback between looping a single track with a duration of 10-15 seconds.

Steps I've tried and failed :

  1. Different audio file formats .mp3 .wav .ogg using setLooping(true):

    MediaPlayer mp1 = MediaPlayer.create(MainActivity.this, R.raw.track1);
    mp1.setLooping(true);
    mp1.start();
    
  2. Creating two mediaplayers and looping one after another using setOnCompletionListenersame failed to loop without gaps.

  3. Using setNextMediaPlayer(nextmp) some how it works but only two loops is possible. We have to prepare and start again after the completion of previous two loops.

    mp1.start();
    mp1.setNextMediaPlayer(mp2);
    
  4. Update: Result of @Jeff Mixon answer: Mediaplayer looping stops with an error Android. Jeff Mixon works fine but only for 10 or 20 loops after that, due to some garbage collection issue the Mediaplayers stops immediately leaving the logs as posted below. I'm really kind of stuck here for 2 years. Thanks in advance.

    E/MediaPlayer(24311): error (1, -38)
    E/MediaPlayer(23256): Error(1,-1007)
    E/MediaPlayer(23546): Error (1,-2147483648)
    

回答1:

From the test that I have done, this solution works fine, over 150 loops with a 13 seconds 160 kbps MP3 without any problem:

public class LoopMediaPlayer {

    public static final String TAG = LoopMediaPlayer.class.getSimpleName();

    private Context mContext = null;
    private int mResId = 0;
    private int mCounter = 1;

    private MediaPlayer mCurrentPlayer = null;
    private MediaPlayer mNextPlayer = null;

    public static LoopMediaPlayer create(Context context, int resId) {
        return new LoopMediaPlayer(context, resId);
    }

    private LoopMediaPlayer(Context context, int resId) {
        mContext = context;
        mResId = resId;

        mCurrentPlayer = MediaPlayer.create(mContext, mResId);
        mCurrentPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mediaPlayer) {
                mCurrentPlayer.start();
            }
        });

        createNextMediaPlayer();
    }

    private void createNextMediaPlayer() {
        mNextPlayer = MediaPlayer.create(mContext, mResId);
        mCurrentPlayer.setNextMediaPlayer(mNextPlayer);
        mCurrentPlayer.setOnCompletionListener(onCompletionListener);
    }

    private MediaPlayer.OnCompletionListener onCompletionListener = new MediaPlayer.OnCompletionListener() {
        @Override
        public void onCompletion(MediaPlayer mediaPlayer) {
            mediaPlayer.release();
            mCurrentPlayer = mNextPlayer;

            createNextMediaPlayer();

            Log.d(TAG, String.format("Loop #%d", ++mCounter));
        }
    };
}

To use LoopMediaPlayer you can just call:

LoopMediaPlayer.create(context, R.raw.sample);


回答2:

Ugly proof-of-concept code, but you'll get the idea:

// Will need this in the callbacks
final AssetFileDescriptor afd = getResources().openRawResourceFd(R.raw.sample);

// Build and start first player
final MediaPlayer player1 = MediaPlayer.create(this, R.raw.sample);
player1.start();

// Ready second player
final MediaPlayer player2 = MediaPlayer.create(this, R.raw.sample);
player1.setNextMediaPlayer(player2);

player1.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
    @Override
    public void onCompletion(MediaPlayer mediaPlayer) {

        // When player1 completes, we reset it, and set up player2 to go back to player1 when it's done
        mediaPlayer.reset();
        try {
            mediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
            mediaPlayer.prepare();
        } catch (Exception e) {
            e.printStackTrace();
        }

        player2.setNextMediaPlayer(player1);
    }
});
player2.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
    @Override
    public void onCompletion(MediaPlayer mediaPlayer) {
        // Likewise, when player2 completes, we reset it and tell it player1 to user player2 after it's finished again
        mediaPlayer.reset();
        try {
            mediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
            mediaPlayer.prepare();
        } catch (Exception e) {
            e.printStackTrace();
        }

        player1.setNextMediaPlayer(player2);
    }
});

// This loop repeats itself endlessly in this fashion without gaps

This worked for me on an API 19 device and a 5-second 128 kbps MP3. No gaps in the loop.



回答3:

At least as of KitKat, Mattia Maestrini's Answer (to this question) is the only solution I've found that allows gapless looping of a large (> 1Mb uncompressed) audio sample. I've tried:

  • .setLooping(true): gives interloop noise or pause even with perfectly trimmed .WAV sample (published bug in Android);
  • OGG format: frameless format, so better than MP3, but MediaPlayer still emits interloop artifacts; and
  • SoundPool: may work for small sound samples but large samples cause heap size overflow.

By simply including Maestrini's LoopMediaPlayer class in my project and then replacing my MediaPlayer.create() calls with LoopMediaPlayer.create() calls, I can ensure my .OGG sample is looped seamlessly. LoopMediaPlayer is therefore a commendably practical and transparent solution.

But this transparency begs the question: once I swap my MediaPlayer calls for LoopMediaPlayer calls, how does my instance call MediaPlayer methods such as .isPlaying, .pause or .setVolume? Below is my solution for this issue. Possibly it can be improved upon by someone more Java-savvy than myself (and I welcome their input), but so far I've found this a reliable solution.

The only changes I make to Maestrini's class (aside from some tweaks recommended by Lint) are as marked at the end of the code below; the rest I include for context. My addition is to implement several methods of MediaPlayer within LoopMediaPlayer by calling them on mCurrentPlayer.

Caveat: while I implement several useful methods of MediaPlayer below, I do not implement all of them. So if you expect for example to call .attachAuxEffect you will need to add this yourself as a method to LoopMediaPlayer along the lines of what I have added. Be sure to replicate the original interfaces of these methods (i.e., Parameters, Throws, and Returns):

public class LoopMediaPlayer {

    private static final String TAG = LoopMediaPlayer.class.getSimpleName();

    private Context mContext = null;
    private int mResId   = 0;
    private int mCounter = 1;

    private MediaPlayer mCurrentPlayer = null;
    private MediaPlayer mNextPlayer    = null;

    public static LoopMediaPlayer create(Context context, int resId) {
        return new LoopMediaPlayer(context, resId);
    }

    private LoopMediaPlayer(Context context, int resId) {
        mContext = context;
        mResId   = resId;

        mCurrentPlayer = MediaPlayer.create(mContext, mResId);
        mCurrentPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mediaPlayer) {
                mCurrentPlayer.start();
            }
        });
        createNextMediaPlayer();
    }

    private void createNextMediaPlayer() {
        mNextPlayer = MediaPlayer.create(mContext, mResId);
        mCurrentPlayer.setNextMediaPlayer(mNextPlayer);
        mCurrentPlayer.setOnCompletionListener(onCompletionListener);
    }

    private final MediaPlayer.OnCompletionListener onCompletionListener = new MediaPlayer.OnCompletionListener() {
        @Override
        public void onCompletion(MediaPlayer mediaPlayer) {
            mediaPlayer.release();
            mCurrentPlayer = mNextPlayer;
            createNextMediaPlayer();
            Log.d(TAG, String.format("Loop #%d", ++mCounter));
        }
    };
    // code-read additions:
    public boolean isPlaying() throws IllegalStateException {
        return mCurrentPlayer.isPlaying();
    }

    public void setVolume(float leftVolume, float rightVolume) {
        mCurrentPlayer.setVolume(leftVolume, rightVolume);
    }

    public void start() throws IllegalStateException {
        mCurrentPlayer.start();
    }

    public void stop() throws IllegalStateException {
        mCurrentPlayer.stop();
    }

    public void pause() throws IllegalStateException {
        mCurrentPlayer.pause();
    }

    public void release() {
        mCurrentPlayer.release();
        mNextPlayer.release();
    }

    public void reset() {
        mCurrentPlayer.reset();
    }
}


回答4:

Something like this should work. Keep two copies of the same file in the res.raw directory. Please note that this is just a POC and not an optimized code. I just tested this out and it is working as intended. Let me know what you think.

public class MainActivity extends Activity {
MediaPlayer mp1;
MediaPlayer mp2;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mp1 = MediaPlayer.create(MainActivity.this, R.raw.demo);
    mp2 = MediaPlayer.create(MainActivity.this, R.raw.demo2);

    mp1.start();

    Thread thread = new Thread(new Runnable() {

        @Override
        public void run() {
            int duration = mp1.getDuration();
            while (mp1.isPlaying() || mp2.isPlaying()) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                duration = duration - 100;
                if (duration < 1000) {
                    if (mp1.isPlaying()) {
                        mp2.start();
                        mp1.reset();
                        mp1 = MediaPlayer.create(MainActivity.this,
                                R.raw.demo);
                        duration = mp2.getDuration();

                    } else {
                        mp1.start();
                        mp2.reset();
                        mp2 = MediaPlayer.create(MainActivity.this,
                                R.raw.demo2);
                        duration = mp1.getDuration();
                    }
                }
            }
        }

    });

    thread.start();
}
}


回答5:

I suggest you to use SoundPool API instead of MediaPlayer.

From the official documentation:

The SoundPool class manages and plays audio resources for applications.

...

Sounds can be looped by setting a non-zero loop value. A value of -1 causes the sound to loop forever. In this case, the application must explicitly call the stop() function to stop the sound. Any other non-zero value will cause the sound to repeat the specified number of times, e.g. a value of 3 causes the sound to play a total of 4 times.

...

Take a look here for a practical example of how to use SoundPool.



回答6:

For some reason, I found that my "OnCompletion" Event was always firing a fraction of second late when attempting to loop an 8-second OGG file. For anyone experiencing this type of delay, try the following.

It is possible to forcibly queue a "nextMediaPlayer" as recommend in previous solutions, by simply posting a delayed Runnable to a Handler for your MediaPlayers and avoiding looping in onCompletion Event altogether.

This performs flawlessly for me with my 160kbps 8-second OGG, min API 16.

Somewhere in your Activity/Service, create a HandlerThread & Handler...

private HandlerThread SongLooperThread = new HandlerThread("SongLooperThread");
private Handler SongLooperHandler;

public void startSongLooperThread(){
    SongLooperThread.start();
    Looper looper = SongLooperThread.getLooper();
    SongLooperHandler = new Handler(looper){
        @Override
        public void handleMessage(Message msg){
            //do whatever...
        }
    }
}

public void stopSongLooperThread(){
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2){
        SongLooperThread.quit();
    } else {
        SongLooperThread.quitSafely();
    }
}`

...start the Thread, declare and set up your MediaPlayers...

@Override
public void onCreate() {
    // TODO Auto-generated method stub
    super.onCreate();

    startSongLooperThread();

    activeSongResID = R.raw.some_loop;
    activeMP = MediaPlayer.create(getApplicationContext(), activeSongResID);
    activeSongMilliseconds = activeMP.getDuration();

    queuedMP = MediaPlayer.create(getApplicationContext(),activeSongResID);
}

@Override
public void onDestroy() {
    // TODO Auto-generated method stub
    super.onDestroy();
    stopSongLooperThread();

    activeMP.release();
    queuedMP.release();
    activeMP = null;
    queuedMP = null;
}

...create a Method for swapping your MediaPlayers...

private void swapActivePlayers(){
    Log.v("SongLooperService","MediaPlayer swap started....");
    queuedMP.start();

    //Immediately get the Duration of the current track, then queue the next swap.
    activeSongMilliseconds = queuedMP.getDuration();
    SongLooperHandler.postDelayed(timedQueue,activeSongMilliseconds);
    Log.v("SongLooperService","Next call queued...");

    activeMP.release();

    //Swap your active and queued MPs...
    Log.v("SongLooperService","MediaPlayers swapping....");
    MediaPlayer temp = activeMP;
    activeMP = queuedMP;
    queuedMP = temp;

    //Prepare your now invalid queuedMP...
    queuedMP = MediaPlayer.create(getApplicationContext(),activeSongResID);
    Log.v("SongLooperService","MediaPlayer swapped.");
}

...create Runnables to post to your thread...

private Runnable startMP = new Runnable(){
    public void run(){
        activeMP.start();
        SongLooperHandler.postDelayed(timedQueue,activeSongMilliseconds);
    }
};

private Runnable timedQueue = new Runnable(){
    public void run(){
        swapActivePlayers();
    }
};

In your Service's onStartCommand() or somewhere in your Activity, start the MediaPlayer...

...
SongLooperHandler.post(startMP);
...