Sonic Pi – Advanced programming

Recently I stumbled upon Sonic Pi, a sequencer with which you can generate music programmatically. There exist versions for Windows, Mac, Linux and Raspberry Pi (that’s where the names comes from). For Windows you can get it as stickware, no installation needed. And the best: it’s free!

If you don’t have it yet, go and get it:

I was fascinated and spent hours exploring the endless possibilities of this software. As a developer my focus was on the possibilities of the programming language.

Sonic Pi Music Sequencer

In this article I sum up some of the programming questions I stumbled upon. As Sonic Pi aims at electronic artist, musicians and music teachers, the documentation is much better on the musical possibilities than on documenting programming features. Just try to find such topics as operators or data types in the official Sonic Pi documentation!

Some sort of Ruby

This is not an introduction into Sonic Pi, the basics you can perfectly learn in the excellent tutorial integrated in the program. No need to repeat this here. As I said before, here I document some advanced undocumented topics. The most important thing you have to know is that the programming language of Sonic Pi is a simplified Ruby. If you don’t find an answer to your programming question in the official Sonic Pi documentation you can just google it with Ruby. Many features of Ruby work, just try it out.

The wonders of division

Very early I stumbled over a phenomenon that can drive a programming newbie to tears. Just look at the following code:

my_number1 = 5/4
print my_number1 # result 1

my_number2 = 3/4
print my_number2 # result 0

Oops! As a Java programmer I’m not really astonished: The types of the two numbers define the type of the result. If you want to have a decimal as a result, at least one number has to be a decimal. If this is not the case, for example because the numbers are the lengths of lists (more of this later), then multiply one of them with 1.0, this will do the trick.

my_number1 = 1.0/3
print my_number1 # result 0.3333333333333333

my_number2 = 1/0.25
print my_number2 # result 4.0

my_number3 = 5/4
print my_number3 # result 1

my_number4 = 3/4
print my_number4 # result 0

my_number4 = (3*1.0)/4
print my_number4 # result 0.75

Note Names and Note Numbers

When using play in Sonic Pi you can use it with the note name, for example :c4 or its number value 60.

Of course, you can convert the names in numbers and vice versa.

# from note name to note value
print :c4.to_i                   # => 60

# from note value to note name
print note_info(60).midi_string  # => C4

# note name without octave
print Note.resolve_note_name(60) # => :C

print note_info(60).octave       # => 4
print note_info(60).pitch_class  # => :C
print note_info(60).midi_note    # => 60
print note_info(60).inspect      # => "#<SonicPi::Note :C4>"

# convert midi to Hertz value
print midi_to_hz(:c4)            # => 261.6255653005986

# calculate interval difference
print :c4 - :d4                  # => -2

# play midi_string
play note_info(61).midi_string


To make a lowercase symbol from the values of a scale is more complicated:

#define a minor scale
my_scale = scale :c4, :minor, num_octaves: 1
#fetch the second note and transform it to a symbol
my_note = note_info(my_scale[1]).midi_string.downcase.to_sym
print my_note # output :d4

String Concatenation

What a word! Basically it’s just the question how can sew text snippets together. Why do we need this an a music sequencer? For debugging I recommend to print out messages to the protocol window. The beginning is easy: Just put a +-sign between to texts:

print "Hello" + " World"

Works like a charm! But then you want to know the length of a sample:

print "Duration of ambi_glass_hum: " + sample_duration :ambi_glass_hum

Don’t be astonished that this gives you an ugly error message. Of course, it’s types again. Although you see nothing like a type declaration, Sonic Pi is a typed language. And as the result of sample_duration is a number, this cannot be concatenated with a string. There are two solutions for this problem:

  1. Use your number in the string with #{your_number}
  2. Convert the number to a string with .to_s
my_string_1 = "one"
my_string_2 = "two"
my_result_1 = "my_result_1: #{my_string_1}, #{my_string_2}"
print my_result_1

