简体   繁体   中英

Accurate android Thread Handler postDelayed

I have been trying to write a metronome in Android however I am finding really hard to sync the beats accurately using the Handler postdelayed method. I manage to achieve an accurate timing using a ScheduledThreadPoolExecutor, but the issue is that with the ScheduledThreadPoolExecutor I can't control the timing from within the run method and therefore I am forced to stop and start the scheduled job, which is not ideal. Is there a way to make the Handler postdelayed more accurate? or a way to reschedule the ScheduledThreadPoolExecutor without having to stop and start the thread?

My current code is as below:

public class Metronome extends Service implements Runnable
{
    private Handler handler = new Handler();
    private SoundPool soundPool;
    private long interval;

    private void initSoundPool()
    {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
        {
            soundPool = new SoundPool.Builder()
                    .setMaxStreams(1)
                    .setAudioAttributes(new AudioAttributes.Builder()
                            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                            .build())
                    .build();
        } else
        {
            soundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0);
        }

        soundId = soundPool.load(context, R.raw.secondary_clave, 1);
    }

    @Override
    public void run()
    {
        handler.postDelayed(this, interval);
        soundPool.play(soundId, 1, 1, 0, 0, 1);
    }

    public void start()
    {
        handler.post(this);
    }

    @Override
    public IBinder onBind(Intent intent)
    {
        return null;
    }
}

With the ScheduledThreadPoolExecutor it's super accurate, however, I don't have control via the "interval" flag inside the run loop, so if I change the interval I have to terminate the executor and start a new one everytime I need to rechedule which is horrible.

public class Metronome extends Service implements Runnable
{        
    private SoundPool soundPool;
    private long interval;
    private ScheduledThreadPoolExecutor beatsPerBarExec;
    private ScheduledFuture<?> futureThread;

    private void initSoundPool()
    {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
        {
            soundPool = new SoundPool.Builder()
                    .setMaxStreams(1)
                    .setAudioAttributes(new AudioAttributes.Builder()
                            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                            .build())
                    .build();
        } else
        {
            soundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0);
        }

        soundId = soundPool.load(context, R.raw.secondary_clave, 1);
    }

    @Override
    public void run()
    {            
        soundPool.play(soundId, 1, 1, 0, 0, 1);
    }

    public void start()
    {
        beatsPerBarExec = new ScheduledThreadPoolExecutor(1);
        futureThread = beatsPerBarExec.scheduleAtFixedRate(this, 0, interval, TimeUnit.MILLISECONDS);
    }

    public void pause()
    {
        futureThread.cancel(false);
        beatsPerBarExec.purge();
        beatsPerBarExec = null;            
    }

    @Override
    public IBinder onBind(Intent intent)
    {
        return null;
    }
}

You may be seeing the effects of drift.

Example: you want your Runnable to run every 200msec. You reschedule your Runnable in the run() method using postDelayed() and pass it 200msec as the delay. When the run() method is called the next time, it may not be exactly 200msec since the previous time. Perhaps it is 210msec. Now you reschedule your Runnable to run in another 200msec. This time the run() method may be called again after 210 msec, which means your sound plays 420msec since the first one, etc.

To eliminate drift, you need to determine the exact clock time you want the Runnable to run at, subtract the current time and use that in the call to postDelayed() . This will take into account any potential variance in the thread timing.

Be aware that when you call postDelayed() you are posting a Runnable to run on the main (UI) thread. This is the thread that handles all the UI updates and when your Runnable is ready to run it will just be queued to the main (UI) thread handler, and may not run immediately.

You can mitigate this problem by scheduling your Runnable to run on a background thread instead of the main (UI) thread. However this means your Runnable will be called on the background thread and I don't know if your other code (that plays the sound) needs to run on the main (UI) thread or not.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM