Ski Sunday theme on Sonic Pi

10559-a-woman-snow-skiing-pv

Now that version 2.10 is released for Sonic Pi, I thought I would choose a suitable piece to exploit some of the new features within it, especially the improved methods of loading samples. In a recent post I  described how I had added access to the sample voices contained in the Sonatina Symphonic Orchestra (hereafter SSO) utilising the features in the (then) 2.10dev version. I have always enjoyed the theme music for Ski Sunday which is a piece called “Pop Looks Bach” by the late  Sam Fonteyn. It a a great highly rhythmical piece, with sections which are repeated and is very suitable for coding into Sonic Pi. I wanted to produce something close to the original recording This involved downloading the audio file and playing it in the program Audacity, where I could reduce the tempo, to hear the note structure more easily, and also apply a low pass filter to try and extract the guitar bass part. In fact it took many hours of listening again and again to various sections to work out the notes for the various parts. I do not claim that what I have produced is an exact replica, but it certainly does justice to the flavour of the piece and sounds pleasing.

I knew that because of the size limitations on the contents of a Sonic Pi buffer that the piece would require at least two buffers, as the code to download and process the samples of the SSO so that they can be utilised by Sonic Pi is quite large, and occupies a good part of one buffer. In fact initially I used three buffers, but by careful analysis of the notes, and extracting repeated sections so that the were only coded once, I was able to fit the entire playing portion of the code into one buffer, giving a total of two overall.

The first buffer contains code to load the 9 voices which are utilised from the SSO. The code is based on that in my previous article here. Also included in this first buffer are various functions, used in playing the piece, as these can be shared with and utilised by the second buffer. Once the first buffer has been run, it can be ignored, and you can run the second buffer as many times as you like independently. However also note that subsequent runs of the first buffer detect if the samples have already been loaded and thus are much quicker. Two lines at the start of the first buffer can be uncommented to clear all samples, which can be useful for testing purposes. The only adjustments required is to ensure that the path to the location of SSO on your system is correct at the start of each buffer, and also to uncomment line 4 in the second buffer to set a sched_ahead_time! of 4 if running on a Pi2 or Pi3. The programs are not suitable for use on earlier Pi models.

Musically, I have used the following parts:

A violin part which plays the main tune through using notes held in nva, nvb and nvcoda

An accompanying harmony part using notes held in hna and hnb which is played by the supersaw synth

Brass chords, played on trumpets and trombones. There are four such sections. One in the intro (bn) accompanied by timpany (nt)  bars 1-5, which is repeated as bn1 several times in the piece (bars 14-17,48-51 and 82-85). One before the middle wind section (cn2) (bars 26-28, 60-62) and one at the end of the central wind section (njabsus and njabst) bars 36-38 and 70-72 , played by trumpets sustained and staccato. Lastly the final sequence of chords (nfinals) (bars 94-100) played on Trumpets, accompanied by horns (hn) and flutes (nfcoda)

A plucked guitar bass played throughout using the :plck synth. This is coded in two sepearate threads, one for bars 1-39 and the other in two sections for bars 40-73 and 74-100

A central wind section played on clarinets and flutes (nf+nf2+nf ) bars 30-35 and 64-69

Rhythm is provided by a one bar riff played on the drum_cymbal_closed sample (live_loop :cm) together with a timpani motive (nt) occasional snare drumrools (dr) (bars 36,37,70 and 71) and a falling drum pattern (drumfall) played with a tuned :tabal_na_0 sample (bars 29,39,63 and 73)

I hope that if you are so included the above taken together with some comments in the program will let you understand how the program works, although it will take quite a bit of study to follow it fully.

The two programs skiSunday-p1.rb (the loader buffer) and skiSunday-p2.rb (the player buffer) are listed below.

#program loads samples and functions required by SkiSunday programs
#written by Robin Newman, March 2016
#require Sonatina Sympohonic Orchestra installed on Desktop
# http://sso.mattiaswestlund.net/download.html

##| sample_free_all  #can uncomment these lines to clear all samples
##| stop

use_debug false
#path to library samples folder (including trailing /)
path="~/Desktop/Sonatina Symphonic Orchestra/Samples/"  #adjust as necessary

