Synchronization between `sync` and `sleep`

Something I find myself always dealing with and feel like I shouldn’t have to is synchronizing between cues and sleeps. See the following basic example:

live_loop :tock do
  4.times do
    cue :tick  
    sleep 1
  end
end

live_loop :synths do
  sync :tock
  4.times do
    play :c4
    sleep 1
  end
end

I think in this case the :synths and :tock both take 4 beats but I believe the sync :tock in :synths will miss every other cue from :tock (probably because they are supposed to be simultaneous and we’ve decided cues don’t trigger in this case?) so the synths will play for 4 beats, wait for 4 beats and then play again.

I work around this by adjusting the sleeps somewhere - for example by making the :tick sleep a 1.0001 rather than 1, to make sure ticks and tocks always trigger after any sleeps have completed. But it gets more annoying when I want different live loops to synchronize with each other.

Is the current behavior intentional or would it be OK to try to “fix” this issue at language level? The synchronization code is a bit daunting but I guess it should be possible to try to order the cue/sleep triggers? I guess I’d like:

A sync at beat B in thread T will always receive a cue C sent at beat B, unless thread T already received C at beat B.

(not sure if the unless part is required, but I imagine someone might chain sync :foo; sync :foo to be able to wait for two iterations)

WDYT?

Hi @siimphh,

have a look at this thread, which will hopefully help to understand, how cue and sync are designed and why.

1 Like

That thread is great and explains how to deal with the current implementation. But how would you feel about chaning the implementation to work more intuitively?

Well, that’s a question for @samaaron. Meanwhile you might be interested in the reasons standing behind the current implementation.

But: I do know, what you mean. It seems to me that this is a case where the idea that a ten-year-old should be able to understand and use without difficulty is somehow in contracdiction to the necessity to avoid race conditions.

2 Likes

I’m still making my metronome thread sleep for 4.0001 beats :slight_smile:

here how I did it

live_loop :tock,delay: 0.0001 do
  4.times do
    cue :tick
    sleep 1
  end
end

live_loop :synths do
  sync :tock
  4.times do
    tick
    play :c4
    sleep 1 if look%4 !=3 #supresses every 4th sleep so depends on next sync
  end
end

live_loop :doodle do #checks the cue :tick working
  sync :tick
  sample :bd_haus
end

I’m pretty new here, but my observation has been to try to avoid using sleep in or at the end of loops that you are trying to sync. This is my solution to your specific example. I understand your problem is in how loops sync to each other but given the design constraints, this solves the problem and is easy (for my dad brain) to reason about.

live_loop :tock do
  4.times do
    cue :tick
    sleep 1
  end
end

live_loop :synths do
  4.times do
    sync :tick
    play :c4
  end
end

I was thinking more about this and concluded that @robin.newman’s answer is more correct in terms of keeping the original intent of the user. My opinion is that solutions requiring fiddling with or balancing out sleep terms across loops isn’t a good user experience and that led me dig a little more.

Looking at the example by @siimphh it seems that conceptually the user is trying to express this:

  • A timer loop that runs 4 beats / 1 meaure
  • A loop that performs a melody for 4 bars / 1 measure; this loop should sync to the timer loop

That seems reasonable on its own and the example code seems like it should work that way at a glance. On one hand the spec for scheduling live loops is clear and reasonable for reducing implementation complexity. On the other hand, beginners and even moderately experienced users and programmers are going to stumble on this. Are there opportunities to evolve live_loop so it could support the semantics of “always sync”? For example either via a new option or perhaps the presence of sync :some_live_loop within a loop would trigger the behavior that the loop is always terminated and restarted on the cue.

I’m totally open to new suggestions. My only constraint is that any approach is deterministic :slight_smile:

1 Like

I will sacrifice some heads of :leafy_green: and attempt to summon the Oracle of Concurrency as so many others have done before me :genie:

In more seriousness, I’ll throw out a strawman to open discussion.

What’s the Problem?