my_result_2 = "my_result_2: " + my_string_1 + ", " + my_string_2
print my_result_2

my_length = sample_duration :ambi_glass_hum
print "sample: #{my_length}"
# or
print "sample: " + my_length.to_s

Arrays, Maps and Rings

As many other programming languages, Sonic Pi knows arrays (or lists, there seems to be no difference) and maps. I can use these to save sequences of notes or note lengths. But there is a third element, which is very specific to music sequencers: rings. A ring is nothing else than an array that automatically starts again when it has reached the last element. This is very handy because in Sonic Pi you don’t work with counters as in a conventional progamming language (although it is possible). The universal counter that controls all your parallel loops is the tick. And as the loops can run a long time, the ticks will soon be much larger than the length of any array. If you convert an array, you don’t have to care for your index, Sonic Pi handles this.

From lists to rings and back

So it’s very useful, if you know how to convert an array to a ring and back with .ring and .to_a. Here is the code:

my_ring = [1, 2, 3].ring
my_array = my_ring.to_a

Reusable procedures with lists

But why use these collections at all? As a software developer, I’m interested in reusability. I don’t want to write dozens of lines of code for every new melody. My vision is to make procedures that can play any sort of melody you give them as an argument. The most simple form is a procedure with to arguments, a list of notes and list of note lengths.

Another important property of lists and maps is its lenght. You can get it just with .lenght, for example [1, 2, 3].length.

#use this procedure to play any simple melody
define :play_my_melody do |my_note_list, my_sleep_list|
  my_length = my_note_list.length

  my_length.times do
    my_counter = tick(:my_melody_tick)
    play my_note_list.ring[my_counter]
    sleep my_sleep_list.ring[my_counter]

my_melody_twinkle = [:c4, :c4, :g4, :g4, :a4, :a4, :g4]
my_sleep_twinkle = [1, 1, 1, 1, 1, 1, 2]
play_my_melody my_melody_twinkle, my_sleep_twinkle
sleep 2

my_melody_godownmoses = [:d4, :bb4, :bb4, :a4, :a4, :bb4, :bb4, :g4]
my_sleep_godownmoses = [1, 1, 1, 1, 1, 1, 1, 2]
play_my_melody my_melody_godownmoses, my_sleep_godownmoses

From a musical standpoint the result is not really convincing, but as a program it works.

Readability with nested lists

The weaknesses of this first version are evident: My control over the sound is pretty limited with the result, that the melody sounds extremely mechanical. And the two list must have the same length. With a longer melody it can be a pretty tedious task to count which note has which length. It would be nice to be able to safe a note and its length together. And even better if I could use more of the endless possibilities to change single note, for example its loudness. This is possible, if I use a nested array.

#use this procedure to play any simple melody
define :play_my_melody do |my_melody_list|
  #reset the local tick
  my_length = my_melody_list.length

  #loop through the notes of the outer list
  my_length.times do
    my_counter = tick(:my_melody_tick)
    #fetch the first element of the inner list
    play my_melody_list.ring[my_counter][0]
    #fetch the second element of the inner list
    sleep my_melody_list.ring[my_counter][1]

