Thursday, 11 February 2016

Sail with me into the Pi (part 1)

Prerequisite: This is a tutorial about Sonic Pi, a live-coding music platform using the Ruby programming language. If you want to code along, first go to the Sonic Pi Website and follow their instructions to download it.

Okay, a bit of background. From time to time I come across a song which becomes an 'earworm'. Getting stuck in my head, struggling to get out. One such song is Sail by AWOLNATION. I've sat and learned it for the guitar, beat-boxed that slow, immersive groove, I've sequenced it on my Network Ensemble project, and last Friday, it was the turn of Sonic Pi.

Now Sonic Pi, in short, is awesome. At a lower level, its pretty familiar to me, it works in the same way as Network Ensemble and Text to Music. When running code in Sonic Pi, it pushes messages to a sound engine (theirs is written in Supercollider), via a network port. The exciting part is that where I've written messages as and when I need them. They've made a very nice Ruby API for sound control, including a number of synthesisers, effects and samples, all ready for people to dive right in to using.

The really cool part is that, Sonic Pi is being used in schools, using music as instant feedback for young people to learn about code. Personally, I know that if I'd had this kind of introduction to code earlier on, it would have been a strong introduction to programming, and brought me much earlier into the trade that has become my life.

Okay, enough background, I used Sonic Pi to create a version of Sail, and I'd like to show you how I did it. The first thing I did was add something to the language, to add some methods which I'd like to see.

Yes, I know I'm starting with something pretty complicated, but trust me, this will make the rest much easier to understand. Here's the code:

module NumberMethods
  def beat; self; end
  def beats; self; end
  def triplet; self.to_f / 3; end
  def triplets; self.to_f / 3; end
  def bar; self * 4; end
  def bars; self * 4; end
end
::Numeric.send(:include, NumberMethods)

So what's happening here is that I'm creating a Ruby module containing some methods, then I'm including that module in the Numeric class. This means that I now have methods named beat, triplet, bar etc. when I use a number in my code. It's fairly simple code, beat and bats return the number itself, unchanged, triplet and triplets return a third of that number, and bar returns 4 times the number.

This is because all of the time operations in Sonic Pi are based on beats. So while usually `sleep 1` in Ruby will wait for 1 second before continuing, in Sonic Pi it will wait for one beat, an amount of time defined by the Beats Per Minute (or BPM). You can use the method `use_bpm` to change the speed, and so `sleep 1` will wait for 1 second if you first call `use_bpm 60` or for half a second if you first call `use_bpm 120`. The song Sail, has 4 beats in the bar, and divides its beats into 3, known in music as a triplet, so that's why I've given myself these methods.

Just to prove that this works, I can write some code using my new methods and the Ruby `puts` method, then run it (alt + r) to see that the expected changes have been made to those numbers.


On the log (right) we can see that 1.beat is, as expected, 1. 1.bar is 4 beats (the number 4) and 2.triplets is two thirds (0.66666666). This means that instead of worrying about our note values, or repeatedly writing the code to divide 1 by 3, we can use these names, known to most musicans, in our code. By making this small change to the way numbers work in Ruby, we have made our code more Domain Specific, meaning we can write our code in a way that's more like the way we would talk to people who have some knowledge in the domain of music.

Okay, we've named our note values, let's have a little look at the notes. At the start of the song, we hear what's sometimes called a vamp, the current chord is played in a way which sets out the rhthmic feel of the piece.

If we analyse this a little further, we hear that it's playing a chord (a collection of notes, played together) on each beat, for the first 2 thirds of each beat, laying out the 'triplet' feel to the song. So what I really want to make the music do is play a combination of notes a given number of times, with this triplet feel.

Once you've got used to writing code, you can usually write what might be described as Speculative Code or, as I like to think of it, Fantasy Code. We write the code we would like to work, then see if we can make it work. We can imagine a line of code which says "vamp Eb and Ab for 2 beats". It would look a little bit like this:

vamp([:Eb4, Ab4], 2)

And there we have the first two beats of the song, right? Well, not quite. There's no method called 'vamp' in Sonic Pi, so we'll have to write one. It looks like this:


Let's break this down a little. Line 12 uses the def keyword, so we are defining a method. This means that when we use the word 'vamp' in future, Ruby will look to this definition to decide what to do. Inside the brackets we are naming two arguments, notes and length, which will be expected when we call the `vamp` method, to tell it how, exactly, to vamp on that occasion.

Line 13 tells Sonic Pi to use the synthesizer named :tri, so when we call `play` on line 15, it will play those notes on the :tri synthesiser.

Line 14 uses the do keyword to start a block, so whatever number is passed in for the 'length' argument, it will run the code between do and the end on line 17 that many times.

