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 sync
ing 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