Grand Piano sample based instrument for Sonic Pi 2

See addition to this article here

Sonic Pi can play user supplied samples, and having come across the excellent samples resource called the Sonatina Symphony Orchestra or SSO, I resolved to use some of the samples the samples there to try and add a sample based Piano for use with Sonic Pi. In the event I added two, as there are sets of samples for a softly played piano and for a loudly played piano. Simply by switching the samples you get two for the price of one as it were. My first attempts, for which I published some audio, were encouraging, but I found it difficult to adjust the tuning of the notes. Basically samples are provided over seven octaves, for every fourth semitone. The idea is that you then use a sample and play it at a slower rate to produce the semitone below the sample note and at a slightly higher rate to produce the semitone above. You need to work out the factors by which to increase and decrease the rate. Initially I tried multiplying by (1 + 1.0/12) for the higher semitone and my (1-0.5/12) for the lower one, also applying a correction factor for tuning. This proved difficult to do, so I abandoned the idea and instead found on the internet a list of the frequencies of the notes in a grand piano tuned to A 440Hz. I put these into a spreadsheet which I used to calculate the rate required for the note above by dividing the frequency of a supplied sample note by the required frequency of the note a semitone above, or for the note below by dividing the frequency of that note by the frequency of the sample. I used the spreadsheet to carry out various text manipulations to produce a three element array for each note specifying the symbolic note name, e.g. :d4 or :gs5, the sample name of the associated base note and the rate multiplier required. In fact the sample name was provided as a parameter to a function which allowed you to choose which the the two sets of piano samples to use. The first part of the spreadsheet is shown below:

notespreadsheet

I extracted the entries in the last column pasting them into the TextWrangler text editor on my Mac, where I further manipulated them to generate an array named sam which contained all of the individual note arrays. The two original sets of piano samples were called names like piano-f-f#3.wav, piano-f-a7.wav for the loud piano samples, and
piano-s-f#3.wav, piano-s-a7.wav for the soft piano samples. I changed the – to _ and replaced all the # with s giving names like piano_f_fs3.wav, piano_f_a7.wav and
piano_s_fs3.wav, piano_s_a7.wav respectively. I used Apple’s Automator program to write a script to carry out the bulk renaming of the 58 samples.
In the program I set up a variable ps to select the sample set required. One of two lines was uncommented:

#ps="piano_f_"
ps="piano_s_"

to choose the required prefix, and then a short function enabled the complete sample name required to be selected in the sam array entries. The function was:

define :sname do |sn,n|
 return (sn+n).intern
end

So an entry in the sam array such as sname(ps,”ds1″) would produce :piano_s_ds1
The .intern converts the calculated string into a symbol signified by the initial :

The remainder of the program defining the instrument follows the same pattern as that which I described previously in an article about adding a flute voice to Sonic Pi, so I will not repeat the explanation here. There are however subtle differences in the envelope used when playing the piano. I found it best to set the attack: parameter to zero, and instead of using the sustain: and release: parameters set to say 0.95 * noteduration and 0.1*noteduration I found it better to use a longer release time, say equal to the note duration or a little less. Small changes could make quite a lot of difference to what the notes sounded like, so play around with these.
There is one problem with the instrument, and that is that the code to set it up takes some 6400 character os so, depending upon how many comments are included. Unfortunately at present the Mac version of Sonic Pi can only handle workspace entries of about 9000 characters before it refuses to play. Sam Aaron is aware of the problem, but says it will not be sorted until a later release, so at present, on a Mac, you can only play short pieces using the instrument. No such problems of course on the Raspberry Pi which works well, with longer pieces.

I have converted several of the pieces I have previously written using Sonic Pi’s built in synths to use the Grand Piano, including Bach’s Minuet in G (which works on a Mac too!), Bach’s Two Part Invention no 8, Scott Joplin’s Maple Leaf Rag and Bach’s Prelude in C, and downloads of these are included below. I have also produced a base program, which includes some test routines which you can try out to see what the instrument sounds like. This too works fine on Mac or Raspberry Pi.

Listing of the base test program:

#defining two grandpiano sample based instruments for Sonic Pi: includes some sample functions for testing
#version 2
#select/adjust samples location
#use_sample_pack '/Users/rbn/Desktop/samples/GrandPiano/'
use_sample_pack '/home/pi/samples/GrandPiano/'