#llist=[0,1,2,3,4,5,6,7,8] #voicenumbers used
#create array of instrument details
voices=[["1st Violins sus","1st Violins","1st-violins-sus-",1,:g3,:b6],\
        ["Clarinets","Clarinets","clarinets-sus-",2,:d3,:d6],\
        ["Flutes sus","Flutes","flutes-sus-",0,:c3,:bb5],\
        ["Trombones sus","Trombones","trombones-sus-",1,:ds2,:e5],\
        ["Trumpet","Trumpet","trumpet-",1,:e3,:f6],\
        ["Trumpets sus","Trumpets","trumpets-sus-",1,:e3,:f6],\
        ["Trumpets stc","Trumpets","trumpets-stc-rr1-",1,:e3,:f6],\
        ["Horns stc","Horns","horns-stc-rr1-",1,:e2,:e5],\
        ["Timpani f lh","Percussion","timpani-f-lh-",0,:c1,:c2]]

uncomment do #can comment if samples loaded, to allow quick redefine of functions
  define :load do |i|
    trigger=0
    live_loop :t do
      sleep 0.3
      if trigger== 1
        cue :start
        stop
      end
    end
    load_samples path+voices[i][1],voices[i][2]
    trigger=1
    sync :start
  end

  for i in (0..8) do
      load(i)
    end
    sleep 2
  end

  puts "The following voices from Sonatina Symphonic Library can be used:-"
  voices.each_with_index do |n,i|
    puts i.to_s,n[0]
  end

  puts voices.length.to_s+" voices"
  #setup global variables
  sampledir=""
  sampleprefix=""
  offsetclass=""
  low=""
  high=""
  paths=""

  #setup data for current inst
  define :setup do |inst,path|
    sampledir=voices.assoc(inst)[1]
    sampleprefix=voices.assoc(inst)[2]
    offsetclass=voices.assoc(inst)[3]
    low=voices.assoc(inst)[4]
    high=voices.assoc(inst)[5]
    #amend path for instrument sampledir
    paths=path+sampledir+"/"
  end

  sleep 0.2

  #define routine to play sample using Sonatina data
  define :pl do |np,d,inst,vol=1,s=0.9,r=0.1,tp=0,pan=0|
    setup(inst,path)
    #check if note in range of supplied samples
    #use lowest/highest sample for out of range
    change=0 #used to give rpitch for coverage outside range
    frac=0
    n=np+tp #note allowing for transposition
    if n.is_a?(Numeric) #allow frac tp or np
      frac=n-n.to_i
      n=n.to_i
    end
    if note(np)+tp<note(low) #calc adjustment for low note       change=note(np).to_i+tp-note(low)       n=note(low)     end     if note(np).to_i+tp > note(high) #calc adjustment for high note
      change = note(np).to_i+tp-note(high)
      n=note(high)
    end
    if change < -5 or change > 5 #set allowable out of range
      #if outside print messsage
      puts 'inst: '+inst+' note '+np.to_s+' with transpostion '+tp.to_s+' out of sample range'
    else #otherwise calc and play it
      #calculate base note and octave
      base=note(n)%12
      oc = note(n) #do in 2 stages because of alignment bug
      oc=oc/12 -1
      #find first part of sample note
      slookup=['c','c#','d','d#','e','f','f#','g','g#','a','a#','b']
      #lookup sample to use,and rpitch offset, according to offsetclass
      case offsetclass
      when 0
        oc += 1 if base == 11 #adjust if sample needs next octave
        snumber=[0,0,3,3,3,6,6,6,9,9,9,0]
        offset=[ 0,1,-1,0,1,-1,0,1,-1,0,1,-1]
      when 1
        snumber=[1,1,1,4,4,4,7,7,7,10,10,10]
        offset=[-1,0,1,-1,0,1,-1,0,1,-1,0,1]
      when 2
        oc -= 1 if base == 0 #adjust if sample needs previous octave
        snumber=[11,2,2,2,5,5,5,8,8,8,11,11]
        offset=[1,-1,0,1,-1,0,1,-1,0,1,-1,0]
      when 3
        snumber=[0,1,2,3,4,5,6,7,8,9,10,11] #this class has sample for every note
        offset=[0,0,0,0,0,0,0,0,0,0,0,0]
      end
      #generate sample name
      sname=sampleprefix+(slookup[snumber[base]]).to_s+oc.to_s
      #play sample with appropriate rpitch value
      sample paths,sname,rpitch: offset[base]+change+frac,sustain: s*d,release: r*d,pan: pan,amp: vol
    end
  end

  #define function to play lists of linked samples/durations using Sonatina samples
  define :plarray do |notes,durations,offsetclass,vol=1,s=0.9,r=0.1,tp=0,pan=0|
    #puts offsetclass
    notes.zip(durations).each do |n,d|
      if n.respond_to?(:each)
        n.each do |nv|
          pl(nv,d,offsetclass,vol,s,r,tp,pan) if ![nil,:r,:rest].include? nv#allow for rests
        end
      else
        pl(n,d,offsetclass,vol,s,r,tp,pan) if ![nil,:r,:rest].include? n#allow for rests
      end
      sleep d
    end
  end

  define :plnarray do |n,d,v=1,shift=0| #used to play harmony supersaw part
    n.zip(d).each do |n,d|
      play n+shift,sustain: 0.9*d,release: 0.1*d,amp: v
      sleep d
    end
  end
  define :pluckarray do |n,d,v=1,shift=0| #used to play plucked bass part
    n.zip(d).each do |n,d|
      play n+shift,release: 1.5*d,amp: v
      sleep d
    end
  end
  define :dl do |d| #used for debugging to determine durations of duration lists
    t=0
    d.each do |d|
      t +=d
    end
    return t
  end

  define :dr do |d,v| #drum roll
    sample :drum_roll,sustain: d*0.9,release: d*0.1,amp: v
    sleep d
    sample :drum_snare_hard,rate: 1.25,amp: v
  end

  define :drfall do |n,d,v| #falling tabla drum motif and drumroll
    sample :tabla_na_o,rpitch: n-:ds4,sustain:0,release: 2*d,amp: v
    sleep d
    sample :tabla_na_o,rpitch: n-:ds4,sustain:0,release: 2*d,amp: v
    sleep d
    sample :tabla_na_o,rpitch: n-:ds4,sustain:0,release: 2*d,amp: v
    sleep d
    sample :tabla_na_o,rpitch: n-5-note(:ds4),sustain:0,release: 2*d,amp: v
    sleep d
    sample :tabla_na_o,rpitch: n-5-note(:ds4),sustain:0,release: 2*d,amp: v
    sleep d
    sample :tabla_na_o,rpitch: n-5-note(:ds4),sustain:0,release: 2*d,amp: v
    sleep d
    sample :tabla_na_o,rpitch: n-12-note(:ds4),sustain:0,release: 2*d,amp: v
  end

  define :riff do |n1,n2| #these riffs used in plucked base part
    return [n1,:r,n1,n1,:r,n2,:r]
  end
  define :riff2 do |n1,n2,n3|
    return [n1,:r,n2,n3,:r,n2,:r]
  end
  define :riff3 do |n1,n2,n3|
    return [n1,:r,n1,n2,:r,n3,:r]
  end

  load_sample :tabla_na_o #used in drfall
  load_sample :drum_cymbal_closed #used in rhythm live_loop :cm
  load_sample :drum_roll
  load_sample :drum_snare_hard

 

