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:
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
in_thread, name: :violins do
# play violins
in_thread, name: :conductor do
# continue conducting
In the example above we have two named threads:
: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
sync: opt. Let’s quickly explore when you’d use each one.
live_loop :bar do
# do stuff
live_loop :quux do
# do other stuff
In this case, we block the main thread waiting for a
:foo cue message. Once received we then redefine both
:quux live loops (or start them if they’re not already running). This has the advantage of ensuring that both
: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)
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
live_loop :quux do
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
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
live_loop :quux, sync: :foo2 do
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