Sonic Pi: beware where it can lead you!

binary

I have added a video of this program driving iTunes visualiser to my youtube channel. See it at https://youtu.be/6V3wqwxYimA

Recently I wrote some code in Sonic Pi which investigated the production of a rhythmic pattern based on the binary numbers 0 to 15. This was inspired by an article I found at http://bernhardwagner.net/musings/RPABN.html where the writer use hex names to represent rhythmic patterns: thus E3 was 11100011 and A7 was 10100111. He also explored generating new patterns by shifting the bits in the original pattern 1 bit left or right. Thus E3 becomes 11000111 or C7 when you rotate it. In the program I developed I started with 4 strings, representing the number 0 to 15

0000000100100011 0100010101100111 1000100110101011 1100110111101111
These were assigned to 4 ring variables r1 to r4, and then 4 live_loops were set up, each one using a different sequence of these 4 rings r1r2r3r4 r2r3r4r1 r3r4r1r2 and r4r1r2r3

Each was used to play either a sequence of notes (chosen at random) from a pentatonic scale, with the tonic note changing using the sequence :c3,:g4,:g2,:c3. The mechanism used was to use the “look” function to test in turn each digit in the sequence and to play the note (two ocataves higher) if a 1 was detected. In each live loop a second note was played (at the base pitch) using the inverse rhythm, ie when a 0 was detected.
The base note altered every 64 ticks, and every 16 ticks the individual rings in each sequence were rotated either clockwise or anti-clockwise). As a rhythmic pattern, the resulting sounds continued ad infinitum, and to give a more presentable end result I decided to play the pattern for a given number of cycles, and to sandwich it between a winding up and winding down start and finish note generated by the fm synth with varying note pitch. I also added a level control so that the sound could gradually fade away before stopping.

The end result was quite pleasing, and I placed a recording on soundcloud.com and published to code on my gist site. By now I had become quite hooked on experimenting with the system, and lots of questions along the line of “I wonder what would happen if?…” sprang to mind. The result of a further couple of days of playing and experimenting resulted in version 2 being born, which I present here. I do this partly for my own benefit, as there are so many bits added that it is quite difficult to follow all the ins and outs of the final program. But here goes, and I hope that you will find it interesting and informative.

The first thing I decided to do was to emphasise the rotating nature of the patterns, by adding an accent to the first pulse of each 16. Here I must say that the one thing that makes the whole program possible is the use of tick and look functions. These can be very powerful in enabling to make selections and changes depending on the current values returned by these functions. In its simplest form tick is a counter that increments by 1 every time you invoke it, whereas look returns the current value that tick holds without altering it. I find it best to use one isolated tick at the start of a loop and then to use multiple look functions to extract its value at other position in the loop. You can use this value to iterate around the values in a ring variable. Thus if a=(ring 2,4,6,8) then a.look will return 2,4,6,8,2,4,6,8… as look changes from 0,1,2,3,4,5,6,7….
However, as well as a basic tick, you can create other tick counters with statements like
tick_set :foo,look/4  In this case as look increases you will get this:
look value:            0,1,2,3,4,5,6,7,8,9,10,11,12,13,14…
look(:foo) value    0,0,0,0,1,1,1,1,2,2,2,2,3,3….
That is look(:foo) increases at quarter of the rate.
Another useful trick I use to give a trigger say every 16 increments is:
puts”triggered” if look%16==0
which will print triggered every 16 times round the loop.

Secondly, in order to hear the rhythm “make up” of the four parts more clearly I decided to utilise the use_bpm_mul function to slow down the tempo during the first half of the “performance” and to speed it back up to its starting value during the second half. The user specifies the percentage drop in tempo required. (figures 0 to 30 are probably appropriate) and a factor is calculated to progressively change the tempo every 64 pulses after the first 32. Halfway through the factor is inverted e.g. 0.8 becomes 1.0/0.8 = 1.25 and the tempo speeds up again. By setting the percentage change to 0 the tempo will remain constant throughout.

The third change was to allow different scale structures rather than the original minor_pentatonic. To do this, I choose a scale type at random, and generated a list of the note offsets for the scale using scale(0,scale_name)
{for octave 0} and I then shuffled this to put the offsets in random order and generated the note by adding in the note value for the base note sequence :c3,:g3,:g2,:c3. To give further variation I also shuffled this sequence. I used the same shuffled sequence for each of the four live_loops x1 to x4, but for two of them I reversed the order of the shuffled sequence. Further flexibility came from having an alternative base note sequence :c3,:f3,:g3,:g2 which could be selected.

