Forum Xamarin.Forms

Metronome in xamarin forms: how to keep timer in sync

ZomilZomil Member

Hello,

I'm working with Xamarin Forms and trying to build a simple metronome (just play a "beep" sound every 500ms for example).
I've tried multiple things with timers but my metronome always end up being out of sync after a few beeps. I didn't expect that something that looks as simple as a metronome would give me a hard time like this..

I've found discussions about timer precision talking about Multimedia Timers for example (that I haven't tested) but none that seems to work cross platform with xamarin..

Below is an example of what I've tried (not including the full code but that should be enough to get the idea).

    public class Metronome2
    {
        private AudioPlayer _audioPlayer;
        private System.Threading.Timer _timer;

        public Metronome2(AudioPlayer audioPlayer)
        {
            _audioPlayer = audioPlayer;
            _timer = new System.Threading.Timer(new System.Threading.TimerCallback(this.TimerCallback));
        }

        public void Start()
        {
            var period = TimeSpan.FromMilliseconds(500);
            _timer.Change(TimeSpan.Zero, period);
        }

        public void Stop()
        {
            _timer.Change(-1, -1);
        }

        private void TimerCallback(object state)
        {
            _audioPlayer.PlaySound(SoundEffect.MetronomeClick);
        }
    }

I also tried this:

    public class Metronome
    {
        private AudioPlayer _audioPlayer;
        private HighPrecisionTimer _timer;

        public Metronome(AudioPlayer audioPlayer)
        {
            _audioPlayer = audioPlayer;
        }

        public void Start()
        {
            _timer = new HighPrecisionTimer(500);
            _timer.Tick += new EventHandler(TimerCallback);
        }

        public void Stop()
        {
            _timer.Dispose();
        }

        private void TimerCallback(object sender, EventArgs args)
        {
            Task.Run(() => _audioPlayer.PlaySound(SoundEffect.MetronomeClick));
        }
    }

    // Slightly adapted from gist.github.com/HakanL/4669495
    public class HighPrecisionTimer : IDisposable
    {
        public event EventHandler Tick;
        protected CancellationTokenSource cancelSource;

        public HighPrecisionTimer(int interval)
        {
            //Tick += new EventHandler(HandleSomething);

            if (interval < 1)
                throw new ArgumentOutOfRangeException();
            System.Diagnostics.Trace.Assert(interval >= 10, "Not reliable/tested, may use too much CPU");

            cancelSource = new CancellationTokenSource();

            var watch = System.Diagnostics.Stopwatch.StartNew();
            long durationMs = 0;
            long totalTicks = 0;
            long nextStop = interval;

            var task = new Task(() =>
            {
                while (!this.cancelSource.IsCancellationRequested)
                {
                    long msLeft = nextStop - watch.ElapsedMilliseconds;
                    if (msLeft <= 0)
                    {
                        watch = System.Diagnostics.Stopwatch.StartNew();
                        durationMs = watch.ElapsedMilliseconds;
                        totalTicks = durationMs / interval;

                        Tick?.Invoke(this, null);

                        // Calculate when the next stop is. If we're too slow on the trigger then we'll skip ticks
                        nextStop = interval * (watch.ElapsedMilliseconds / interval + 1);
                    }
                    else if (msLeft < 16)
                    {
                        System.Threading.SpinWait.SpinUntil(() => watch.ElapsedMilliseconds >= nextStop);
                        continue;
                    }

                    System.Threading.Thread.Sleep(1);
                }
            }, cancelSource.Token, TaskCreationOptions.LongRunning);

            task.Start();
        }

        public void Dispose()
        {
            this.cancelSource.Cancel();
        }
    }

Any idea of what I am missing? Or lib that I should investigate that would work for ios and android? (even if platform specific code is required)

Any help much appreciated :)

Answers

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭

    1 - I wouldn't use a timer - its not guaranteed to be that accurate. Task.Delay is my preferred mechanism.
    2 - After a fast read I suspect your code is all on the same thread... So I'd bet its going out of sync by the duration of the "click" sound its playing. You need to get this stuff happening asynchronously. HINT: await and async

Sign In or Register to comment.