#select loud (f) or soft (p) piano samples
#ps="piano_f_"
ps="piano_p_"

define :sname do |sn,n| #generates sample name for selected pack
 return (sn+n).intern
end
#array holding sample info for each note: [note, sample name, rate to play]
sam = [[:c1,sname(ps,"c1"),1],[:cs1,sname(ps,"c1"),1.05946207098999]]
sam.concat [[:d1,sname(ps,"ds1"),0.943873759671286],[:ds1,sname(ps,"ds1"),1],[:e1,sname(ps,"ds1"),1.05946121072025]]
sam.concat [[:f1,sname(ps,"fs1"),0.943873745116142],[:fs1,sname(ps,"fs1"),1],[:g1,sname(ps,"fs1"),1.05946252159492]]
sam.concat [[:gs1,sname(ps,"a1"),0.943874545454545],[:a1,sname(ps,"a1"),1],[:as1,sname(ps,"a1"),1.05946363636364]]
sam.concat [[:b1,sname(ps,"c2"),0.94387399398224],[:c2,sname(ps,"c2"),1],[:cs2,sname(ps,"c2"),1.05946359989237]]
sam.concat [[:d2,sname(ps,"ds2"),0.943874973162068],[:ds2,sname(ps,"ds2"),1],[:e2,sname(ps,"ds2"),1.05946385846542]]
sam.concat [[:f2,sname(ps,"fs2"),0.943874826213586],[:fs2,sname(ps,"fs2"),1],[:g2,sname(ps,"fs2"),1.05946360269237]]
sam.concat [[:gs2,sname(ps,"a2"),0.943872727272727],[:a2,sname(ps,"a2"),1],[:as2,sname(ps,"a2"),1.05946363636364]]
sam.concat [[:b2,sname(ps,"c3"),0.943874079793293],[:c3,sname(ps,"c3"),1],[:cs3,sname(ps,"c3"),1.05945892227837]]
sam.concat [[:d3,sname(ps,"ds3"),0.943874828847477],[:ds3,sname(ps,"ds3"),1],[:e3,sname(ps,"ds3"),1.05946786832344]]
sam.concat [[:f3,sname(ps,"fs3"),0.943874765536738],[:fs3,sname(ps,"fs3"),1],[:g3,sname(ps,"fs3"),1.05946582917561]]
sam.concat [[:gs3,sname(ps,"a3"),0.943872727272727],[:a3,sname(ps,"a3"),1],[:as3,sname(ps,"a3"),1.05946363636364]]
sam.concat [[:b3,sname(ps,"c4"),0.943874079793293],[:c4,sname(ps,"c4"),1],[:cs4,sname(ps,"c4"),1.05946274452845]]
sam.concat [[:d4,sname(ps,"ds4"),0.9438750092406],[:ds4,sname(ps,"ds4"),1],[:e4,sname(ps,"ds4"),1.05946446306492]]
sam.concat [[:f4,sname(ps,"fs4"),0.943874765536738],[:fs4,sname(ps,"fs4"),1],[:g4,sname(ps,"fs4"),1.05946312642908]]
sam.concat [[:gs4,sname(ps,"a4"),0.943875],[:a4,sname(ps,"a4"),1],[:as4,sname(ps,"a4"),1.05946363636364]]
sam.concat [[:b4,sname(ps,"c5"),0.943873972529436],[:c5,sname(ps,"c5"),1],[:cs5,sname(ps,"c5"),1.05946285816941]]
sam.concat [[:d5,sname(ps,"ds5"),0.9438750092406],[:ds5,sname(ps,"ds5"),1],[:e5,sname(ps,"ds5"),1.05946285600414]]
sam.concat [[:f5,sname(ps,"fs5"),0.943873490011338],[:fs5,sname(ps,"fs5"),1],[:g5,sname(ps,"fs5"),1.05946304607231]]
sam.concat [[:gs5,sname(ps,"a5"),0.943873863636364],[:a5,sname(ps,"a5"),1],[:as5,sname(ps,"a5"),1.05946363636364]]
sam.concat [[:b5,sname(ps,"c6"),0.943876731963688],[:c6,sname(ps,"c6"),1],[:cs6,sname(ps,"c6"),1.05946488294314]]
sam.concat [[:d6,sname(ps,"ds6"),0.943873492378527],[:ds6,sname(ps,"ds6"),1],[:e6,sname(ps,"ds6"),1.05946115338567]]
sam.concat [[:f6,sname(ps,"fs6"),0.943870863119772],[:fs6,sname(ps,"fs6"),1],[:g6,sname(ps,"fs6"),1.05946026297653]]
sam.concat [[:gs6,sname(ps,"a6"),0.943875],[:a6,sname(ps,"a6"),1],[:as6,sname(ps,"a6"),1.05946590909091]]
sam.concat [[:b6,sname(ps,"c7"),0.943874820831343],[:c7,sname(ps,"c7"),1],[:cs7,sname(ps,"c7"),1.05946488294314]]
sam.concat [[:d7,sname(ps,"ds7"),0.943873492378527],[:ds7,sname(ps,"ds7"),1],[:e7,sname(ps,"ds7"),1.05946115338567]]
sam.concat [[:f7,sname(ps,"fs7"),0.943874241543805],[:fs7,sname(ps,"fs7"),1],[:g7,sname(ps,"fs7"),1.05946026297653]]
sam.concat [[:gs7,sname(ps,"a7"),0.943875],[:a7,sname(ps,"a7"),1],[:as7,sname(ps,"a7"),1.05946306818182]]
sam.concat [[:b7,sname(ps,"c8"),0.943874954909329],[:c8,sname(ps,"c8"),1]]
#puts sam