With quite a complex range of notes being played, it was sometimes difficult to get the full sense of the rhythms, so I added an alternative note selection, where instead of playing a sequence based on the notes in a given scale type, I played just the base note for each thread, with an offset for threads 2,3 and 4 to give the notes in a major chord: +4,+7 and +12. I also simplified things a bit for both the scale and chord options by setting the main rhythm part an octave higher that the inverted rhythm part.

The next change to the sound output was to set the pan values for the main rhythm parts to 0.8 and for the inverted parts to -0.8 and then to flip them to the opposite side every 32 ticks.

In the original program as discussed I played both the main rhythm and the inverse rhythm together. This gave a very full texture, but made it difficult to hear the rhythms generated.  In this version I have added options to play just the normal or inverted rhythms or both together as previously. I also added options to select which ones of the four generated parts should play. All of the above may sound very confusing but you can see the end result in the two play commands inside each of the four live_loops x1 to x4

play n+12,amp: rvol+krv,attack: y*t,release: (1-y)*t,pan: 0.8*flip if (r.look == 1) and rhythm.include?"n"#main rhythm
play n,amp: lvol+klv,attack: y*t,release: (1-y)*t,pan: -0.8*flip if (r.look != 1) and rhythm.include?"i" #inverse rhythm

r holds the current rhythm eg (ring 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0)
The first note plays when a 1 is encoutered, the second when a 0 is found as look iterates through the ring. A second condition for the note to play is given by rhythm.include?”n” and rhythm.include?”i” If the variable rhythm includes an “n” then the main rhythm note is played and if it contains an “i” then the inverse rhythm note is played. Either or both can be selected.
The accents are given by krv and klv which are set non-zero on each 16th tick
flip is either 1 or -1 and is switched every 32 ticks reversing the pan values.
t {between 0 and 1} is set by by user and adjusts the timbre of the note by altering the attack/release proportions. rvol and lvol enable the user to adjust the relative amp settings for the right and left channels, bearing in mind that they cover different frequency ranges.
n is set to the current base note (plus chord offset) which alters every 64 when using the chord play option, otherwise, if using the scale based option, it is set to  note(base+bs.look) where bs holds the shuffled list of scale note offsets, which is added to the base note, or to note(base+bs.reverse.look) if it is to play the sequence backwards.

I now present sections of the program each with some comments. Unfortunately it is not possible to incorporate full comments in the program itself, or it exceeds the length limit to be able to play on a Mac. (It is a few character short of this limit!)

#Rotating binary rhythms by Robin Newman (v2), January 2016
#see rbnrpi.wordpress.com for details

use_debug false

define :bpm do
  return 60.0/Thread.current.thread_variable_get(:sonic_pi_spider_sleep_mul)
end

#function rotates ring entries
define :rv do |r,dir=1| #if dir = -1 then the rotation is backwards
  return ring( r[0].rotate(dir),r[1].rotate(dir),r[2].rotate(dir),r[3].rotate(dir))
end

loop do
  rs=Random.new_seed
  puts "random seed "+rs.to_s+"\n"
  use_random_seed(rs)

  sname=scale_names.choose
  ######### user settings below ###########
  rvol=0.4 #vol sets for left and right
  lvol=0.6
  kr=0.4 #accent inc right
  kl=0.4 #accent inc left
  numpasses=2 #should be even
  t=0.1
  tempochange=[0,10,16,20].choose
  plscale=[TRUE,TRUE,FALSE,TRUE].choose #TRUE scale based: FALSE chord notes
  entryexit=[TRUE,FALSE].choose #control entry/exit notes
  y=0.25 #adjust attack/release ratio
  startlevel=0.7
  loops=["1234","13","24","12","34"].choose#set loops to play x1...x4
  rhythm=["ni","ni","n","i"].choose  #n normal i=inverse
  ##############################################
  puts "USER DATA SETTINGS"
  puts "Number of 256 'tick' passes set to "+numpasses.to_s
  puts "playing major chords" if plscale==FALSE
  puts"playing notes from "+sname.to_s+" scale" if plscale==TRUE
  puts"playing loop numbers "+loops
  puts "playing rhythms "+rhythm+" (n normal, i inverse)"
  puts "entryexit note selected " if entryexit
  use_bpm 60