#Ski Sunday arranged for SP by Robin Newman April 2016
#Run the sample loader program first, then this program.

#set_sched_ahead_time! 4  #uncomment for Pi2 or Pi3
path="~/Desktop/Sonatina Symphonic Orchestra/Samples/" #adjust as necessary
voices=[["1st Violins sus","1st Violins","1st-violins-sus-",1,:g3,:b6],\
        ["Clarinets","Clarinets","clarinets-sus-",2,:d3,:d6],\
        ["Flutes sus","Flutes","flutes-sus-",0,:c3,:bb5],\
        ["Trombones sus","Trombones","trombones-sus-",1,:ds2,:e5],\
        ["Trumpet","Trumpet","trumpet-",1,:e3,:f6],\
        ["Trumpets sus","Trumpets","trumpets-sus-",1,:e3,:f6],\
        ["Trumpets stc","Trumpets","trumpets-stc-rr1-",1,:e3,:f6],\
        ["Horns stc","Horns","horns-stc-rr1-",1,:e2,:e5],\
        ["Timpani f lh","Percussion","timpani-f-lh-",0,:c1,:c2]]
#######
q=0.15 #set note duration of quaver
qd=3*q/2 #dotted quaver
sq=q/2 #semiquaver
c=2*q #crotchet
cd=3*q #dotted crotchet
m=2*c #minim

