Using :sync with live_loops

I’m curious to see what people’s thoughts are on the best way to use :sync with multiple threads. I have seen it used outside the block of code, following the name of a live_loop, inside the block of code as well as using cue :tick inside a block of code and then syncing with :tick.

The differences seem to be in when you start the program, certain threads have to run before others sync up with it and also the length of time it takes for a change to happen in a thread that has been synced with another.

Can anyone break down the different uses and benefits to each different way?

Thanks

2 Likes

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

This is very helpful. Thank you for such a detailed response!

1 Like

Hello,
Try to catch the way it works but i must miss something… Why this code produce a little delay before launching the live_loop first ?

use_bpm 120

# nombre de temps par mesure
beats_per_mesure = 4
################################
live_loop :metronome do
  #stop
  play :c8
  sleep 1
end

live_loop :_1_MESURE, sync: :metronome do
  sleep beats_per_mesure
end

live_loop :_4_MESURES, sync: :metronome do
  sleep 4 * beats_per_mesure
end

live_loop :_8_MESURES do
  sleep 8 * beats_per_mesure
end
###############################


live_loop :first do
  sync "/live_loop/_1_MESURE"
  play_pattern_timed [:c3, :e3, :g3],[0.5, 1, 0.5]
end

How can i get my code coloured ?
thanks !

Hi,

I’m trying the sync: :variable.
When I run my live_loops I have a delay of 1 or 2 bars.
Can’t figure out what is going on.
I could provide the code, but it looks like something trivial

In pseudo code

live_loop :chords do
stuff
end

live_loop :melodie, sync: :chords do
stuff
end
Thanks

Hi @Lecavalier,

Sam may have other comments also, but in addition to the posts here above in this thread, you may want to read Live loops Sync questions :-) if you haven’t already - there has been much discussion about this topic and the delay that you are noticing, particularly over in that thread. In short though: :melodie will only start when :chords repeats, not start when :chords starts.

3 Likes

yes it’s always weird to understand. This is a simple example to test.

live_loop :metro do
  use_synth :beep
  play :c5, amp:0.5 # as boring as a real metronome
  sleep 1
  
end

## 1 bar = 4 beats
live_loop :bar do
  sleep 4
end


live_loop :bdrum do
  sync :metro
  sample :bd_ada
end

live_loop :snare do
  sync :bar
  sleep 2
  sample :drum_snare_hard
  sleep 0.5
  sample :drum_snare_hard
  sleep 0.5
  sample :drum_cymbal_open, pan: -1, amp: 0.5
  # sleep 1 #
  
end

live_loop :hits_hats do
  sync :metro
  with_fx :bpf do
    sample :drum_cymbal_soft
    sleep 0.5
    sample :drum_cymbal_soft
    sleep 0.499 # less than 0.5 !
  end
  
end



## as the loop takes 4 beats to play
## the musician behind the propher is not free to receive from the chief "the same man"
## so this live_loop will be played only every 2 bars
live_loop :player_prophet do
  ##| stop
  sync :bar
  use_synth :prophet
  with_fx :reverb do
    play_pattern_timed [:c2, :C3], 2, sustain: 0.5
    # this is surprenant but this is how it works.
  end
end

## as the loop takes less than 4 beats to play
## the musician behind the piano is free to receive from the chief "the same man !" and play 
live_loop :player_piano do
  ##| stop
  sync :bar
  use_synth :piano
  play_pattern_timed [:c3, :c2, :b6, :b5+0.15,
                      72, 72.25,72.5, :c2-0.15], 0.25, sustain: 0.1
  
end
1 Like