In the first section, a function bpm is defined. This uses an internal function in Sonic Pi to return the current bpm setting for the context when the function is called. I use this in calculating the current reduction in tempo as the program progresses.

The program utilises “selection rings” each of which contains 4 rings of 16 binary digits. A second function rv is defined which rotates the elements of each of these four rings either one place clockwise or anticlockwise depending on the dir variable.
Following this, the remainder of the program is placed inside a loop which repeats indefinitely (until you press the stop button), on each iteration playing a rotating binary rhythm set up with randomly selected parameters to control it.
Normally Sonic Pi generates a deterministic range of random numbers when it is running. This means that the same sequence of numbers is generated on each run, and that values returned by .choose or .shuffle will be repeatable. By changing the random number seed using the use_random_seed() function the sequence can be changed to a different one. To save manually changing this you can use the Ruby function Random.new_seed to generate an arbitrary value to place in the use_random_seed function. This means that a different setup will be generated EVERY time the loop starts. You could move the three lines after the loop do line outside this loop, and everything would work fine, but I have deliberately placed the lines inside the loop so that the (very long) seed number is printed for EVERY cycle played, and you can see the number required to recreate a particular rendition. But be warned, you will NOT be able to recreate a “nice” result without retaining the value of this very long integer. You can of course, just put in your own (known) value instead of using this, although in this case you should place it outside the loop, otherwise you will get exactly the same version on each iteration of the loop.
The scale_name is selected by sname=scale_names.choose and then there is a section where the user can preset various program settings. In this version choices are made for you for a list associated with each entry with a .choose command. Many of these settings have already been discussed: others are self evident. startlevel adjusts the initial level set in the with_fx :level command.
When the program is run, the values associated with many of these parameters are printed on the screen. If you want to make manual adjustments, you can remove the outside loop doend and set individual values for the parameters on each run.

bn=[ring(:c3,:f3,:g3,:g2 ),ring(:c3,:g3,:g2,:c3)].choose.shuffle #base notes
  puts"base notes "+bn.to_s
  bs=scale(0,sname).shuffle #get sequence of notes in scale shuffled
  #tempo changes
  fac1= (1-tempochange.to_f/100)**(1.0/(numpasses*4/2)) #calculate bmp_mul factor for slow down
  #puts fac1
  puts "tempo reduction set to "+((1-fac1**(numpasses*4/2))*100).round.to_s+"%" #check
  fac5=fac4=fac3=fac2=fac1 #set individual starting fac values for each loop

  #start with 4 16 bit rhythm patterns based on the binary numbers 0-15
  #set up binary rhythms r1 0-3,r2 4-7,r3 8-11,r4 12-15
  br=["0000000100100011","0100010101100111","0100010101100111","1100110111101111"].shuffle
  r1=[br[0],br[0].reverse].choose
  r2=[br[1],br[1].reverse].choose
  r3=[br[2],br[2].reverse].choose
  r4=[br[3],br[3].reverse].choose
  puts "Starting Rhythms "+r1+" "+r2+" "+r3+" "+r4 #currently selected rhythms

  #now convert to rings
  r1= r1.split('').map(&:to_i).ring
  r2= r2.split('').map(&:to_i).ring
  r3= r3.split('').map(&:to_i).ring
  r4= r4.split('').map(&:to_i).ring

  #make 4 selection rings
  rl1=ring(r1,r2,r3,r4)
  rl2=rl1.rotate
  rl3=rl2.rotate
  rl4=rl3.rotate