sc= :drum_cymbal_closed #used for drum part
use_synth :supersaw #used for accompanying part

d=(ring q,q,q,q,c,q,q) #drum rhythm
nt=[:a1,:a1,:d2,:r,:a1,:a1,:d2,:r,:a1,:a1,:d2] #timpani part
dt=[q,q,m,5*c,q,q,m,5*c,q,q,m]
#nv.nva,nvb violin parts
nv=[:r,:a5,:g5,:a5,  :fs5,:a5,:e5,:a5,  :d5,:a5,:cs5,:a5,  :d5,:a5,:e5,:a5,  :fs5,:a4,:d5,:cs5,  :b4,:d5,:a4,:d5,  :g4,:d5,:fs4,:d5,  :g4,:d5,:a4,:d5,  :b4,:d5,:g5,:fs5,  :e5,:g5,:d5,:g5,  :cs5,:g5,:b4,:g5,]
nva=[:cs5,:g5,:d5,:g5,  :e5,:a4,:a5,:g5,  :fs5,:e5,:fs5,:g5,  :a5,:fs5,:e5,:d5,  :e5,:a5,:fs5,:a5,  :g5,:e5,:cs5,:a4]
nvb=[:cs5,:g5,:d5,:g5,  :e5,:a4,:a5,:g5,  :fs5,:e5,:fs5,:gs5, :a5,:e5,:cs5,:e5,  :d5,:cs5,:b4,:cs5, :d5,:e5,:fs5,:gs5,:a5]
dva=[m+q]+[q]*(((nv+nva).length)-1)
dvb=dva+[q]
#hna,hnb harmony part played by supersaw synth
hn=[:r,:d5,:cs5,:d5,:e5,:fs5,:d5, :g5,:fs5,:e5,:d5,:e5,:fs5,:g5,:b5, :cs6,:b5,:a5,:g5,:a5,:b5]
hn2=[:cs6,:a5,:g5,:e5,:d5,:cs5,:d5,:e5,:fs5,:a5,:b5,:g5,:a5,:a4]
hn3=[:cs6,:a5,:g5,:e5,:d5,:cs5,:d5,:b4,:cs5,:a4,:e5,:fs5,:e5,:d5,:fs5,:gs5,:fs5,:gs5,:b5,:a5]
hna=hn+hn2
hnb=hn+hn3
hda=[6*c]+[c]*20 +[q]*12+[m,m]
hdb=[6*c]+[c]*20 +[q]*8+[c]+[q]*10+[m]
#cn,bn1,bn,cn2 with dur. dn1,cd2,dn brass chords
cn=[[:fs4,:a4,:d5],[:fs4,:a4,:d5],[:g4,:a4,:d5],[:g4,:a4,:d5],[:g4,:a4,:d5],[:fs4,:a4,:d5],[:fs4,:a4,:d5],[:fs4,:a4,:d5],[:g4,:a4,:d5]]
bn1=[:r]+cn
bn=bn1 * 2
dn1=[c,q,q,c,q,q,c,q,q,c,c]
cn2=[:r,[:a5,:c6],:r,[:d5,:g5,:b5],[:cs5,:e5,:a5]]
cd2=[q,q,c,cd,q]
dn=[2*c,q,q,c,q,q,c,q,q,c,m,q,q,c,q,q,c,q,q,q]
#nf,nf2 flute part middle section with dur df,df2
nf=[:r,:d5,:f5,:g5,:f5,:ab5,:f5,:g5,:f5,:d5,:f5,:g5]
df=[c,q,q,c,q,c,q,c,q,q,q,q]
nf2=[:r,:d5,:a4,:c5,:d5,:f5,:d5,:f5,:d5]
df2=[c,q,q,c,q,c,q,c,m]
#njab sustained and stac brass chords at end of middle section with dur djab
njabsus=[:r,:r,[:g4,:b4,:d5,:g6],:r,:r,[:g4,:b4,:d5,:g6],:r,   :r,[:f4,:a4,:c5,:f5],[:g4,:b4,:d5,:g5],:r,:r]
djab=[q,m,cd,q,m,cd,q,q,c,c,q,q]
njabst=[[:a4,:e5,:a6],:r,:r,[:a4,:e5,:a6],:r,:r,[:a4,:e5,:a6], :r,:r,:r,[:gs4,:c5,:ds5,:gs5],[:a4,:cs5,:e5,:a6]]
#last play-through and coda violin part ncoda, with dur dcoda
ncoda=nv+[:cs5,:g5,:d5,:g5,  :e5,:a4,:a5,:g5, :fs5,:e5,:fs5,:g5, :a5,:fs5,:e5,:d5, :e5,:fs5,:g5,:fs5, :g5,:a5,:b5,:cs6, :d6,:d5,:fs5,:a5, :g5,:e5,:g5,:a5]
ncoda.concat [:fs5,:d5,:fs5,:a5, :g5,:e5,:g5,:a5]*3+[:d6,:cs6,:b5,:a5, :b5,:a5,:b5,:cs6, :d6,:cs6,:b5,:a5, :b5,:a5,:b5,:cs6,[:fs5,:a5,:d6]]
dcoda=[m+q] +[q]*nv.length+[q]*71+[c]
#flute part final 7 bars nfcoda with dur dfcoda
nfcoda=([:d6]*12+[:d6,:cs6,:b5,:a5])*2+[:d6,:cs6,:b5,:a5, :b5,:a5,:b5,:cs6, :d6,:cs6,:b5,:a5, :b5,:a5,:b5,:cs6,[:fs5,:a5,:d6]]
dfcoda=[q]*48+[c]
#riffs used in plucked bass part (see functions in loader prog)
riffd=[c,q,q,q,q,q,q]
bsnbase=riff(:d2,:a1)*6+riff(:g1,:d2)+riff(:g2,:b1)+riff(:cs2,:e2)+riff2(:a2,:e2,:cs2)
bsna=bsnbase+[:d2,:cs2,:b1,:b1,:e1,:e1,:a2,:a2]
bsnb=bsnbase+[:d2,:b1,:cs2,:a2,:b1,:b1,:e2,:e2]
bsnc=bsnbase+[:d2,:d2,:b1,:b1,:e2,:e2,:a2,:a2]+riff(:d3,:a2)*6+[:d2]
bsd=riffd*10+[cd,q,cd,q,cd,q,c,c]
bsn2=riff(:a1,:e2)+riff(:a2,:e2)+[:a1,:c2,:r,:b1,:a1]
bsd2=riffd*2+[q,q,c,cd,q]
bsn3= [:r,:cs1,:e1,:a1,:e1]+riff3(:g1,:b1,:d2)*2+riff3(:d2,:f2,:a2)*2+riff3(:g1,:b1,:d2)*2+[:a1,:r,:g1,:a1,:r,:g1,:a1,:r,:f1,:g1,:gs1,:a1]
bsd3= [c*2,q,q,q,q]+riffd*6+[q,m,cd,q,m,cd,q,q,c,c,q,q]
#flute chords in last few bars nfc, nfinals with dur dfinals
nfc=[[:d4,:fs4,:a4],[:d4,:fs4,:a4],[:d4,:g4,:a4]]
nfinals=([:r]+nfc)*6+[[:d4,:a4,:d5]]
dfinals=[c,q,q,m]*6+[c]
#hn horn part final few bars hn with dur hd
hn=[:r,:fs4,:fs4,:g4,:fs4]*6
hd=[c,q,q,m]+[q,q,q,q,m]*5 + [c]

