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

define :foo do
  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.

Calling functions

Once we have defined our function we can call it by just writing its name:

define :foo do
  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:

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:

define :my_player do |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:

define :chord_player do |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!