Question await Task.Delay alternative for high precision?

SpyderTL

Member
Joined
Jul 27, 2022
Messages
7
Programming Experience
10+
I am starting a new C# project which is going to be similar to the music language Sonic Pi, and I thought that await Task.Delay would be a good way to programmatically start and stop playing notes with fairly high precision.

However, trying to keep multiple tracks in sync using this method does not appear to be accurate enough, and I’ve already found several explanations online about how the threadpool will cause multiple tasks to get out of sync, even if they are all awaiting the same Task.Delay value, which I suspected from the beginning was going to be a problem. I just haven’t been able to come up with a good way to keep them in sync.

I tried converting all of the Tasks to Threads with Task.Run, and using Thread.Sleep instead of await Task.Delay, but the end result was the same. The first two threads would be fairly stable, but spinning up a third and fourth thread would take a few hundred milliseconds of delay, causing the later threads to immediately become out of sync with the earlier ones.

My current solution is to create new threads with Task.Run, but to pass a synchronized “start time” value to each thread. This start time can be used by each thread independently to compare to the current Environment.TickCount to determine when to start playing notes. This solves the problem, but it requires me to write additional code to calculate the timing values for each note that is played, and add those values to the initial value of the thread, which isn’t pretty.

So, my question is… does anyone have any suggestions on how I can keep up to, say, 32 Tasks / Threads in sync to the point that they can play the exact same sequence of notes at the exact same time?

My goal is to be able to write a multi-track song completely in C#, putting each track in a separate function, and have all of the tracks start and end at the exact same time.

Thanks for your help.
 
First, if you need precise control over threads, don't use the system thread pool. Task.Run() uses the system thread pool. Use the Thread class instead.

Next, single threaded systems have long managed to do this. My C-64 could do it. It's a matter of using the right data structure. If the top of my head, a queue of notes with start times should be good.

Next, since notes are sequential time deltas are you really need, not exact system times.
 
If you are willing to use P/Invoke and InterOp, there are also the Win32 OS timers, one of which is the high resolution timer, as well as waitable timer objects.
 
If you are willing to use P/Invoke and InterOp, there are also the Win32 OS timers, one of which is the high resolution timer, as well as waitable timer objects.
I have actually written a midi sequencer for the C64. ;)

And I know how to use high resolution timers in Win32.

The part that I don’t know how to do is to either start multiple tasks or start multiple threads, and somehow have them start playing and stop playing notes at the exact same time, or at least close enough to where the delay is not noticeable, maybe 5-10 ms at the most.

As a simple example, I want to write one function that plays 4 notes, one per beat at 120 bpm. And I want to call that function 32 times, and have them run at the same time, so that all 128 notes are triggered, but they are in perfect sync, so that you really only hear 4 notes. (probably 4 very loud notes)
 
Last edited:
For example, this is what I want to write. I just need a way to be able to call this function multiple times and have those calls run in parallel, and in sync.

C#:
private async Task Play4Notes()
{
    for(int x = 0; x < 4; x++)
    {
        await Play(64, 100);
        await Task.Delay(100);
    }
}

await Task.WhenAll(Enumerable.Range(0, 32).Select(x => Play4Notes()));


How close to this can I get by writing only C# code? I know that the Sonic Pi guys have figured this out, as they have the ability to run "code" in multiple "threads", although I suspect there is a lot of actual code running in the background to make this work properly. I just need to be able to replicate that behavior somehow, without impacting the simplicity of the code listed above.

Note: The code above isn't actually multi-threaded, so, technically, there should be a way to resume these "tasks" with very high precision if the Synchronization Context was using a Thread.SpinWait() for example. Would writing my own Synchonization Context be an option, if it were single threaded, and never actually called Thread.Sleep?
 
Last edited:
Need to step back and recall how you did your sequencer on the C64. Need to the same approach.
 
Need to step back and recall how you did your sequencer on the C64. Need to the same approach.
Again, I’m not playing midi files. I already know how to write a program that reads note event data, and executes those events at the correct time.

What I am trying to do is write C# functions that play notes in a sequence. I want the code to 100% control what notes are played and when. For example, if I want random notes picked from a chord and played for a random duration, separated by a random delay, I want to write that function once, and call it multiple times, so that at any given time, up to 32 random notes could be playing. I don’t want to pre-generate the note events and store them in memory. I want the C# code in the function to contain all of the timing and note information.

For an example of what I am trying to replicate, check out Sonic Pi.
 