In the second section of the program, first the bass notes are setup in the ring bn and shuffled, and the shuffled note offsets for the chosen scale are setup in the ring bs.
fac1, the factor to use with each application of the use_bpm_mul command is calculated using the formula
fac1= (1-tempochange.to_f/100)**(1.0/(numpasses*4/2)) Basically with a tempo change every 64 ticks and a complete pass of the rhythm changes every 256 ticks there 4 changes each complete pass. If the program runs for numpasses there are (numpasses*4/2) steps to reduce the tempo by the required percentage, HALF the duration of the piece. (1-tempochange.to_f/100) is the complete tempo change required e.g. 0.8 for 20%  **(1.0/number of stages) works out the number of stages root of this number which gives the required factor. eg for 20% reduction with numpasses=4 we get 1/8th root of 0.8 = 0.9724924724660731 If we multiply this number by itself 8 times we get 0.8 which is the required factor. fac1 to fac5 are all set to this value to give one separate variable for each of the live_loops that need it.
br holds the initial 4 binary strings for number 0-3,4-7,8-11,12-15 which are initially shuffled in their order. The shuffled order is then split into four strings r1,r2,r3 and r4 and these are randomly reversed in their order. eg
r1=[br[0],br[0].reverse].choose
Then four selection rings rl1 to rl4 are created, using the four elements r1..r4 arranged in different orders r1r2r3r4, r2r3r4r1, r3r4r1r2 and r4r1r2r3
Later in the program they will be rotated using the rv function previously described.

  if entryexit then #optional fm "wind up" note to start
    use_synth :fm
    p=play 36,sustain: 5,divisor: 12,amp: 0.3
    control p,note_slide: 3,note: 56,amp_slide: 4,amp: 1
    sleep 3
    control p,amp_slide: 2,amp:0
  end

