Press Run to start the engine: its live metrics show here.
5 Programming Structures
Now that you’ve learned the basics of creating sounds with play and
sample and creating simple melodies and rhythms by sleeping between
sounds, you might be wondering what else the world of code can offer
you…
Well, you’re in for an exciting treat! It turns out that basic
programming structures such as looping, conditionals, functions and
threads give you amazingly powerful tools to express your musical ideas.
Let’s get stuck in with the basics…
5.1 Blocks
A structure you’ll see a lot in Sonic Pi is the block. Blocks allow us
to do useful things with large chunks of code. For example, with synth
and sample parameters we were able to change something that happened on
a single line. However, sometimes we want to do something meaningful to
a number of lines of code. For example, we may wish to loop it, to add
reverb to it, to only run it 1 time out of 5, etc. Consider the
following code:
Blocks
play 50
sleep 0.5
sample :elec_plip
sleep 0.5
play 62
To do something with a chunk of code, we need to tell Sonic Pi where
the code block starts and where it ends. We use do for start and
end for end. For example:
Blocks · 2
do
play 50
sleep 0.5
sample :elec_plip
sleep 0.5
play 62
end
However, this isn’t yet complete and won’t work (try it and you’ll get
an error) as we haven’t told Sonic Pi what we want to do with this
do/end block. We tell Sonic Pi this by writing some special code
before the do. We’ll see a number of these special pieces of code
later on in this tutorial. For now, it’s important to know that wrapping
your code within do and end tells Sonic Pi you wish to do something
special with that chunk of code.
5.2 Iteration and Loops
So far we’ve spent a lot of time looking at the different sounds you
can make with play and sample blocks. We’ve also learned how to
trigger these sounds through time using sleep.
As you’ve probably found out, there’s a lot of fun you can have with
these basic building blocks. However, a whole new dimension of fun opens
up when you start using the power of code to structure your music and
compositions. In the next few sections we’ll explore some of these
powerful new tools. First up is iteration and loops.
Repetition
Have you written some code you’d like to repeat a few times? For
example, you might have something like this:
Repetition
play 50
sleep 0.5
sample :elec_blup
sleep 0.5
play 62
sleep 0.25
What if we wished to repeat this 3 times? Well, we could do something
simple and just copy and paste it three times:
Repetition · 2
play 50
sleep 0.5
sample :elec_blup
sleep 0.5
play 62
sleep 0.25
play 50
sleep 0.5
sample :elec_blup
sleep 0.5
play 62
sleep 0.25
play 50
sleep 0.5
sample :elec_blup
sleep 0.5
play 62
sleep 0.25
Now that’s a lot of code! What happens if you want to change the
sample to :elec_plip? You’re going to have to find all the places
with the original :elec_blup and switch them over. More importantly,
what if you wanted to repeat the original piece of code 50 times or
1000? Now that would be a lot of code, and a lot of lines of code to
alter if you wanted to make a change.
Iteration
In fact, repeating the code should be as easy as saying do this three
times. Well, it pretty much is. Remember our old friend the code
block? We can use it to mark the start and end of the code we’d like
to repeat three times. We then use the special code 3.times. So,
instead of writing do this three times, we write 3.times do -
that’s not too hard. Just remember to write end at the end of the
code you’d like to repeat:
Iteration
3.times do
play 50
sleep 0.5
sample :elec_blup
sleep 0.5
play 62
sleep 0.25
end
Now isn’t that much neater than cutting and pasting! We can use this to
create lots of nice repeating structures:
Iteration · 2
4.times do
play 50
sleep 0.5
end
8.times do
play 55, release:0.2
sleep 0.25
end
4.times do
play 50
sleep 0.5
end
Nesting Iterations
We can put iterations inside other iterations to create interesting
patterns. For example:
Nesting Iterations
4.times do
sample :drum_heavy_kick
2.times do
sample :elec_blip2, rate:2
sleep 0.25
end
sample :elec_snare
4.times do
sample :drum_tom_mid_soft
sleep 0.125
end
end
Looping
If you want something to repeat a lot of times, you might find yourself
using really large numbers such as 1000.times do. In this case, you’re
probably better off asking Sonic Pi to repeat forever (at least until
you press the stop button!). Let’s loop the amen break forever:
Looping
loop do
sample :loop_amen
sleep sample_duration :loop_amen
end
The important thing to know about loops is that they act like black
holes for code. Once the code enters a loop it can never leave until you
press stop - it will just go round and round the loop forever. This
means if you have code after the loop you will never hear it. For
example, the cymbal after this loop will never play:
Looping · 2
loop do
play 50
sleep 1
end
sample :drum_cymbal_open
Now, get structuring your code with iteration and loops!
5.3 Conditionals
A common thing you’ll likely find yourself wanting to do is to not only
play a random note (see the previous section on randomness) but also
make a random decision and based on the outcome run some code or some
other code. For example, you might want to randomly play a drum or a
cymbal. We can achieve this with an if statement.
Flipping a Coin
So, let’s flip a coin: if it’s heads, play a drum, if it’s tails, play a
cymbal. Easy. We can emulate a coin flip with our one_in function
(introduced in the section on randomness) specifying a probability of 1
in 2: one_in(2). We can then use the result of this to decide between
two pieces of code, the code to play the drum and the code to play the
cymbal:
Flipping a Coin
loop do
if one_in(2)
sample :drum_heavy_kick
else
sample :drum_cymbal_closed
end
sleep 0.5
end
Notice that if statements have three parts:
The question to ask
The first choice of code to run (if the answer to the question is yes)
The second choice of code to run (if the answer to the question is no)
Typically in programming languages, the notion of yes is represented by
the term true and the notion of no is represented by the term
false. So we need to find a question that will give us a true or
false answer which is exactly what one_in does.
Notice how the first choice is wrapped between the if and the else
and the second choice is wrapped between the else and the end. Just
like do/end blocks you can put multiple lines of code in either
place. For example:
Flipping a Coin · 2
loop do
if one_in(2)
sample :drum_heavy_kick
sleep 0.5
else
sample :drum_cymbal_closed
sleep 0.25
end
end
This time we’re sleeping for a different amount of time depending on
which choice we make.
Simple if
Sometimes you want to optionally execute just one line of code. This is
possible by placing if and then the question at the end. For example:
Simple if
use_synth :dsaw
loop do
play 50, amp:0.3, release:2
play 53, amp:0.3, release:2if one_in(2)
play 57, amp:0.3, release:2if one_in(3)
play 60, amp:0.3, release:2if one_in(4)
sleep 1.5
end
This will play chords of different numbers with the chance of each note
playing having a different probability.
5.4 Threads
So you’ve made your killer bassline and a phat beat. How do you play
them at the same time? One solution is to weave them together manually -
play some bass, then a bit of drums, then more bass… However, the
timing soon gets hard to think about, especially when you start weaving
in more elements.
What if Sonic Pi could weave things for you automatically? Well, it can,
and you do it with a special thing called a thread.
Infinite Loops
To keep this example simple, you’ll have to imagine that this is a
phat beat and a killer bassline:
Infinite Loops
loop do
sample :drum_heavy_kick
sleep 1
end
loop do
use_synth :fm
play 40, release:0.2
sleep 0.5
end
As we’ve discussed previously, loops are like black holes for the
program. Once you enter a loop you can never exit from it until you hit
stop. How do we play both loops at the same time? We have to tell Sonic
Pi that we want to start something at the same time as the rest of the
code. This is where threads come to the rescue.
Threads to the Rescue
Threads to the Rescue
in_thread do
loop do
sample :drum_heavy_kick
sleep 1
end
end
loop do
use_synth :fm
play 40, release:0.2
sleep 0.5
end
By wrapping the first loop in an in_thread do/end block we tell Sonic
Pi to run the contents of the do/end block at exactly the same time as
the next statement after the do/end block (which happens to be the
second loop). Try it and you’ll hear both the drums and the bassline
weaved together!
Now, what if we wanted to add a synth on top. Something like:
Threads to the Rescue · 2
in_thread do
loop do
sample :drum_heavy_kick
sleep 1
end
end
loop do
use_synth :fm
play 40, release:0.2
sleep 0.5
end
loop do
use_synth :zawa
play 52, release:2.5, phase:2, amp:0.5
sleep 2
end
Now we have the same problem as before. The first loop is played at the
same time as the second loop due to the in_thread. However, the third
loop is never reached. We therefore need another thread:
Threads to the Rescue · 3
in_thread do
loop do
sample :drum_heavy_kick
sleep 1
end
end
in_thread do
loop do
use_synth :fm
play 40, release:0.2
sleep 0.5
end
end
loop do
use_synth :zawa
play 52, release:2.5, phase:2, amp:0.5
sleep 2
end
Runs as threads
What may surprise you is that when you press the Run button, you’re
actually creating a new thread for the code to run. This is why pressing
it multiple times will layer sounds over each other. As the runs
themselves are threads, they will automatically weave the sounds
together for you.
Scope
As you learn how to master Sonic Pi, you’ll learn that threads are the
most important building blocks for your music. One of the important jobs
they have is to isolate the notion of current settings from other
threads. What does this mean? Well, when you switch synths using
use_synth you’re actually just switching the synth in the current
thread - no other thread will have their synth switched. Let’s see this
in action:
Scope
play 50
sleep 1
in_thread do
use_synth :tb303
play 50
end
sleep 1
play 50
Notice how the middle sound was different to the others? The use_synth
statement only affected the thread it was in and not the outer main run
thread.
Inheritance
When you create a new thread with in_thread, the new thread will
automatically inherit all of the current settings from the current
thread. Let’s see that:
Inheritance
use_synth :tb303
play 50
sleep 1
in_thread do
play 55
end
Notice how the second note is played with the :tb303 synth even though
it was played from a separate thread? Any of the settings modified with
the various use_* functions will behave in the same way.
When threads are created, they inherit all the settings from their
parent but they don’t share any changes back.
Naming Threads
Finally, we can give our threads names:
Naming Threads
in_thread(name::bass) do
loop do
use_synth :prophet
play chord(:e2, :m7).choose, release:0.6
sleep 0.5
end
end
in_thread(name::drums) do
loop do
sample :elec_snare
sleep 1
end
end
Look at the log pane when you run this code. See how the log reports the
name of the thread with the message?
Naming Threads · 2
[Run36, Time4.0, Thread:bass]
|- synth :prophet, {release:0.6, note:47}
Only One Thread per Name Allowed
One last thing to know about named threads is that only one thread of
a given name may be running at the same time. Let’s explore this.
Consider the following code:
Only One Thread per Name Allowed
in_thread do
loop do
sample :loop_amen
sleep sample_duration :loop_amen
end
end
Go ahead and paste that into a buffer and press the Run button. Press
it again a couple of times. Listen to the cacophony of multiple amen
breaks looping out of time with each other. Ok, you can press Stop now.
This is the behaviour we’ve seen again and again - if you press the Run
button, sound layers on top of any existing sound. Therefore if you have
a loop and press the Run button three times, you’ll have three layers of
loops playing simultaneously.
However, with named threads it is different:
Only One Thread per Name Allowed · 2
in_thread(name::amen) do
loop do
sample :loop_amen
sleep sample_duration :loop_amen
end
end
Try pressing the Run button multiple times with this code. You’ll only
ever hear one amen break loop. You’ll also see this in the log:
Only One Thread per Name Allowed · 3
==> Skipping thread creation: thread with name :amen already exists.
Sonic Pi is telling you that a thread with the name :amen is already
playing, so it’s not creating another.
This behaviour may not seem immediately useful to you now - but it will
be very handy when we start to live code…
5.5 Functions
Once you start writing lots of code, you may wish to find a way to
organise and structure things to make them tidier and easier to
understand. Functions are a very powerful way to do this. They give us
the ability to give a name to a bunch of code. Let’s take a look.
Defining functions
Defining functions
define :foodo
play 50
sleep 1
play 55
sleep 2
end
Here, we’ve defined a new function called foo. We do this with our old
friend the do/end block and the magic word define followed by the name
we wish to give to our function. We didn’t have to call it foo, we could
have called it anything we want such as bar, baz or ideally
something meaningful to you like main_section or lead_riff.
Remember to prepend a colon : to the name of your function when you
define it. Function names should also start with a lower case letter, so
use :foo rather than :Foo.
Calling functions
Once we have defined our function we can call it by just writing its
name:
Calling functions
define :foodo
play 50
sleep 1
play 55
sleep 0.5
end
foo
sleep 1
2.times do
foo
end
We can even use foo inside iteration blocks or anywhere we may have
written play or sample. This gives us a great way to express
ourselves and to create new meaningful words for use in our compositions.
Functions are remembered across runs
So far, every time you’ve pressed the Run button, Sonic Pi has started
from a completely blank slate. It knows nothing except for what is in
the buffer. You can’t reference code in another buffer or another
thread. However, functions change that. When you define a function,
Sonic Pi remembers it. Let’s try it. Delete all the code in your
buffer and replace it with:
Functions are remembered across runs
foo
Press the Run button - and hear your function play. Where did the code
go? How did Sonic Pi know what to play? Sonic Pi just remembered your
function - so even after you deleted it from the buffer, it
remembered what you had typed. This behaviour only works with functions
created using define (and defonce).
Parameterised functions
You might be interested in knowing that just like you can pass min and
max values to rrand, you can teach your functions to accept
arguments. Let’s take a look:
Parameterised functions
define :my_playerdo |n|
play n
end
my_player 80
sleep 0.5
my_player 90
This isn’t very exciting, but it illustrates the point. We’ve created
our own version of play called my_player which is parameterised.
The parameters need to go after the do of the define do/end block,
surrounded by vertical goalposts | and separated by commas ,. You
may use any words you want for the parameter names.
The magic happens inside the define do/end block. You may use the
parameter names as if they were real values. In this example I’m playing
note n. You can consider the parameters as a kind of promise that
when the code runs, they will be replaced with actual values. You do
this by passing a parameter to the function when you call it. I do this
with my_player 80 to play note 80. Inside the function definition, n
is now replaced with 80, so play n turns into play 80. When I call
it again with my_player 90, n is now replaced with 90, so play n
turns into play 90.
Let’s see a more interesting example:
Parameterised functions · 2
define :chord_playerdo |root, repeats|
repeats.times do
play chord(root, :minor), release:0.3
sleep 0.5
end
end
chord_player :e3, 2
sleep 0.5
chord_player :a3, 3
chord_player :g3, 4
sleep 0.5
chord_player :e3, 3
Here I used repeats as if it was a number in the line repeats.times
do. I also used root as if it was a note name in my call to play.
See how we’re able to write something very expressive and easy to read
by moving a lot of our logic into a function!
5.6 Variables
A useful thing to do in your code is to create names for things. Sonic
Pi makes this very easy: you write the name you wish to use, an equal
sign (=), then the thing you want to remember:
Variables
sample_name = :loop_amen
Here, we’ve ‘remembered’ the symbol :loop_amen in the variable
sample_name. We can now use sample_name everywhere we might have
used :loop_amen. For example:
Variables · 2
sample_name = :loop_amen
sample sample_name
There are three main reasons for using variables in Sonic Pi:
communicating meaning, managing repetition and capturing the results
of things.
Communicating Meaning
When you write code it’s easy to just think you’re telling the computer
how to do stuff - as long as the computer understands it’s OK. However,
it’s important to remember that it’s not just the computer that reads
the code. Other people may read it too and try to understand what’s going
on. Also, you’re likely to read your own code in the future and try to
understand what’s going on. Although it might seem obvious to you now -
it might not be so obvious to others or even your future self!
One way to help others understand what your code is doing is to write
comments (as we saw in a previous section). Another is to use meaningful
variable names. Look at this code:
Communicating Meaning
sleep 1.7533
Why does it use the number 1.7533? Where did this number come from?
What does it mean? However, look at this code:
Communicating Meaning · 2
loop_amen_duration = 1.7533
sleep loop_amen_duration
Now, it’s much clearer what 1.7533 means: it’s the duration of the
sample :loop_amen! Of course, you might say why not simply write:
Communicating Meaning · 3
sleep sample_duration(:loop_amen)
Which, of course, is a very nice way of communicating the intent of the
code.
Managing Repetition
Often you see a lot of repetition in your code and when you want to
change things, you have to change it in a lot of places. Take a look at
this code:
Managing Repetition
sample :loop_amen
sleep sample_duration(:loop_amen)
sample :loop_amen, rate:0.5
sleep sample_duration(:loop_amen, rate:0.5)
sample :loop_amen
sleep sample_duration(:loop_amen)
We’re doing a lot of things with :loop_amen! What if we wanted to
hear what it sounded like with another loop sample such as
:loop_garzul? We’d have to find and replace all :loop_amens with
:loop_garzul. That might be fine if you have lots of time - but what
if you’re performing on stage? Sometimes you don’t have the luxury of
time - especially if you want to keep people dancing.
What if you’d written your code like this:
Managing Repetition · 2
sample_name = :loop_amen
sample sample_name
sleep sample_duration(sample_name)
sample sample_name, rate:0.5
sleep sample_duration(sample_name, rate:0.5)
sample sample_name
sleep sample_duration(sample_name)
Now, that does exactly the same as above (try it). It also gives us
the ability to just change one line sample_name = :loop_amen to
sample_name = :loop_garzul and we change it in many places through
the magic of variables.
Capturing Results
Finally, a good motivation for using variables is to capture the results
of things. For example, you may wish to do things with the duration of a
sample:
Capturing Results
sd = sample_duration(:loop_amen)
We can now use sd anywhere we need the duration of the :loop_amen
sample.
Perhaps more importantly, a variable allows us to capture the result
of a call to play or sample:
Capturing Results · 2
s = play 50, release:8
Now we have caught and remembered s as a variable, which allows us
to control the synth as it is running:
Capturing Results · 3
s = play 50, release:8
sleep 2
control s, note:62
We’ll look into controlling synths in more detail in a later section.
Warning: Variables and Threads
Whilst variables are great for giving things names and capturing the
results of things, it is important to know that they should typically
only be used locally within a thread. For example, don’t do this:
Warning: Variables and Threads
a = (ring 6, 5, 4, 3, 2, 1)
live_loop :sorteddo
a = a.sort
sleep 0.5
puts "sorted: ", a
end
live_loop :shuffleddo
a = a.shuffle
sleep 0.5
end
In the above example we assign a ring of numbers to a variable a and
then used it within two separate live_loops. In the first live loop
every 0.5s we sort the ring (to (ring 1, 2, 3, 4, 5, 6)) and then
print it out to the log. If you run the code, you’ll find that the
printed list is not always sorted!. This may surprise you - especially
that sometimes the list is printed as sorted, and sometimes it is
not. This is called non-deterministic behaviour and is the result of a
rather nasty problem called a race-condition. The problem is due to the
fact that the second live loop is also manipulating the list (in this
case shuffling it) and by the time the list is printed, sometimes it has
just been sorted and sometimes it has just been shuffled. Both live
loops are racing to do something different to the same variable and
every time round a different loop ‘wins’.
There are two solutions to this. Firstly, don’t use the same variable
in multiple live loops or threads. For example, the following code will
always print a sorted list as each live loop has its own separate
variable:
Warning: Variables and Threads · 2
live_loop :shuffleddo
a = (ring 6, 5, 4, 3, 2, 1)
a = a.shuffle
sleep 0.5
end
live_loop :sorteddo
a = (ring 6, 5, 4, 3, 2, 1)
a = a.sort
sleep 0.5
puts "sorted: ", a
end
However, sometimes we do want to share things across threads. For
example, the current key, BPM, synth etc. In these cases, the solution
is to use Sonic Pi’s special thread-safe state system via the fns get
and set. This is discussed later on in section 10.
5.7 Thread Synchronisation
Once you have become sufficiently advanced live coding with a number of
functions and threads simultaneously, you’ve probably noticed that it’s
pretty easy to make a mistake in one of the threads which kills
it. That’s no big deal, because you can easily restart the thread by
hitting Run. However, when you restart the thread it is now out of
time with the original threads.
Inherited Time
As we discussed earlier, new threads created with in_thread inherit
all of the settings from the parent thread. This includes the current
time. This means that threads are always in time with each other when
started simultaneously.
However, when you start a thread on its own it starts with its own
time which is unlikely to be in sync with any of the other currently
running threads.
Cue and Sync
Sonic Pi provides a solution to this problem with the functions cue
and sync.
cue allows us to send out heartbeat messages to all other threads. By
default the other threads aren’t interested and ignore these heartbeat
messages. However, you can easily register interest with the sync
function.
The important thing to be aware of is that sync is similar to
sleep in that it stops the current thread from doing anything for a
period of time. However, with sleep you specify how long you want to
wait while with sync you don’t know how long you will wait - as
sync waits for the next cue from another thread which may be soon
or a long time away.
Let’s explore this in a little more detail:
Cue and Sync
in_thread do
loop do
cue :tick
sleep 1
end
end
in_thread do
loop do
sync :tick
sample :drum_heavy_kick
end
end
Here we have two threads - one acting like a metronome, not playing any
sounds but sending out :tick heartbeat messages every beat. The
second thread is synchronising on tick messages and when it receives
one it inherits the time of the cue thread and continues running.
As a result, we will hear the :drum_heavy_kick sample exactly when
the other thread sends the :tick message, even if the two threads
didn’t start their execution at the same time:
Cue and Sync · 2
in_thread do
loop do
cue :tick
sleep 1
end
end
sleep(0.3)
in_thread do
loop do
sync :tick
sample :drum_heavy_kick
end
end
That naughty sleep call would typically make the second thread out
of phase with the first. However, as we’re using cue and sync, we
automatically sync the threads bypassing any accidental timing
offsets.
Cue Names
You are free to use whatever name you’d like for your cue messages -
not just :tick. You just need to ensure that any other threads are
syncing on the correct name - otherwise they’ll be waiting for ever
(or at least until you press the Stop button).
Let’s play with a few cue names:
Cue Names
in_thread do
loop do
cue [:foo, :bar, :baz].choose
sleep 0.5
end
end
in_thread do
loop do
sync :foo
sample :elec_beep
end
end
in_thread do
loop do
sync :bar
sample :elec_flip
end
end
in_thread do
loop do
sync :baz
sample :elec_blup
end
end
Here we have a main cue loop which is randomly sending one of the
heartbeat names :foo, :bar or :baz. We then also have three loop
threads syncing on each of those names independently and then playing a
different sample. The net effect is that we hear a sound every 0.5
beats as each of the sync threads is randomly synced with the cue
thread and plays its sample.
This of course also works if you order the threads in reverse as the
sync threads will simply sit and wait for the next cue.
Sonic Pi
Lowering the many barriers of entry for creative experiences with code.