#note aliases to allow flats
flat=[:db1,:eb1,:fb1,:gb1,:ab1,:bb1,:cb2,:db2,:eb2,:fb2,:gb2,:ab2,:bb2,:cb3,:db3,:eb3,:fb3,:gb3,:ab3,:bb3,:cb3,:db4,:eb4,:fb4,:gb4,:ab4,:bb4,:cb4,:db5,:eb5,:fb5,:gb5,:ab5,:bb5,:cb5,:db6,:eb6,:fb6,:gb6,:ab6,:bb6,:cb7,:db7,:eb7,:fb7,:gb7,:ab7,:bb7,:cb8]
sharp=[:cs1,:ds1,:e1,:fs1,:gs1,:as1,:b1,:cs2,:ds2,:e2,:fs2,:gs2,:as2,:b2,:cs3,:ds3,:e3,:fs3,:gs3,:as3,:b3,:cs4,:ds4,:e4,:fs4,:gs4,:as4,:b4,:cs5,:ds5,:e5,:fs5,:gs5,:as5,:b5,:cs6,:ds6,:e6,:fs6,:gs6,:as6,:b6,:cs7,:ds7,:e7,:fs7,:gs7,:as7,:b7]

#add es and bs with aliases
flat.concat [:es1,:es2,:es3,:es4,:es5,:es6,:es7,:bs1,:bs2,:bs3,:bs4,:bs5,:bs6,:bs7]
sharp.concat [:f1,:f2,:f3,:f4,:f5,:f6,:f7,:c2,:c3,:c4,:c5,:c6,:c7,:c8]
extra=[]
flat.zip(sharp).each do |f,s|
 extra.concat [[f,(sam.assoc(s)[1]),(sam.assoc(s)[2])]]
end
sam = sam + extra #add in flat definitions

#definition to play a sample "note" specify note, duration,pan,volume,release type as parameters
define :pl do |n,d=0.2,pan=0,v=0.8,nodamp=0|
 if nodamp == 0
 rt=d #gives reasonable result: experiment with value
 else
 rt = sample_duration(sam.assoc(n)[1]) #release is sample duration time
 end
 sample (sam.assoc(n)[1]),rate: (sam.assoc(n)[2]),attack: 0,sustain: d*0.95,release: rt,amp: v,pan: pan
end

define :ntosym do |n| #this returns the equivalent note symbol to an input integer e.g. 59 => :b4
 #nb no error checking on integer range included
 #only returns notes as n or n sharps.But will sound ok for flats
 @note=n % 12
 @octave = n / 12 - 1
 #puts @octave #for debugging
 #puts @note
 lookup_notes = {
 0 => :c,
 1 => :cs,
 2 => :d,
 3 => :ds,
 4 => :e,
 5 => :f,
 6 => :fs,
 7 => :g,
 8 => :gs,
 9 => :a,
 10 => :as,
 11 => :b}
 return (lookup_notes[@note].to_s + @octave.to_s).to_sym #return the required note symbol
