3

I'm developing a sort of music production app, which includes audio playback. I'm currently running into a problem where the playback is not consistent. The timer I use to progress through beats seems to skip or lag on some tick events. You can hear the pauses in the sample video: Video

I implement the timer and its event like so

private void Method()
{
    ///some other code here
    
    //the math for the timer period is to figure out how many times the timer should tick in 1 minute. 
    int _period = (int)Math.Round(60000f / (float)NUD_ConfigBPM.Value, MidpointRounding.AwayFromZero)
    _threadtimer = new System.Threading.Timer(_ => _threadtimer_Tick(), null, 0, _period);
}


private void _threadtimer_Tick()
{
    //vorbis is a List<List<CachedSound>>
    //each beat can have several sounds to play at once 
    foreach (var _sample in vorbis[_playbackbeat]) {
        AudioPlaybackEngine.Instance.PlaySound(_sample);
    }
    //simulating a playhead by highlighting cells of the DataGridView
    try {
        trackEditor.ClearSelection();
        trackEditor.Columns[_playbackbeat - 8].Selected = true;
    } catch { }

    _playbackbeat++;

    //once playback has reached the end of the file, stop it.
    if (_playbackbeat >= vorbis.Count) {
        _threadtimer.Dispose();
        _playing = false;
        btnTrackPlayback.ForeColor = Color.Green;
        btnTrackPlayback.Image = Properties.Resources.icon_play;
        trackEditor.SelectionMode = DataGridViewSelectionMode.CellSelect;
    }
}

AudioPlaybackEngine.Instance.PlaySound() is implemented from this NAudio article "Fire and Forget"

The Underlying Question

Where are these pauses coming from, and are there better timers or methods I could be using to solve this? The entire issue goes away at slower BPM (slower timer tick period).
Instead of a timer problem, could it be an NAudio problem, and the AudioPlaybackEngine is creating the delays?

More Info

I timed the spacing between every tick at 360BPM (_period is 167). Every tick was consistent in spacing (give or take a couple ms). Even for the beats with large audio delays, the tick event was on time.

I have tried using Multimedia timer, and the various high resolution timers in this thread. AccurateTimer and PrecisionRepeatActionOnIntervalAsync and returned the same result.

I also changed _playbacktimer_Tick() to be async, and got the same result.

CocoaMix86
  • 69
  • 1
  • 9
  • I have tried removing the simulated playhead block. That did not help. – CocoaMix86 May 02 '23 at 22:39
  • That timer is not 100% accurate. There's a bunch of ideas here on how to get very high accuracy: https://stackoverflow.com/questions/9228313/most-accurate-timer-in-net – MikeH May 02 '23 at 23:27
  • I've tried the top posted answer `AccurateTimer` and run into the same problems with random delays happening. – CocoaMix86 May 03 '23 at 04:39
  • Windows does 64 interrupts per second for its timings. That's one interrupt every 15.625ms. While Windows may be able to measure things at a higher resolution, its timing for the execute of code is limited to that resolution. – Enigmativity May 05 '23 at 01:48
  • There's no "perfect" solution w/o hardware addons. This implementation (based on Windows multimedia timer too) is quite nice IMHO: https://stackoverflow.com/a/24843946/403671 . At least these MM timers are not drifting, they are just a few milliseconds before or after the "real" tick. – Simon Mourier May 06 '23 at 08:13
  • Random idea: start multiple timers at different initial start times within ms/ns. If tick is out of tolerance on a timer, ignore it. If in tolerance do the work. – Kit May 06 '23 at 18:14
  • try/catch alone is going to wack things out if a throw happens. Is this occurring? – Kit May 06 '23 at 18:28
  • @Kit How will the multiple timers work? – Enigmativity May 07 '23 at 05:50
  • Using multiple timers dependent on various factors such as the HAL (hardware abstraction layer for those not familiar) could begin to approximate real-time even though Windows isn’t. This could remove the human perceived delays by NOP-ing work out of tolerance. A real solution is a real-time OS of course. – Kit May 07 '23 at 15:11
  • Think of it this way: if “play beat” must occur after exactly 1 sec after last beat, and you have 4 timers firing after 998ms, 1001ms, 1003ms, and 1004ms and your tolerance is 1.1ms, then timer tick for 1001ms “wins” and the others are ignored. Might need some error correction though… – Kit May 07 '23 at 15:24
  • @Kit - It doesn't work like that. Windows is like a drummer hitting a beat every 16.25ms. At each beat Windows checks if any timers should have fired and executes them. In your example above there is a nearly two-thirds chance that all of the timers would fire at the same time. – Enigmativity May 07 '23 at 22:14
  • I kind of suspected that :( thanks for the info. – Kit May 08 '23 at 11:56

1 Answers1

5

Windows handles times internally with a 15.6ms resolution by default. So just about all methods at doing things in an interval will be limited by this resolution. In addition, Windows is not a real time OS, so it provides no real timing guarantees. Your thread could preempted at any time, or just not get scheduled.

If you just want to measure time the solution is simple, just use a stopwatch. This usually has a resolution of about 100ns.

To ensure that your thread is scheduled consistently, you might consider increasing the priority of the thread. But will probably not be able to do this with timers that raise events on the threadpool.

You can also increase the timer resolution in Windows, but this is a system wide change, and it does have effects on power consumption, so be sure to turn it down again when not needed. Or you could use the multi media timer. Unfortunately it does not have built-in managed wrapper. I'm not sure if the multimedia timer just increases the timer resolution, or does something else.

You can also use a spinwait and a stopwatch to do nothing between waits, but this will prevent the CPU core from doing anything else, so it's not an ideal solution.

The best solution is to use the audio playback system to avoid the need for high-resolution events, and delegate that to the library/OS/Hardware. But I'm not familiar with audio playback to provide any specific recommendations.

JonasH
  • 28,608
  • 2
  • 10
  • 23