The fourth section generates the starting note (if selected by the value of entryexit.
It uses the fm synth, and starts a low note playing sustained for 5 beats with a starting pitch 36. As this note plays, its pitch is controlled and raised over 3 seconds up to 56 whilst at the same time the amp: is increased to 1, and then faded out to 0.

  with_fx :level do |v| #used to fade out rhythms at the end
    control v,amp: startlevel #set initial level
    with_fx :reverb do
      live_loop :audio do
        tick
        if look >= (numpasses-1)*256+128 #start fading during last pass
        then
          limit=1 #set level endpoint
          limit=0.7 if entryexit #leave 30% if exit note
          control v,amp: startlevel*( 1.0-limit*(look-128-(numpasses-1)*256).to_f/128) #fade to 30% or 0%
        end
        fac5=1.0/fac5 if look==numpasses*256/2 #change fac to speed up at half way point
        use_bpm_mul fac5 if (32-look%64)==0
        sleep t
        stop if look==numpasses*256 #stop after numpasses completed
      end

The main execution section starts with the fifth section. First with_fx wrappers for :level and :reverb are set up. The level setting control is v, which is used to set the starting value to startlevel. Thereafter it is controlled by the live_loop :audio
This live_loop uses its tick counter to wait until the program is halfway through the final complete loop pass when it triggers a reduction in the level setting either to zero or to 30% depending upon whether the exit note is to be played or not as selected in the user settings. The bpm setting for this loop is adjusted according to the tempochange selected by the user, so that the timings remain synchronised to the four main live_loops x1 to x4 which generate the notes played. The factor fac5 is applied every 64 ticks after the first 32 to accomplish this. Halfway through fac5 is changed to 1.0/fac5 so that the tempo starts increasing again. A stop command kills the loop when look equals the number of passes selected *256, 256 being the number of ticks in one rhythmic cycle.

      live_loop :x1 do
        use_synth :blade
        tick
        tick_set :rc1,look/256 #used to select current ring from selection ring
        tick_set :bs1,look/64 #used to select base note pitch
        tick_set :flipper1,look/32 #used to flip pan settings

        base=bn.look(:bs1) #get base note
        r=rl1.look(:rc1) #get current ring

        if look%16==0 then #set emphasis for first beat of 16 (added to amp: setting)
          krv=kr;klv=kl
        else
          krv=klv=0
        end

        if look(:flipper1)%2==0 then #set pan flip
          flip=1
        else
          flip=-1
        end

        fac1=1.0/fac1 if look==numpasses*256/2 #change fac to speed up at half way point

        use_bpm_mul fac1 if (32-look%64)==0#apply bpm_mul every 64 beats
        puts "initial tempo=100%" if look == 0
        puts "tempo now reduced to "+(((bpm.to_f/60*10000).round).to_f/100).to_s+"% will increase back to 100%" if look == numpasses/2*256 - 32 and (tempochange !=0)
        if plscale then #select note according to whether scale or chord selected
          n=note(base+bs.look)
        else
          n=base
        end
        if loops.include?"1" then
          play n+12,amp: rvol+krv,attack: y*t,release: (1-y)*t,pan: 0.8*flip if (r.look == 1) and rhythm.include?"n"#main rhythm
          play n,amp: lvol+klv,attack: y*t,release: (1-y)*t,pan: -0.8*flip if (r.look != 1) and rhythm.include?"i" #inverse rhythm
        end
        sleep t

        rl1=rv(rl1) if look%16==0 #rotate all the rings in the selection ring

        if look(:rc1)==numpasses then
          cue :go
          stop
        end
        puts "Current pass "+(look(:rc1)+1).to_s if look%256==0 #print current cycle on the screen
      end

The four live_loops x1 to x4 are essentially the same, with minor differences. Each one uses a different synth. I will describe live_loop :x1. To avoid accidental “double ticking” ONE tick call is used in the loop in the third line. Three other named ticks are started.
:rc1 is set to increment every 256 ticks and is used to select the current ring from the selection ring.
:bs1 is set to increment every 64 ticks and is used to select the bass note pitch.
:flipper is set to increment every 32 ticks and is used to flip the pan settings.
The comments in the live_loop together with the previous description of other parts of the program should make the remaining operation of the loop reasonably clear.
Unlike the other three x2..x4 loops this loop also has a couple of puts statements to put information about the current tempo setting and the current pass number on the screen.
The application of the rv rotate function alters the rhythmic pattern as the loop plays.
Live_loop :x4 has one important addition. It includes the line:

      cue :fin if look==(numpasses*256)-20 #trigger finish loop

This is used to trigger final “wind-down” fm note if it has been enabled according to the user flag entryexit. This trigger occurs 20 ticks or 2 seconds at full tempo before the the live_loops :x1 rto :x4 terminate.
live_loop :x1 also contains an extra command to send a cue :go to allow the loop doend to complete when live_loop :x1 terminates in the case that the entryexit  is NOT to be generated. Otherwise the loop exit follows when the wind-down note has completed.

   end #reverb

    #end piece.....fm "wind down" finishes.
    if entryexit then
      sync :fin
      use_synth :fm
      p=play 56,attack: 0.1,sustain_level: 0.9,sustain: 6,divisor: 12,amp: 1
      sleep 1 #sustain for 1 sec before changing
      control p,note_slide: 6,note: 36
      sleep 3
      puts "finished!"
      sleep 1
    end
    sync :go if !entryexit #wait for sync from loop x1
    sleep 1
  end#end level
end#loop

The final section first ends the :reverb fx after the end of live_loop :x4, then triggers the exit note assuming entryexit is TRUE when it receives its sync :fin Again this is produced using the fm synth. This starts with the pitch with which the entry note finished 56 and uses a control function to reduce the pitch to 36 over 6 beats, producing a winding down sound to finish the piece.

For me this turned into a large project, with much experimentation and things to try out. As I said at the beginning be warned. Beware where Sonic Pi can lead you. This is why I think it is such a great program. it lets you explore both musical, mathematical and programming ideas all at the same time. It gives -usually :-)  immediate aural feedback to your ideas, is very powerful and flexible and basically great fun to use. Sadly it does use up an inordinate amount of time…but it is all worth it!

The program has been tested on Sonic Pi v2.9 on both MAcOSX, Win PC and Raspbian Jessie running on a Pi2. It is unlikely to perform very well on a Model B+ or earlier. I haven’t tested it on Ubuntu, but I see no reason why it wont; work on that platform too.

You can download the complete version 2 program from my gist here

There is a soundcloud item containing two audio specimens from this new version here

3 thoughts on “Sonic Pi: beware where it can lead you!

  1. Small typo:
    “thus E3 was 11010011 and A7 was 11100111.”
    Should be:
    “thus E3 was 11100011 and A7 was 10100111.”

    Thanks for you sonic-pi articles. Please keep up the good work.

  2. “By now I had become quite hooked on experimenting with the system, and lots of questions along the line of “I wonder what would happen if?…” sprang to mind.”

    Sounds like this is a shared experience we have, as we dig deeper into SPi. Once you get hooked and start thinking in SPi-dialect Ruby, it becomes your musical “Golden Hammer”.

    Even Sam Aaron described something similar, in his Pi Podcast interview (ca. 33:27).

    Might have to do with the affordances of a system meant for learning. Pedagogues since Vygotsky talk about “scaffolding” for something similar.

Leave a reply to Reed Bement Cancel reply