################ plarray and plnarray defined in loader prog
define :sec1 do
  in_thread do
    plarray(nv+nva,dva,"1st Violins sus",2)
  end
  in_thread do
    plnarray(hna,hda,0.22,-12)
  end
  sleep 35*c
  in_thread do
    plarray(bn,dn,'Trombones sus')
  end
  in_thread do
    plarray(bn,dn,'Trumpets sus')
  end
  in_thread do
    plarray(nt,dt,"Timpani f lh")
  end
  sleep 13*c
end
define :sec2 do #bars 17-39, 51-73
  in_thread do
    plarray(nv+nvb,dvb,"1st Violins sus",2)
  end
  in_thread do
    plnarray(hnb,hdb,0.22,-12)
  end
  sleep 36*c
  in_thread do
    plarray(bn1,dn1,'Trombones sus',0.8,0.90,0.1,7) #transposed up 7
  end
  in_thread do
    plarray(bn1,dn1,'Trumpets sus',0.8,0.90,0.1,7)
  end
  sleep 8*c
  in_thread do
    plarray(cn2,cd2,'Trumpet',1.2)
  end
  in_thread do
    plarray(cn2,cd2,'Trumpets sus',1.2)
  end
  plarray(cn2,cd2,'Trombones sus',1.2,0.90,0.1,-12) #transposed down 12
  in_thread do
    drfall(:a2,sq,0.5) #drum fall (see loader prog)
  end
  sleep 4*c

  in_thread do
    plarray(nf+nf2+nf,df+df2+df,"Clarinets")
  end
  plarray(nf+nf2+nf,df+df2+df,"Flutes sus")
  in_thread do # drum roll acccompaniment next 2 bars
    2.times do
      sleep c
      dr(c,0.6) #drumroll
      sleep m
    end
  end
  in_thread do
    plarray(njabsus,djab,'Trumpets sus',1)
  end
  plarray(njabst,djab,'Trumpets stc',1,1.2,0.5)
  in_thread do
    drfall(:a2,sq,0.5) #drum fall
  end