Line 15 shows us the first bit of Sonic Pi sound making. The `play` method will play the notes we give it on a synthesiser. We're just passing in the notes that have been set as the 'notes' argument, then there's some more named arguments for the `play` method.

These all belong to the volume envelope, commonly described as an ADSR envelope. We don't want it to take any noticeable time to increase the volume of this note from silence, so I've set the attack argument to 0.01, a negligable amount. Now, I want this vamp to play for the first 2 triplets of each beat, so I'm setting sustain to 1 triplet, so that will play for the first triplet at full volume, then release is the next triplet, the third triplet is silent. This is a strong way to introduce this triplet 'feel' to the song. Notice I've used the `triplet` method we introduced earlier, instead of dividing 1 by 3 repeatedly.

An important thing to remember about the `play` method is that it will not stop your code working until it's done. If we were to call `play` again immediately afterwards, it would play at the same time as the first. For this reason, we need to tell it to wait for 1 beat before playing again. This is what the code on line 16 says.

Line 18 uses the end keyword to finish defining the `vamp` method. We're done.

Now to use it, on line 20 I set the beats per minute to 120, simply deciding how fast I want this to be played.

Then, on line 21, we've got the fantasy code I wrote earlier, hoping we could make it work, and now it does. A quick alt + r and success! It sounds a little like the first two notes of the song.


Okay, so that seems pretty hard work for one chord, played twice. But that's the beauty of it, we've written a method that we can use to play this vamp on any chord for any amount of time. So we can drop in the rest of the opening sequence with a few more vamp methods.


So the vamp method has paid off, we've said "play this for this amount of time" 8 times instead of re-writing the specific instructions on lines 15 and 16 over and over again. Without moving the repeated code into the `vamp` method the instructions in these 8 lines would take 34 lines of code, all of which would be unchanged. With all that near-identical code it is very difficult to read the code and work out what, exactly, it is going to do. So we've made it shorter and easier to read.

It sounds like this:

We've go the introduction, the first 8 bars sounding ok. There's just two little changes I'm going to make. Firstly, the synthesiser the vamp is played on is inside the `vamp` method, so when this grows bigger, we'll have to find the method in order to change this information. So I'm going to define this in a variable, earlier on.


This is a change to the code which won't change what it actually does, known as a refactor. We've done exactly the same thing, slightly differently. This does open up a few possibilities for future changes, however. For one, when there's a lot more code, we can define all of the synths up at the top, so we could change what kind of sound each part will make, without having to first dig up the method they are using. The second is that we could change the synth dynamically, in another part of the code, but more on that later.

The second change I'd like to make is one to the way it sounds. We've added our vamp, and it sounds ok, but we can make it sound better. We're going to use an effect. We do this by putting our play method inside a `with_fx` block, like so:


Now, Sonic Pi provides some very clear and full help for all of it's effects. You just click help on the top-right of the Sonic Pi window.

Then select the Fx tab, at the bottom, then select the effect we're using, in this case, reverb.


So, armed with this information, we can add a few arguments to change the amount of reverb that'll be applied to our vamp:


That's it for today, it sounds good. Try it yourself. Here's all the code in this tutorial, you can just copy and paste it into Sonic Pi (gotcha: Sonic Pi uses alt + v to paste, not ctrl + v). 


module NumberMethods
  def beat; self; end
  def beats; self; end
  def triplet; self.to_f / 3; end
  def triplets; self.to_f / 3; end
  def bar; self * 4; end
  def bars; self * 4; end
end
::Numeric.send(:include, NumberMethods)

@vamp_synth = :tri

def vamp(notes, length)
  use_synth @vamp_synth
  length.times do
    with_fx :reverb, damp: 0.8, room: 0.4 do
      play notes, attack: 0.01, sustain: 1.triplets, release: 1.triplet
      sleep 1.beat
    end
  end
end

use_bpm 120
vamp([:Eb4, :Ab4], 2)
vamp([:eb4, :gb4, :eb3], 6)
vamp([:eb4, :ab4, :eb3], 2)
vamp([:eb4, :gb4, :eb3], 6)
vamp([:eb4, :ab4, :eb3], 2)
vamp([:db4, :f4,  :db3], 8)
vamp([:gb4, :db5, :gb3], 4)
vamp([:db4, :f4,  :db3], 4)


I'm done for now, but keep an eye on this site, there'll be more of this tutorial. Next time, we're going to look at some of those heavy bass sounds and get the two parts playing together.

Thanks for reading, feel free to get in touch on twitter, where I'm called @MarmiteJunction, or drop a comment here. I'm always up for chatting code.

No comments:

Post a Comment