Using :sync with live_loops

Hi @mrbombmusic, excellent question!

Sonic Pi is a multi-threaded system, just like a band. What does this mean? Well, just like a band has multiple members that are capable of doing things at the same time (one band member plays the guitar, another the drums, etc) threads are also capable of running at the same time. In Sonic Pi you can create threads in a variety of ways - in_thread being the basic way, at for scheduling a thread for the future and live_loop for creating a looping thread that you can live code.

In a real band, you often want to ensure each band member is playing to the same rhythm. This means that they’re all playing to the same tempo (at the same speed). However, this isn’t quite enough - you can totally imagine a band where the drummer is just slightly ahead of the guitarist - they’re both playing at the same speed, but the drums hit a bit too early every time (we say that they drums are out of phase) . This can be fixed by ensuring that the drummer and the guitarist start at the same time. The band members need to have the same tempo and phase.

Getting multiple threads playing to the same rhythm is essentially the same problem. Firstly you have to make sure they’re playing to the same tempo - this is actually the default in Sonic Pi - the BPM is set to 60 unless you change it (which incidentally you can do on a per-thread basis for some really interesting a-rhythmic music). You also need to make sure that the sleep times are all similar (i.e. stick to whole numbers). However, getting them in phase is slightly more tricky. This isn’t a typical requirement of programming languages, so Sonic Pi has invented a few concepts to help out: cue and sync.

One way to imagine these two concepts is to think of a conductor and an orchestra. The violins may be waiting for their cue to start playing, and only when the conductor indicates to them do they start. In other words, the violins were syncing on the conductor’s cue:

in_thread, name: :violins do
  sync :start_violins
  # play violins
end

in_thread, name: :conductor do
   sleep 1
   cue :start_violins
   # continue conducting
end

In the example above we have two named threads: :violins and :conductor. The :violins thread immediately calls sync which means it blocks, waiting for a :start_violins cue message from another thread. If one doesn’t arrive, then the :violins thread blocks forever (until you hit the big app Stop button which kills all threads and resets the system). Luckily we do have another thread which is about to call cue with the right message (:start_violins) which does so after a second of waiting. After this second, the cue message is sent, the :violins thread is woken up, inherits the logical time of the cue thread (:conductor) and then continues. The two threads are now in sync and phase.

There are a few ways of working with sync. Firstly is to call it manually as above. If the sync is in the outer context (i.e. not within a do/end block) then it will block the main thread (each time you hit the Run button a main thread is created to run the code in the buffer). If the sync is within a thread then it will only block that thread. The third way is to use live_loop's sync: opt. Let’s quickly explore when you’d use each one.

Outer Context

sync :foo
live_loop :bar do
  # do stuff
end 

live_loop :quux do
  # do other stuff
end

In this case, we block the main thread waiting for a :foo cue message. Once received we then redefine both :bar and :quux live loops (or start them if they’re not already running). This has the advantage of ensuring that both :bar and :quux are in phase with another thread. However, it has the disadvantage of slowing down when you can re-define the live loops. Essentially, you can only re-define the live loops as often as there are :foo cues. One solution to this is to delete the sync line after the live loops have started, but it’s often easy to forget to do this, and you can be left wondering why your live loops aren’t updating (which has happened to me many times)

Inner Context

If you wish to have different live loops synced with different threads, and also for the requirement to sync one thread not affect the update rate of others (as happened above) then the solution is to place the sync within the live loops:

live_loop :bar do
  sync :foo
  sample :bd_haus
  sleep 0.5
end 

live_loop :quux do
  sync :foo2
  play :e1
  sleep 1
end

Now both live loops get updated immediately on run. However, their rates are now limited to the rate of the incoming :cue messages. If the :foo cue only comes in every 4 seconds, then the :bar live loops won’t loop around every 0.5s, instead it will be every 4s as it has to wait for incoming cue messages which are slowing it down.

So, how do we allow live loops to be instantly updated (i.e. not block the main thread), loop at the correct rate (i.e. not have to wait for external syncs) yet also start in phase? For this we need the sync: opt.

Live Loop :sync

To solve this problem I created the :sync opt for live loop. Let’s look at it in action:

live_loop :bar, sync: :foo do
  sample :bd_haus
  sleep 0.5
end 

live_loop :quux, sync: :foo2 do
  play :e1
  sleep 1
end

Now, there’s no sync in the main thread blocking things, there’s no sync inside the live loops slowing them down. Instead it’s sneakily placed itself inside the opts of live_loop. In this position, the sync will only block the live loop from starting. If the live loop isn’t already running, then the first thing it will do is wait for a corresponding cue message. Once one has arrived, the live_loop starts running and never again waits for another cue message. This allows you to sync up the thread on start, but not have to worry about things blocking or slowing down again.

Hope that this helps :slight_smile:

25 Likes