end

###### start playing here ############
with_fx :level,amp: 3 do
  with_fx :reverb,room: 0.8 do
    live_loop :barnumbers do #print bar number info
      puts "Bar "+(tick + 1).to_s
      sleep 4*c
      if look== 99
        stop
      end
    end
    sleep 3*c #1st three beats bar 1
    live_loop :cm,delay: c do #closed cymbal riff starts bar 2
      sample sc,beat_stretch: c,amp: 0.3
      sleep d.tick
      if look==7*98 #notes in riff * bars
        stop
      end
    end
    #intro starts 4th beat bar 1
    in_thread do
      plarray(bn,dn,'Trombones sus')
    end
    in_thread do
      plarray(bn,dn,'Trumpets sus')
    end
    in_thread do
      plarray(nt,dt,"Timpani f lh")
    end
    in_thread do #start plucked bass
      with_synth :pluck do #bass pluck bars 1-39
        sleep c
        pluckarray(bsna+bsnb+bsn2+bsn3,bsd*2+bsd2+bsd3,0.3)
      end
    end
    sleep 13*c #time for intro to complete
    sec1 #bars 6-16
    sec2 #bars 17-39

    in_thread do
      with_synth :pluck do
        #bars 40-73 start bsna 4 bars in(after 28 elements):ignores intro
        pluckarray([:r]+bsna[28..-1]+bsnb+bsn2+bsn3,[c*4]+bsd[28..-1]+bsd+bsd2+bsd3,0.3)
        #bars 74-100 again ingore intro in bsna
        pluckarray([:r]+bsna[28..-1]+bsnc,[c*4]+bsd[28..-1]+bsd+riffd*6+[c],0.3)
      end
    end
    sec1 #bars 40-50
    sec2 #defined in part 1 bars 51-73
    sec1 #bars 74-84
    in_thread do
      plarray(ncoda,dcoda,"1st Violins sus",2) #bars 85-100
    end
    sleep 36*c #delay to bar 94
    in_thread do
      plarray(nfinals,dfinals,'Trumpets sus',1.2) #bars 94-100
    end
    in_thread do
      plarray(hn,hd,'Horns stc',0.7) #bars 94-100
    end
    in_thread do
      plarray(nfcoda,dfcoda,"Flutes sus") #bars 94-100
    end
  end
end

You can download the programs here

You can download the sso here  unzip it and place in a known location. Adjust program path to suit.

You can hear the piece played by Sonic Pi 10 here

A completely different way to use Sonic Pi with a midi controller

Recently I cam across a tweet from Phil Helliwell @kill9zombie in which he gave a link to a gist where he had described controlling Sonic Pi by means of an external midi controller. https://gist.github.com/kill9zombie/8b24389239891a5dbe3e This intrigued me, and I decided to investigate further. It turned out that it was very easy to set up, and I thought it would be useful to write up the procedure and give details of a simple program I have used to control Sonic Pi using 6 rotary potentiometers on my M-Audio Oxygen8 keyboard. The program should work with minor modification with other midi controllers, indeed Phil uses the program with an Akai lpd8 controller.