A user trying to setup one or more loops synchronized to another ‘master’ loop may have difficulty achieving the desired results, particularly if the loops all have a similar duration. As the example here (and other anecdotal user testing) illustrates, the scheduling algorithm can lead to threads missing cues that they would have been ready to handle, depending on where the thread ends up in the scheduling roster.

In my opinion, users should be able to specify a loop to sync to, with a guarantee that the loop will start at each master loop’s cue.

What Does It Look Like?

One option could be adding a method option to live_loop called (for example) master_loop:

live_loop :bars do
  4.times do
    sleep 1
  end
end

live_loop :four_on_floor, master_loop: :bars do
  4.times do
    sample: drum_hard_bass
    sleep 1
  end
end

The concept should be simple enough, I hope - loops tied to the “master” loop will be scheduled to start at the same time, with each new run dependent on the “master” loop. This would be analogous to hardware multi-track loopers where one track is recorded and the other tracks are automatically quantized and synchronized. There might be some other considerations for doing this in Sonic Pi. For example, the hardware loopers in sync mode will limit the length of all tracks to the length of the first loop. I guess in Sonic Pi the loop length could be more ambiguous like if a loop kicked off a long-running pad then you could have overlapping loops if the audio is still going from the previous run when the next run kicks off.

Finally, I understand that “master/slave” terminology isn’t always the best choice for various reasons, so perhaps another convention like “group” or “parent” or “timer_loop” would be more suitable in place.

I’ve been thinking about this, and I wonder if there isn’t a simpler solution.

It appears that the order of cues and syncs that happen during the same tick is important.

Could this be changed, so that a cue that happens during the same time as a sync unblocks the sync’ing thread, no matter which order they arrive in.

I think that would make Sonic Pi easier to use, and be more intuitive than what happens now. Maybe.

It may break some legacy code however, if people have been relying on the current behaviour.

1 Like

Many apologies, but I still don’t quite get the semantics you’re looking for. Could your expand your example a little - perhaps with a step by step analysis of the behaviour you’d like to see?

The behavior I’m after is that the example posted in the first message would output a note at each beat in continuum once started (which is what I believe the user’s intent was?)

Apologies again, but I must be missing something. It’s unfortunately not at all obvious (to me) what you specifically mean.

Ah ok, I think I’m getting it. One approach is to do this:



live_loop :tock do
  4.times do
    cue :tick
    sleep 1
  end
end

live_loop :synths, sync: :tock do
  4.times do
    play :c4
    sleep 1
  end
end
1 Like

So my understanding of the original example is that the user expects that the synth loop will trigger each time the tock loop cues, because the 2 loops should be identical in length. But in execution, the synth loop misses its cue every other time because of the way the loops are scheduled. This feels counter-intuitive to me (a Sonic Pi beginner, long-time programmer familiar with async paradigms).

My expectation - or what I’m asking to achieve - is a way that I can sync 2 loops together without being caught by the scheduling semantics.

Agreed that this is non-intuitive though. I’ll add it to my TODO list to explore in depth, but I’m unsure I’ll be able to look at it before the next release.

1 Like

Yes, this solution mostly solves the problem - I thought I had tried it this way with the sync option and it didn’t work, but it is now and may have had other variables at play. I think this works well if the loops are identical (or nearly) duration, but this is only syncing them up the first run right? So if the loops are different lengths they will drift apart each run (I confirmed this with a longer sleep and an extra cue in the synth loop).

I still think it’s worthwhile to explore a semantic for forcing the sync. That is, in the example, whenever tock cues, schedule a new synth run automatically, or something like that :slight_smile: . Thanks for all your feedback, I have a better understanding now.

Yes, using sync: as an opt only syncs the first time, but not again (unless the thread dies).

I’m not sure what you mean by forcing a sync. Would this result in more than one thread called :synths? This would be in direct violation of the named thread semantic which ensures that only one thread of a given name may be executed at once. Unless you intend to kill the existing thread and start a new one? In which case, how would you handle thread locals?

1 Like

Hi @perpetual_monday,

well, unless your system can not keep the timing there should be no drift. At least I have never seen synced live_loops drifting at all, I’d rather get Xruns or live_loops completely stop if the performance is to low.

You might want to have a look at some practical examples