I don’t want to pre-generate the note events and store them in memory. I want the C# code in the function to contain all of the timing and note information.
But code is stored in memory.
 
Okay. I see you want to do a procedural way of producing the sound. In general, just using threads and fixed delays is not going to be accurate long term because threads scheduling can be delayed by the OS. Each thread will need to be polling the the high resolution timer(s) to see when it needs to transition to the next note or pause. I would imagine some kind of finite state machine.

Your idea of using your own context to be effectively single threaded maybe another option, but that will mean that you need to have the awaita at strategically placed parts of your code to ensure that you yield your "task thread" to another "task thread" at the appropriate times. This seems to go against your goal of trying to write each "task" so that they know nothing about the other "task"s.
 
Okay. I see you want to do a procedural way of producing the sound. In general, just using threads and fixed delays is not going to be accurate long term because threads scheduling can be delayed by the OS. Each thread will need to be polling the the high resolution timer(s) to see when it needs to transition to the next note or pause. I would imagine some kind of finite state machine.

Your idea of using your own context to be effectively single threaded maybe another option, but that will mean that you need to have the awaita at strategically placed parts of your code to ensure that you yield your "task thread" to another "task thread" at the appropriate times. This seems to go against your goal of trying to write each "task" so that they know nothing about the other "task"s.
The "await" statements will essentially be on every line of code. I'm either awaiting a note to stop playing, or awaiting a timer to play the next note.

The await statement hands the thread back to the thread pool, or in my case, hopefully, my custom synchronization context, which will check for the next scheduled Task, and spin wait until that Task is ready to run again.

I really don't know how Synchronization Context really works, so I may be missing something, but this is the idea that I've currently got in my head. It'll probably be a day or two before I can get some time to work on this again, but I'll let you know if I make any progress.

Just let me know if anyone has any suggestions or if you've tried to do something like this in the past.

I found a post talking about a custom single threaded synchronization context, and it was part of the project AsyncEx, which is on GitHub and NuGet. Maybe it already has what I need, and I won't have to write anything at all.
 
One of the Steven's who actively blogs about context, the scheduler, and async/await had a post with code for a single threaded context, but I can't recall right now which Steven it was. What I vaguely recall was the code was partly to demonstrate the difference between how the context worked in a console app vs. a WPF or Windows GUI app.
 
One of the Steven's who actively blogs about context, the scheduler, and async/await had a post with code for a single threaded context, but I can't recall right now which Steven it was. What I vaguely recall was the code was partly to demonstrate the difference between how the context worked in a console app vs. a WPF or Windows GUI app.
Yep.


I tested this on a quick proof of concept, and it looks like it is going to be accurate enough to use as-is. I'll let you know when I get a chance to implement it in my project.

Thanks for the help.
 
Unfortunately, the SingleThreadedSyncronizationContext didn't seem to make much difference. Tracks would still start to randomly drift apart, and it was very noticeable that something wasn't quite right. I tried to find a way to customize the custom SingleThreadedSyncronizationContext so that I could detect when a Task.Delay was in the queue, but all of the timing logic is actually on the Task.Delay side, not in the SyncronizationContext, so there wasn't much I could do without completely rewriting the internals of the Task.Delay code.

Once I realized that Task.Delay wasn't going to give me the accuracy I needed, I started looking into alternatives, and ultimately decided just to write my own function that simply stored the current tick count plus the wait time, and called Task.Delay(1) until the calculated time is reached.

It's not perfect, and it wastes both threads and CPU time, but it's close enough for me to move on to my next phase, which is writing some code to generate some music. So far I'm pretty happy with how quickly I can throw together a few functions in a loop, and mix in some random values, and end up with something very reminiscent of a classic RPG soundtrack.

Good stuff. I may post some video at some point if anyone is interested.

Thanks again for all of the help.
 
This is why in post #9 I specifically said:
that will mean that you need to have the awaita at strategically placed parts of your code to ensure that you yield your "task thread" to another "task thread" at the appropriate times.
Essentially, your awaits will have to fall on every beat. If you are 60 beats per second, you'll need to write the code so that whatever you are awaiting never takes more than 100 milliseconds.

Anyway, it looks like you took the busy waiting approach.
 
For Sonic Pi, I think that they have their own interpreter and virtual machine for their domain specific language. Since they control the virtual machine that is executing the language, they are in a position to break and yield to the other routines.
 
Back
Top Bottom