install prerequisites:

on both Mac and Pi

gem install micromidi

on a Pi

sudo apt-get update
sudo apt-get install libasound2-dev

The Midi Control system uses two ruby programs. A server, which you run from a terminal window which received midi input from your keyboard/controller and then sends processes and sends it on to a small client program which is incorporated in the program you are running in your Sonic Pi buffer.
The server program requires you to load in the gem micromidi which it uses to communicate with your keyboard/controller. The server also uses Ruby’s built in drb module. This DRuby distributed object system for Ruby, lets you control objects in one ruby program from another one. This excert form the DRuby documentation explains how it works.

dRuby allows methods to be called in one Ruby process upon a Ruby object located in another Ruby process, even on another machine. References to objects can be passed between processes. Method arguments and return values are dumped and loaded in marshalled format. All of this is done transparently to both the caller of the remote method and the object that it is called upon.

An object in a remote process is locally represented by a DRb::DRbObjectinstance. This acts as a sort of proxy for the remote object. Methods called upon this DRbObject instance are forwarded to its remote object. This is arranged dynamically at run time. There are no statically declared interfaces for remote objects, such as CORBA’s IDL.

dRuby calls made into a process are handled by a DRb::DRbServer instance within that process. This reconstitutes the method call, invokes it upon the specified local object, and returns the value to the remote caller. Any object can receive calls over dRuby. There is no need to implement a special interface, or mixin special functionality. Nor, in the general case, does an object need to explicitly register itself with a DRbServer in order to receive dRuby calls.

One process wishing to make dRuby calls upon another process must somehow obtain an initial reference to an object in the remote process by some means other than as the return value of a remote method call, as there is initially no remote object reference it can invoke a method upon. This is done by attaching to the server by URI. Each DRbServer binds itself to a URI such as ‘druby://example.com:8787’. A DRbServer can have an object attached to it that acts as the server’s front object. A DRbObject can be explicitly created from the server’s URI. This DRbObject’s remote object will be the server’s front object. This front object can then return references to other Ruby objects in the DRbServer’s process.

The server program is shown below. I have added some extra def k<n> functions to accommodate the numbering on my Oxygen-8 controller.

#!/usr/bin/env ruby
# encoding: utf-8

require 'drb/drb'
require 'micromidi'

# The URI we're using for RDb, we use unix sockets just because they're quicker.
DRB_URI="drbunix:/var/tmp/sonic-pi-midiconnector"

class SPIMidiConnector

  def initialize
    @input = UniMIDI::Input.use(0)
    @midithread = listen()
  end

  # Return the value of the pot.
  #
  # If we haven't moved a pot yet then just return 0.
  def get(key)
    @midithread[key] || 0
  end

  # Some convenience methods (they're shorter to type while livecoding).
  # These should return the midi value (0 to 127) of the potentiometer on the LPD8.
  def k1; get(:k1); end
  def k2; get(:k2); end
  def k3; get(:k3); end
  def k4; get(:k4); end
  def k5; get(:k5); end
  def k6; get(:k6); end
  def k7; get(:k7); end
  def k8; get(:k8); end
  def k9; get(:k9); end
  def k10; get(:k10); end
  def k25; get(:k25); end
  def k71; get(:k71); end
  def k91; get(:k91); end
 
  def thread
    @midithread
  end

private

  def listen
    midithread = Thread.new(@pads_map) do |pad_map|
      MIDI.using(@input) do

        # When we get a control change message, update a
        # thread attribute with the value.
        thru_except :control_change do |msg|
          key = "k#{msg.data[0]}".to_sym
          puts "key: #{key} value: #{msg.value}"
          midithread[key] = msg.value
        end

        join
      end
    end
    midithread
  end
end

# Then we start the DRb server, as per the DRb docs.

FRONT_OBJECT = SPIMidiConnector.new

$SAFE = 1

DRb.start_service(DRB_URI, FRONT_OBJECT)
DRb.thread.join

The client code which Phill supplies in the gist is added to your Sonic Pi program running in the current buffer is even easier.