my_melody_godownmoses = [[:d4, 1],
[:bb4, 1],
[:bb4, 1],
[:a4,  1],
[:a4,  1],
[:bb4, 1],
[:bb4, 1],
[:g4,  2]
play_my_melody my_melody_godownmoses

It sounds as mechanical as before, but at least the code is more readable and a melody is easier to write. And you see, that with a third or a forth element in the nested array I can control other aspects like the amplitude (loudness). I leave it to you to go on in this direction.

Ruby Hashes (Hash Maps)

A very useful element in Ruby are hashes (in other programming languages called hash maps or just maps). Hashes store keys and their values. There are several ways to define a map:

The first syntax I found only for Sonic Pi, but not for Ruby:

my_map = (map note: :c4, times: 4, amp: 0.5, bpm: 120)

The second syntax is pure Ruby and the shortest and most elegant way:

my_map3 = {note: :c4, times: 4, amp: 0.5, bpm: 120}

The third syntax is called hash rocket syntax in Ruby. It’s quite awkward, I wouldn’t use it:

my_map3 = {:note => :c4, :times => 4, :amp => 0.5, :bpm => 120}

And that’s how you use the named values:

play my_map2[:note], amp: my_map2[:amp]

Of course, Ruby hashes are objects with a lot of useful methods:

#show all keys
print my_map2.keys

#show all values
print my_map2.values

#iterate over map
my_map2.each {|key, value| print "#{key} is #{value}" }

#iterate over keys
my_map2.each_key {|key| print "#{key} is #{my_map2[key]}" }

#is key existing
print 'my_map2.has_key?(:note): ' + my_map2.has_key?(:note).to_s
print "my_map2.has_key?(:x): #{my_map2.has_key?(:x)}"

Defaulting Option maps as arguments

Some things I want to control only once per melody, for exaple, how quick it is played or with which instrument it is played. Instead of using another array to control general settings like beats per minute (bpm) and the instrument (synth), I could use a map. This is more readable and I can program a fallback for all the settings that are not in the map.

This is the combined code:

#use this procedure to play any simple melody
define :play_my_melody do |my_melody_list, my_option_map|
  #this inline if falls back to a bpm of 80
  #if no element bpm is present in the map
  use_bpm my_option_map[:bpm] == nil ? 80 : my_option_map[:bpm]

  #fallback to piano if there is no element synth
  use_synth my_option_map[:synth] == nil ? :piano : my_option_map[:synth]

  #reset the local tick
  my_length = my_melody_list.length

  #the melody is repeated once if the map contains no times element
  my_times = my_option_map[:times] == nil ? 1 : my_option_map[:times]

  #repeat the whole melody
  my_times.times do
    #loop through the notes of the outer list
    my_length.times do
      my_counter = tick(:my_melody_tick)
      #fetch the first element of the inner list
      play my_melody_list.ring[my_counter][0]
      #fetch the second element of the inner list
      sleep my_melody_list.ring[my_counter][1]

#my_option_map = (map times: 2, bpm: 100, synth: :prophet)
my_option_map = (map times: 1, synth: :beep)
my_melody_godownmoses = [[:d4, 1],
[:bb4, 1],
[:bb4, 1],
[:a4,  1],
[:a4,  1],
[:bb4, 1],
[:bb4, 1],
[:g4,  2]
play_my_melody my_melody_godownmoses, my_option_map

This last example uses some other advanced stuff: Inline-Ifs. I take it for granted that you know the normal if. A more compact form, which exists also in other programming languages, compresses the three parts, condition, then part, else part just in one line, separated by ? and :.

In the line “use_bpm my_option_map[:bpm] == nil ? 80 : my_option_map[:bpm]”, the condition is “my_option_map[:bpm] == nil”, the then part is “80” and the else part is “my_option_map[:bpm]”. The result of this inline if gives you the value for use_bpm.

Looping over a list or map

With the method .each you can loop over a list or map. This can be used to listen to all available synths, as the function synth_names gives you a list of all synths.

synth_names.each do |my_synth|
  use_synth my_synth
  play :c4
  sleep 1
  play :e4
  sleep 2

Merging maps

Sometimes it’s useful to split someting into more than one map. With the following code the maps can be merged again:

my_map = my_map.merge(my_map_external)

Colons in front or after?

You may survive sonic pi programming quite well without knowing why the colons are sometimes in front and sometimes after. There event exists :: in Ruby, but in Sonic Pi I doubt that you ever will see it. Let me try to explain the mystery of colons:

If a colon is in front of a string, it defines a symbol. Symbols in Ruby are immutable and reusable constants. They are often used instead of string variables, because two symbols with the same name are identical and exist only once in memory. This makes comparing symbols really fast.

I assume that this performance advantage is the main reason, why you see them so often in Sonic Pi. With music you don’t want any programming to delay your sound, especially if you are not on a desktop or notebook but on a microcontroller like the raspberry pi with its limited memory and CPU.

A colon after a string we have just seen in the chapter of hashes: In a hash in the short syntax version, the key name is followed by a colon and then by the value:

my_map3 = {note: :c4, times: 4, amp: 0.5, bpm: 120}

Working with the length of samples

In Sonic Pi you work not only with notes (synth and play), but also with samples, recordes snippets of music. Samples have a certain length. This might be a problem, if the sample length goes not along with your tick.

There are two solutions for this problem. The first one is to read the length of the sample with sample_duration and to set the following sleep to this length. The second one is to stretch or compress it with beat_stretch according to your needs. But be careful, these manipulations change not only the length, but also the tone height. If you compress it, it becomes shorter and higher. If you stretch it, it becomes longer and deeper.

print sample_duration :ambi_dark_woosh # result: 3.702...

#play sample in full length
sample :ambi_dark_woosh
sleep sample_duration :ambi_dark_woosh
sleep 0.5

#make it longer
sample :ambi_dark_woosh, beat_stretch: 5
sleep 5
sleep 0.5

#make it shorter
sample :ambi_dark_woosh, beat_stretch: 2
sleep 2
sleep 0.5

sample :ambi_dark_woosh, beat_stretch: 1
sleep 1
sleep 0.5

Listen by yourself:

Sonic Pi Sample Duration

OSC and Python

Since Sonic Pi 3 there exists an interface to control Sonic Pi from an external application. The means for this is OSC (Open Sound Control). I tried the example with Python that can be found in the link below. Python should already be installed. Before you can start, you have to load the necessary library with the following steps in Windows:

  • open a command line
  • type “pip3 install python-osc” and press Return
  • you should a message like “Successfully installed python-osc-1.7.0”
  • control whether the package was installed with “pip3 list”

In Sonic Pi you should start a basic OSC listener as described in the link below. Just copy the code in the editor and run it. Don’t worry, you will hear nothing yet. Here is my slightly changed version of the listener code in Sonic Pi:

live_loop :my_loop do
 a, b, c, d, e = sync "/osc/trigger/synth"
 print a
 synth a.to_sym, note: b, cutoff: c, sustain: d, amp: e

Now you can open your python tool and copy the python code. As soon as you run it, you will hear a sound:

from pythonosc import osc_message_builder
from pythonosc import udp_client

sender = udp_client.SimpleUDPClient('', 4559)
sender.send_message('/trigger/synth', ["prophet", 60, 100, 8, 0.5])
sender.send_message('/trigger/synth', ["dpulse", 62, 100, 8, 1])


In the meantime I have written another article how to control Sonic Pi with an android tablet by OSC. 


Unfortunately, there is no such thing as include in Sonic Pi to load stuff from another file. There exists the load_buffer command, but with this you can only replace the code in your buffer window with the one from a file. That’s not exactly a replacement for include. However, definitions of functions are global for all buffer windows. A way to modularization would therefore be to place often used functions in a seperate file. Before you can use them, you load them with load_buffer, for example (Windows version):

load_buffer "D:/my_directory/my_pattern.rb"

As I have a lot of samples and user defined synths on my NAS, which is not always running, the following code is quite useful to find out whether the NAS is on.

my_external_root = "Z:/my_directory/"
if File.exists?(my_external_root)
  my_external_sample_directory = my_external_root + "freewavesamplescom/korg/"
  my_sample_hat = my_external_sample_directory + "korg-n1r-closed-hi-hat.wav"
  load_sample my_sample_hat
  print "NAS not loaded or mapped, map to drive Z:"

More Sonic Pi stuff

Well, for the moment, that’s all from me. But of course there is a lot of stuff out there, here some of my favority links:















Comments are closed.