end

define :tr do |nv,shift| #this enables transposition of the note. Shift is number of semitones to move
 if shift ==0 then
 return nv
 else
 return ntosym(note(nv)+shift)
 end
end

s = 1.0 / 8 #s is speed multiplier set for 60 bpm here
#note use of 1.0
dsq = 1 * s
sq = 2 * s
sqd = 3 * s
q = 4 * s
qt = 2.0/3*q
qd = 6 * s
qdd = 7 * s
c = 8 * s
cd = 12 * s
cdd = 14 * s
m = 16 * s
md = 24 * s
mdd = 28 * s
b = 32 * s
bd = 48 * s

define :plarray do |nt,dur,shift=0,vol=0.6,pan=0| #This plays associated arrays of notes and durations, transposing if set, and handling rests
 nt.zip(dur).each do |n,d|
 if n != :r then
 puts n
 pl(tr(n,shift),d,pan)
 end
 sleep d
 end
end

#===============define test and sample playing functions
#select which to call at the end

define :test do #plays a single note
 pl(:cs4,1)
 sleep 1
end

define :test2 do #plays a chromatic scale
 sc=scale(:c4,:chromatic)
 sc.each do |n|
 pl(ntosym(n),0.2)
 sleep 0.2
 end
end

define :randnotes do #plays pairs of 50 notes at random
 puts note(:c1)
 puts note(:c8)
 49.times do
 st=[dsq,sq,sqd,q,qd,c,cd,m].choose
 pl(ntosym(rrand_i(24,108).to_i),st)
 pl(ntosym(rrand_i(24,108).to_i),st)
 sleep st
 end
 pl(ntosym(rrand_i(24,108).to_i),1,0,0.8,1) #add extra pair with long release time
 pl(ntosym(rrand_i(24,108).to_i),1,0,0.8,1)
 sleep 5
end

define :chromatic do #plays all notes in range as chromatic scale
 #puts note(:c1) #uncomment to get values for loop
 #puts note(:c8)
 24.upto(107) do |n|
 pl(ntosym(n),dsq)
 sleep dsq
 end
 pl(ntosym(108),0.2,0,0.8,1)#long release time
end

define :transposetest do |shift = 0| #shows transpose being used
 l=[]
 #build scale in l array as symbolic values, not numbers
 scale(:c4,:major,num_octaves: 2).each do |n|
 l = l + [ntosym(n)]
 end
 #use plarray function to play: parameters shown below
 #plarray do |nt,dur,shift=0,vol=0.6,pan=0|
 plarray(l,[dsq]*l.length,shift) #generate duration array as demisemiquavers
 plarray(l.reverse[1..-1],[dsq]*(l.length-1),shift) #play descending as well
end
#============== select test/sample to try here ============
#loop {test}

#loop {test2}

randnotes #uncomment to select
chromatic #uncomment to select

comment do #change to uncomment to select
 0.upto(12) do |shift|
 transposetest(shift)
 end
end
#===============end

This program contains the code to set up the two grand piano voices. It also includes some sample and test routines at the end to try out the voice. As supplied, it plays 50 pairs of notes selected at random and for random note duration, followed by a chromatic scale over the entire range of the voice from :c1 up to :c8.

As well as the above program GP-setup.txt, also included are the following programs:
For Raspberry Pi or Mac: GP-BachMinuetInG.txt

For RaspberryPi ONLY (too long to play on Mac): GP-Bach2PartInventionNo8.txt,
GP-BachPreludeInC.trxt, GP-ScottJoplinMapleLeafRag.txt

You can download a zip file of the samples directory GrandPiano.zip here

You can download a zip file of the Mac and Raspberry Pi compatible programs here

You can download a zip file of the Raspberry Pi only programs here

Remember you need to install the samples folder in a known location and adjust the Sonic Pi programs, so that they know where they are. The two locations I suggest are in the home folder for the user pi on the Raspberry Pi and on the desktop of the logged in user on an Apple Mac.

Finally, do turn off printed output in the Prefs if running on a Pi. It cannot cope if it has to print lots of output when running some of these programs. You may also find you need to increase the set_sched_ahead_time! value where included. Performance will improve on a second or third run of the program, once all of the samples are fully loaded and prepared.

Leave a comment