require 'drb/drb'
DRB_URI="drbunix:/var/tmp/sonic-pi-midiconnector"
@lpd = DRbObject.new_with_uri(DRB_URI)

use_bpm 120

live_loop :kick do
  sample :bd_haus
  sleep 1
end

with_fx :bitcrusher do
  live_loop :saw do
    sync :kick
    use_synth :dsaw
    play chord(:c, :major, num_octaves: 4).choose,  cutoff: @lpd.k1, release: 2, attack: 1
    sleep 0.5
  end
end

This specimen program lets you control the cutoff value for the play command using rotary 1 on the lpd8 controller (or whichever one you are using). In fact the business part of the connection is all contained within the first three lines of code.

require 'drb/drb'
DRB_URI="drbunix:/var/tmp/sonic-pi-midiconnector"
@lpd = DRbObject.new_with_uri(DRB_URI)

Both of the above programs can be downloaded from https://gist.github.com/kill9zombie/8b24389239891a5dbe3e
either as a zip folder or by selecting the raw version of each file and saving them from your browser.

Once these are in place, you can access any of the available controllers using the construct: @lpd.k<n>    where <n> represents the controller number.

First you can test the connection between the midi controller and the server program. In a terminal window, navigate to the folder containing the server program and start it running by typing

ruby drb_server.rb

Now rotate a control knob on your midi controller and you should see its value and name output on the terminal screen. If the knobs that you choose to use have key names in the range k1-k8 then all is well. If you have a entry like k91 which is one of the values I get with my Oxygen-8 controller then you have to add an entry like

 def k91; get(:k91); end

to the server program as I have done in the listing above.

Now turning to the client program. Start Sonic Pi running and in an entry buffer window add the client program listed above. (You can either type it in, copy and paste it, or use the load key to select and load the file where it is saved.)

As supplied, it responds to changes in controller knob k1. If that is not available in the outputs you can see in the server terminal window, you may have to allocate it to an alternative number.

Having set the system up and tested it on both my Mac and my Pi2, I wrote a simple program which utilised 6 of the rotary controllers on my Oxygen-8, and I used the to control the parameters of a single note played repetitively in a live_loop.  I used k numbers 1,5,7,10,71 and 91 to match the codings on my knobs. These were used to select the synth and adjust the note value, duration, cutoff and amplitude of the note and its pan position. The Sonic Pi client program is shown below.

#demo of Sonic Pi controlled by three external midi-potentiometers

require 'drb/drb'
DRB_URI="drbunix:/var/tmp/sonic-pi-midiconnector"
@lpd = DRbObject.new_with_uri(DRB_URI)

live_loop :midi do
  
  k= @lpd.k7
  n=@lpd.k10
  c=@lpd.k5
  d=@lpd.k1
  d=0.05+0.4*d/127
  a=@lpd.k91/127.0*2 - 1
  fx=@lpd.k71/127.0
  with_fx :level,amp: fx do
    s=:beep  if range(110,128).include? k
    s=:tri   if range(88,110).include? k
    s= :pulse  if range(66,88).include? k
    s= :tb303   if range(44,66).include? k
    s= :fm   if range(22,44).include? k
    s= :zawa  if range(0,22).include? k
    puts "Synth "+s.to_s
    puts "Note "+n.to_s
    puts "Cutoff "+c.to_s
    puts "Duration "+d.to_s
    puts "Pan "+p.to_s
    puts "Level "+fx.to_s
    use_synth s
    play n,sustain: d,cutoff: c,pan: p
    sleep d
  end
end

The end effect is quite impressive. It brings a new dimension to live coding, especially if like me your typing is not very fluent. The program gives feedback as to the current settings via puts statements, and you can adjust the parameters as the program plays. As written it selects between 5 different synths, but you can also alter the choices and rerun the program to alter the live_loop whilst it is playing, getting the best of both ways of live coding. You may want to increase slightly the minimum duration value on a Raspberry Pi, to prevent breakup of the sound, especially with some of the more complex sysnths like zawa and tb303, but otherwise the program performs well on both Mac and Pi. Unfortunately I think there are issues in using it on Windows, and currently it doesn’t run on my Windows box.

You can hear a sample recorded with the above programs here

watch a visualised video produced from this program here