Adding a new sample based flute voice for Sonic Pi

This article has recently been updated, with the addition of some notes missing from the range of the flute “instrument”. The article is still worth reading if you want to understand how the new flute voice is implemented, and the changes will make no difference to the sample Telemann piece which the program plays. The changes are explained in a post here. You can download the amended version there.

I will be upgrading the tuning of this instrument, using the same method employed for the grand piano later. THIS HAS NOW BEEN DONE. DETAILS HERE

 

Recently, as reported here I came across a nice library of samples based on the instruments of an orchestral orchestra called the Sonatina Symphonic Orchestra (hereafter SSO). I decided to see if I could produce a new instrument for use in Sonic Pi. The aim was to be able to use it like a built in synth and to transcribe music for it to play.

The SSO presents samples in two ways. It has folders arranged by instrument containing single note wav file samples for a wide variety of instruments, some stacatto and some sustained, and also a list of .sfz sforzando format files, which contain text descriptions of the samples. These can be opened with a text editor. .sfz is a standard format for player apps which work with samples, and it enables them to produce a complete range of notes, using the individual samples. I was working on a Mac and found the free program Sforzando which is also available on Windows, produced by the company Pogue, which is an sfz player. You can drop an sfz file on it, and it will pull in the associated samples and let you play them on a built in keyboard. The sfz file contains the location of the samples, and more importantly which sample to use for which range of notes. When you play a note, the program reads which sample to use, and adjusts its pitch if necessary before playing it. I looked at the solo flute sfz file. This references 13 samples, and uses them to cover a range of notes from :c3 up to :c6 Each sample (apart from the lowest and the highest) is used to cover three semitones by playing the sample at the pitch ti was recorded, and playing a semitone lower or higher by adjusting the play rate accordingly. Small correction factors are also suggested for fine tuning.
A copy of the flute solo sfz file is shown below.

sdf// ------------------------------
//  Sonatina Symphonic Orchestra
// ------------------------------
//            Flute
// ------------------------------


ampeg_release=0.425


sample=Samples\Flute\flute-c3.wav
lokey=c3
hikey=c#3
pitch_keycenter=c3


sample=Samples\Flute\flute-d#3.wav
lokey=d3
hikey=e3
pitch_keycenter=d#3


sample=Samples\Flute\flute-f#3.wav
lokey=f3
hikey=g3
pitch_keycenter=f#3


sample=Samples\Flute\flute-a3.wav
lokey=g#3
hikey=a#3
pitch_keycenter=a3


sample=Samples\Flute\flute-c4.wav
lokey=b3
hikey=c#4
tune=-16


sample=Samples\Flute\flute-d#4.wav
lokey=d4
hikey=e4
pitch_keycenter=d#4
tune=-7


sample=Samples\Flute\flute-f#4.wav
lokey=f4
hikey=g4
pitch_keycenter=f#4
tune=-4


sample=Samples\Flute\flute-a4.wav
lokey=g#4
hikey=a#4
pitch_keycenter=a4
tune=-17


sample=Samples\Flute\flute-c5.wav
lokey=b4
hikey=c#5
pitch_keycenter=c5
tune=-4


sample=Samples\Flute\flute-d#5.wav
lokey=d5
hikey=e5
pitch_keycenter=d#5
tune=-8


sample=Samples\Flute\flute-f#5.wav
lokey=f5
hikey=g5
pitch_keycenter=f#5
tune=-16


sample=Samples\Flute\flute-a5.wav
lokey=g#5
hikey=a#5
pitch_keycenter=a5
tune=-17


sample=Samples\Flute\flute-c6.wav
lokey=b5
hikey=c6
pitch_keycenter=c6
tune=-18

My aim was to use Sonic Pi 2 in a similar manner, to choose the relevant sample for any given note in the range :c3 to :c6, and to have a mechanism to play the note for a given duration, at a given amplitude. Also if possible I wanted to include a transpose facility as is used for standard built in Synths in Sonic Pi.
The first thing to do was to take the samples and add them to my samples folder. I also renamed them, as the original names included both – and # characters which were not ideal in a variable name. I replaced the minus  – with an underline _ and the (comment) # with a s (for sharp), giving sample names.
flute_a3.wav, flute_a4.wav, flute_a5.wav, flute :c3.wav, flute_c4.wav, flute_c5.wav, flute_c6.wav, flute_ds3.wav, flute_ds4.wav, flute_ds5.wav, flute_fs3.wav, flute_fs4.wav and flute_fs5.wav

I put them in a folder called flute inside my samples directory. I started the program, first by defining the sample location, and then by creating a sample array which I called sam. This initially had 36 entries, each consisting of a three element list of the format [note name, sample name, rate value] Three different basic rate values were required: 1 when the sample was played at the pitch recorded, (1 + 1.0/12) when it was to be played a semitone higher: {a rate of 2 would double the pitch, and since there are 12 semitones in the octave a rate of 1 + 1.0/12 raisers the pitch by a semitone} and (1 – 0.5/12) when it was to be played a semitone lower : {here halving the rate to 0.5 would lower pitch by an octave, so we want 1/12 of that difference subtracted giving (1-0.5/12)}. The sfz file also suggests some corrections for certain samples to improve the tuning. These are expressed in cents or 1/100 of a semitone or 1/1200 of an octave. Before creating the array I defined a function called rateoffset which generated the rate required for the note. This function and the sam array definition are shown below.

define :rateoffset do |s,c=0|#first parameter semitone shift, second correction in cents (1/1200 of an octave)
 case s
 when 0
 val=1
 when -1
 val=(1-0.5/12)
 when 1
 val=(1+1.0/12)
 end
 return val*(1+c.to_f/1200) #make sure cent value is a float to allow division
end

sam=[[:c3,:flute_c3,rateoffset(0)],[:cs3,:flute_c3,rateoffset(1)]]
sam.concat [[:d3,:flute_ds3,rateoffset(-1)],[:ds3,:flute_ds3,rateoffset(0)],[:e3,:flute_ds3,rateoffset(1)]]
sam.concat [[:f3,:flute_fs3,rateoffset(-1)],[:fs3,:flute_fs3,rateoffset(0)],[:g3,:flute_fs3,rateoffset(1,-15)]]
sam.concat [[:gs3,:flute_a3,rateoffset(-1)],[:a3,:flute_a3,rateoffset(0)],[:as3,:flute_a3,rateoffset(1)]]
sam.concat [[:b3,:flute_c4,rateoffset(-1,-16)],[:c4,:flute_c4,rateoffset(0)],[:cs4,:flute_c4,rateoffset(1,-16)]]
sam.concat [[:d4,:flute_ds4,rateoffset(-1,-7)],[:ds4,:flute_ds4,rateoffset(0)],[:e4,:flute_ds4,rateoffset(1,-7)]]
sam.concat [[:f4,:flute_fs4,rateoffset(-1,-4)],[:fs4,:flute_fs4,rateoffset(0)],[:g4,:flute_fs4,rateoffset(1,-4)]]
sam.concat [[:gs4,:flute_a4,rateoffset(-1,-17)],[:a4,:flute_a4,rateoffset(0)],[:as4,:flute_a4,rateoffset(1,-17)]]
sam.concat [[:b4,:flute_c5,rateoffset(-1,-4)],[:c5,:flute_c5,rateoffset(0)],[:cs5,:flute_c5,rateoffset(1,-4)]]
sam.concat [[:d5,:flute_ds5,rateoffset(-1,-8)],[:ds5,:flute_ds5,rateoffset(0)],[:e5,:flute_ds5,rateoffset(1,-8)]]
sam.concat [[:f5,:flute_fs5,rateoffset(-1,-16)],[:fs5,:flute_fs5,rateoffset(0)],[:g5,:flute_fs5,rateoffset(1,-16)]]
sam.concat [[:gs5,:flute_a5,rateoffset(-1,-17)],[:a5,:flute_a5,rateoffset(0)],[:as5,:flute_a5,rateoffset(1,-17)]]
sam.concat [[:b5,:flute_c6,rateoffset(-1,-18)],[:c6,:flute_c6,rateoffset(0)]]

Dealing first with rateoffset. This has two parameters. The first specifies the semitone offset which will be -1, 0 or 1 for each sample. The second specifies the tuning correction in cents to be applied when the offset is non-zero. A case statement is used to select the factor (stored in val) for the semitone offset. As discussed above this is (1 + 1.0/12) or (1 – 0.5/12) Note the values 1.0 and 0.5 make sure that floating point arithmetic is used. The tuning correction is given by the factor (1+c.to_f/1200) note c can be positive or negative, and will normally be expressed as an integer. The .to_f converts it to floating point so that floating point division is used when it is divided by 1200. A return command makes the function return the calculated rate value for the note.
The array sam has a three element array list for each of the 36 notes. A typical entry is [:gs4,:flute_a4,rateoffset(-1,-17)] The first element is the name of the note to be sounded using standard Sonic Pi symbolic notation. The second is the sample name to be used, the third is the calculated rate using the rateoffset function.
The notes in sam are added using base notes such as c,d,e,f,g and their sharp values cs,ds,fs,gs etc. So that we can deal with music where the notes are specified as flats eg :bb4 or :eb5 I then added a few more lines to add duplicate values to the sam array for these alternatives db=cs gb=fs bb=as etc

#note aliases
flat=[:db3,:eb3,:fb3,:gb3,:ab3,:bb3,:cb3,:db4,:eb4,:fb4,:gb4,:ab4,:bb4,:cb4,:db5,:eb5,:fb5,:gb5,:ab5,:bb5,:cb5]
sharp=[:cs3,:ds3,:e3,:fs3,:gs3,:as3,:b3,:cs4,:ds4,:e4,:fs4,:gs4,:as4,:b4,:cs5,:ds5,:e5,:fs5,:gs5,:as5,:b5]
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

First I created two arrays for the corresponding notes of flats and sharps named flat and sharp. I initialised a blank array called extra in which to build the extra notes required, and then traversed the flat and sharp arrays together using the Ruby construction flat.zip(sharp).each do|f,s|
This zips or links the arrays together and then iterates round each pair of corresponding elements, allocating them to the variables f and s.
The next line extra.concat [[f,(sam.assoc(s)[1]),(sam.assoc(s)[2])] looks horrible but carries out the required operation. It adds on (or concatenates) to the end of the extra array  the next calculated entry. The first element is f (the current note in the flat array say :db3. The second element is (sam.assoc(s)[1])  sam.assoc(s) goes to the sam array and find the array entry whose first element is s (in this case :cs3 the value corresponding to the first f entry :db3). the [1] then selects the SECOND element of that 3 element array (remember array elements number from 0) which is flute_c3. The third element similarly returns sam.assoc(:cs3)[2] or the THIRD element of this group which is rateoffset(1) So effectively we add in the original sam entry for :cs3 with the :cs3 replaced by :db3.
Finally we add the array extra onto the end of the array sam. The completed sam array is printed out below (using puts sam in Sonic Pi) You will see the added flat entries at the end.

[[:c3, :flute_c3, 1.0], [:cs3, :flute_c3, 1.0833333333333333], [:d3, :flute_ds3, 0.9583333333333334], [:ds3, :flute_ds3, 1.0], [:e3, :flute_ds3, 1.0833333333333333], [:f3, :flute_fs3, 0.9583333333333334], [:fs3, :flute_fs3, 1.0], [:g3, :flute_fs3, 1.0697916666666667], [:gs3, :flute_a3, 0.9583333333333334], [:a3, :flute_a3, 1.0], [:as3, :flute_a3, 1.0833333333333333], [:b3, :flute_c4, 0.9455555555555556], [:c4, :flute_c4, 1.0], [:cs4, :flute_c4, 1.0688888888888888], [:d4, :flute_ds4, 0.9527430555555556], [:ds4, :flute_ds4, 1.0], [:e4, :flute_ds4, 1.0770138888888887], [:f4, :flute_fs4, 0.955138888888889], [:fs4, :flute_fs4, 1.0], [:g4, :flute_fs4, 1.0797222222222222], [:gs4, :flute_a4, 0.9447569444444445], [:a4, :flute_a4, 1.0], [:as4, :flute_a4, 1.067986111111111], [:b4, :flute_c5, 0.955138888888889], [:c5, :flute_c5, 1.0], [:cs5, :flute_c5, 1.0797222222222222], [:d5, :flute_ds5, 0.9519444444444445], [:ds5, :flute_ds5, 1.0], [:e5, :flute_ds5, 1.076111111111111], [:f5, :flute_fs5, 0.9455555555555556], [:fs5, :flute_fs5, 1.0], [:g5, :flute_fs5, 1.0688888888888888], [:gs5, :flute_a5, 0.9447569444444445], [:a5, :flute_a5, 1.0], [:as5, :flute_a5, 1.067986111111111], [:b5, :flute_c6, 0.9439583333333333], [:c6, :flute_c6, 1.0], [:db3, :flute_c3, 1.0833333333333333], [:eb3, :flute_ds3, 1.0], [:fb3, :flute_ds3, 1.0833333333333333], [:gb3, :flute_fs3, 1.0], [:ab3, :flute_a3, 0.9583333333333334], [:bb3, :flute_a3, 1.0833333333333333], [:cb3, :flute_c4, 0.9455555555555556], [:db4, :flute_c4, 1.0688888888888888], [:eb4, :flute_ds4, 1.0], [:fb4, :flute_ds4, 1.0770138888888887], [:gb4, :flute_fs4, 1.0], [:ab4, :flute_a4, 0.9447569444444445], [:bb4, :flute_a4, 1.067986111111111], [:cb4, :flute_c5, 0.955138888888889], [:db5, :flute_c5, 1.0797222222222222], [:eb5, :flute_ds5, 1.0], [:fb5, :flute_ds5, 1.076111111111111], [:gb5, :flute_fs5, 1.0], [:ab5, :flute_a5, 0.9447569444444445], [:bb5, :flute_a5, 1.067986111111111], [:cb5, :flute_c6, 0.9439583333333333]]

Note the rate values which are either around 0.95 or 1 or around 1.07.
Now we need a definition to substitute for the play command in Sonic Pi. This one will play a sample note. I named it pl and it looks like this

#definition to play a sample "note" specify note, duration,volume as parameters
define :pl do |n,d=0.2,pan=0,v=0.8|
 st = 0
 if [:b5,:c6].include? n then #bodge for :b5 and :c6. Too breathy so cut the start
 st = 0.2
 end
 sample (sam.assoc(n)[1]),rate: (sam.assoc(n)[2]),attack: d/50,sustain: d*0.88,release: d*0.1,amp: v,pan: pan,start: st
end

It started off very straight-forward but later I added some “bodge” code to deal with a problem with the highest two notes :b5 and :c6. The sample concerned was very breathy and if a short duration note was played, you just heard a lot of air without much note to it. As an experiment I chopped of the start of the note for these two pitches playing from 20% of the sample onwards using the sample start: parameter. You can play around with the 0.2 and perhaps reduce it. Otherwise the function takes parameters for the note and its duration and also for the pan position in the stereo image and the volume. Three of the parameters have default values so pl(:a4) would work for example. The sample command selects the sample using sam.assoc(n)[1] as described earlier (n is the note required), retrieves the rate from sam.assoc(n)[2] and sets attack sustain and release values governed by the duration value d. pan: amp: and start: values are also set, the start value being 0 apart from the two breathy notes.
The next problem was to write code to deal with being able to transpose a note if required. To do this I needed to be able to convert the note from a symbol to a midi-note value, add the transposition offset and then convert it back to a symbol that the pl command could handle. Two definitions are required. The first ntosym converts an integer midi-note value to an equivalent note symbol. Thus 59 would return :b4 and 72 :c5
The second function tr has two parameters a symbolic note and the required shift as a number of semitones. It returns the transposed symbolic note.

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

I have not added any error checking to these, but in normal use they should be ok. The first function ntosym takes the input integer n and works out the note position in the octave by using n % 12 This give the integer remainder when the note is divided by 12, so for n = 63 to would give 3. It calculates the octave using n / 12 -1 This is using integer division so for n = 63 it would give 5 -1 = 4. From the lookup table we get 3 => :ds
The return value from the function puts these two values together in a string (note the .to_s) and adds them together giving in this example :ds + 4 +> :ds4
One thing to note, the result will always use a sharp for “black” notes even though musically the music might demand say :eb4 in this case. However they will sound the same. Since this will only occur during the transposition process it is not inconvenient, whereas it would be during note entry if you could not use :eb4 instead of :ds4

So we are largely done with the extra bits we need to create and cope with a sample based instrument source. Now we can use the techniques I have used in my previous articles to play some music using it. I have chosen a piece by Georg Philipp Telemann for two flutes. It is a canon: both flutes play exactly the same part (which makes it easier adding the notes!) but with one flute starting several beats after the first part. The music is shown here.

 

Telemann__Sonaten_im_Kanon_op_5
This is the first movement of three. The second flute starts playing one bar or 6 crotchet beats after the first one. As usual I start the note entry section by defining note values in variables. This is followed by a function plarray which is used to play the notes which are helped in two arrays, one containing symbolic note names and the other their associated durations. The function also handles rests which are represented by the symbol :r
I won’t comment further on this section here, as it is exactly the same as I have used in previous articles. If you are uncertain what is happening you can get more detail there.

s = 1.0 / 20 #s is speed multiplier
#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,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

#set up the note and duration arrays here
tn = [:r,:d5,:g5,:fs5,:e5,:d5,:c5,:b4,:c5,:b4,:a4,:b4,:g4,:a4,:d4,:r]
td = [c,c,m,q,q,q,q,q,q,q,q,q,q,c,c,c]
#bar 3
tn.concat [:r,:d5,:g5,:fs5,:g5,:a5,:b5,:c6,:b5,:a5,:b5,:c6,:a5,:g5,:fs5,:g5,:a5,:g5,:fs5,:g5,:a5,:fs5,:d5,:e5,:fs5,:g5,:a5,:b5,:c6,:b5,:a5,:b5,:c6,:a5,:r]
td.concat [c,c,m,q,q,q,q,c,q,q,q,q,cd,q,q,q,c,q,q,q,q,q,sq,sq,q,q,q,q,c,q,q,q,q,m,c]
#bar 7
tn.concat [:r,:d5,:d5,:d5,:d5,:d5,:d5,:d4,:d5,:d4,:d5,:c5,:b4,:a4,:b4,:c5,:b4,:a4,:b4,:g4,:c5,:a4,:b4,:c5,:b4,:a4,:b4,:g4,:a4,:fs4]
td.concat [q,q,q,q,q,q,q,q,q,q,sq,sq,sq,sq,sq,sq,sq,sq,q,q,q,q,sq,sq,sq,sq,q,q,q,q]
#bar 9
tn.concat [:g4,:g5,:a5,:fs5,:g5,:b4,:c5,:a4,:b4,:c5,:b4,:a4,:b4,:e5,:fs5,:ds5,:e5,:fs5,:e5,:ds5,:e5,:g5,:fs5,:e5,:ds5,:fs5,:b4,:r,:b4,:g5,:b4,:r]
td.concat [cd,q,q,q,cd,q,q,q,sq,sq,sq,sq,q,q,q,q,sq,sq,sq,sq,q,q,q,q,q,q,c,c,q,q,c,c]
#bar 12
tn.concat [:b4,:a5,:b5,:a5,:g5,:fs5,:g5,:b5,:a5,:g5,:fs5,:e5,:fs5,:b4,:ds5,:e5,:e4,:r,:r,:r,:a5,:gs5,:a5,:b5]
td.concat [q,c,sq,sq,q,q,q,c,sq,sq,q,q,c,c,c,c,c,c,c,c,m,q,q,c]
#bar 15
tn.concat [:e5,:c6,:b5,:c6,:a5,:b5,:e5,:d5,:c5,:b4,:a4,:r,:r,:r,:gs5,:a5,:e5,:cs5,:a4,:e5,:d5,:cs5,:d5,:a4,:fs4,:d4,:d5,:cs5,:b4]
td.concat [c,q,q,q,q,c,c,c,q,q,c,c,c,c,c,q,q,q,q,q,sq,sq,q,q,q,q,q,sq,sq]
#bar 18
tn.concat [:cs5,:e5,:g5,:fs5,:e5,:d5,:r,:r,:a4,:a4,:a4,:a4,:b4,:cs5,:a4,:d5,:d4,:d5,:d5,:d5,:e5,:fs5,:d5,:g5,:r,:g5,:fs5,:e5,:d5,:c5]
td.concat [c,c,c,q,q,c,c,q,q,q,q,sq,sq,sq,sq,q,q,q,q,sq,sq,sq,sq,c,c,m,q,q,q,q]
#bar 21
tn.concat [:b4,:c5,:b4,:a4,:b4,:g4,:a4,:d4,:r,:r,:d5,:g5,:fs5,:g5,:a5,:b5,:c6,:b5,:a5,:b5,:c5,:a5,:g5,:fs5,:g5]
td.concat [q,q,q,q,q,q,c,c,c,c,c,m,q,q,q,q,c,q,q,q,q,cd,q,q,q]
#bar 24
tn.concat [:a5,:g5,:fs5,:g5,:a5,:fs5,:d5,:e5,:fs5,:g5,:a5,:b5,:c6,:b5,:a5,:b5,:c6,:a5,:r,:r,:d5,:d5,:d5,:d5,:d5,:d5,:d4,:d5,:d4,:d5,:c5,:b4,:a4]
td.concat [c,q,q,q,q,q,sq,sq,q,q,q,q,c,q,q,q,q,m,c,q,q,q,q,q,q,q,q,q,q,sq,sq,sq,sq]
#bar 27
tn.concat [:b4,:c5,:b4,:a4,:b4,:g4,:c5,:a4,:b4,:c5,:b4,:a4,:b4,:g4,:a4,:fs4,:g4,:g5,:a5,:fs5,:g5,:b4,:c5,:a4,:as4,:c5,:a4,:as4,:r,:b4,:c5,:a4,:b4,:r]
td.concat [sq,sq,sq,sq,q,q,q,q,sq,sq,sq,sq,q,q,q,q,cd,q,q,q,cd,q,q,q,qt,qt,qt,c,c,qt,qt,qt,c,c]
#bar 30
tn.concat [:g5,:a5,:fs5,:g5,:a5,:fs5,:g5,:a5,:fs5,:g5,:fs5,:e5,:d5,:c6,:b5,:c6,:a5,:b5,:c6,:a5,:b5,:c6,:a5,:b5,:a5,:g5,:a5,:fs5]
td.concat [qt,qt,qt,qt,qt,qt,qt,qt,qt,cd,sq,sq,q,q,qt,qt,qt,qt,qt,qt,qt,qt,qt,q,sq,sq,c,c]
#bar 32
tn.concat [:g5,:d5,:g5,:d5,:g5,:d5,:g5,:b4,:c5,:a4,:g4,:b4,:a4,:g4,:b4,:a4,:g4,:b4,:a4,:g4,:a4,:fs4,:g4,:r,:r]
td.concat [q,q,q,q,q,q,q,q,c,c,q,sq,sq,q,sq,sq,q,sq,sq,c,c,c,m,c,m]
puts tn.length #debugging check notes and duration arrays have same lengths
puts td.length

The final section of the program plays the piece. As there are two parts playing simultaneously, one is put into a thread and the other played directly. The sleep 6*c at the start of the threaded part ensures that it waits one bar (or 6 beats) before starting to play.

in_thread do #start the second part of the canon after 6 beat rest in thread transposed two semitones down
 sleep 6*c
 plarray(tn,td,-2,-0.8) #transpose -2 pan -0.8
end

plarray(tn,td,-2,0.8) #play the main canon part transposed 2 semitones down pan 0.8

You will notice both parts are identical. The difference being they start at different times. Notice also that I have transposed them down 2 semitones. This is to avoid using the two top notes :b5 and :c6 with their breathy sounds. You can compare running the program with the two offsets (the -2 in the parameter list) changed to 0. You will hear the slightly different timbre that the delayed start brings to these two top notes in this case.
One further change you might like to experiment with is adding two further parts playing in octaves. This can be achieved by replacing the playing section above with that below.

in_thread do #start the second part of the canon after 6 beat rest in thread down an octave
 sleep 6*c
 plarray(tn,td,-12,-0.8)
end
in_thread do #start the main part of the canon straight away in thread down an octave
 plarray(tn,td,-12,0.8)
end
in_thread do #start the second part of the canon after 6 beat rest in thread at normal pitch
 sleep 6*c
 plarray(tn,td,0,-0.8)
end
plarray(tn,td,0,0.8) #play the main canon part at normal pitch

The complete program is listed here

#defining a flute sample based instrument for Sonic Pi, and playing a Canon for two flutes by Telemann
#use_sample_pack '/Users/rbn/Desktop/samples/flute/'
use_sample_pack '/home/pi/samples/flute/'

define :rateoffset do |s,c=0|#first parameter semitone shift, second correction in cents (1/1200 of an octave)
 case s
 when 0
 val=1
 when -1
 val=(1-0.5/12)
 when 1
 val=(1+1.0/12)
 end
 return val*(1+c.to_f/1200) #make sure cent value is a float to allow division
end

sam=[[:c3,:flute_c3,rateoffset(0)],[:cs3,:flute_c3,rateoffset(1)]]
sam.concat [[:d3,:flute_ds3,rateoffset(-1)],[:ds3,:flute_ds3,rateoffset(0)],[:e3,:flute_ds3,rateoffset(1)]]
sam.concat [[:f3,:flute_fs3,rateoffset(-1)],[:fs3,:flute_fs3,rateoffset(0)],[:g3,:flute_fs3,rateoffset(1,-15)]]
sam.concat [[:gs3,:flute_a3,rateoffset(-1)],[:a3,:flute_a3,rateoffset(0)],[:as3,:flute_a3,rateoffset(1)]]
sam.concat [[:b3,:flute_c4,rateoffset(-1,-16)],[:c4,:flute_c4,rateoffset(0)],[:cs4,:flute_c4,rateoffset(1,-16)]]
sam.concat [[:d4,:flute_ds4,rateoffset(-1,-7)],[:ds4,:flute_ds4,rateoffset(0)],[:e4,:flute_ds4,rateoffset(1,-7)]]
sam.concat [[:f4,:flute_fs4,rateoffset(-1,-4)],[:fs4,:flute_fs4,rateoffset(0)],[:g4,:flute_fs4,rateoffset(1,-4)]]
sam.concat [[:gs4,:flute_a4,rateoffset(-1,-17)],[:a4,:flute_a4,rateoffset(0)],[:as4,:flute_a4,rateoffset(1,-17)]]
sam.concat [[:b4,:flute_c5,rateoffset(-1,-4)],[:c5,:flute_c5,rateoffset(0)],[:cs5,:flute_c5,rateoffset(1,-4)]]
sam.concat [[:d5,:flute_ds5,rateoffset(-1,-8)],[:ds5,:flute_ds5,rateoffset(0)],[:e5,:flute_ds5,rateoffset(1,-8)]]
sam.concat [[:f5,:flute_fs5,rateoffset(-1,-16)],[:fs5,:flute_fs5,rateoffset(0)],[:g5,:flute_fs5,rateoffset(1,-16)]]
sam.concat [[:gs5,:flute_a5,rateoffset(-1,-17)],[:a5,:flute_a5,rateoffset(0)],[:as5,:flute_a5,rateoffset(1,-17)]]
sam.concat [[:b5,:flute_c6,rateoffset(-1,-18)],[:c6,:flute_c6,rateoffset(0)]]

#note aliases
flat=[:db3,:eb3,:fb3,:gb3,:ab3,:bb3,:cb3,:db4,:eb4,:fb4,:gb4,:ab4,:bb4,:cb4,:db5,:eb5,:fb5,:gb5,:ab5,:bb5,:cb5]
sharp=[:cs3,:ds3,:e3,:fs3,:gs3,:as3,:b3,:cs4,:ds4,:e4,:fs4,:gs4,:as4,:b4,:cs5,:ds5,:e5,:fs5,:gs5,:as5,:b5]
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,volume as parameters
define :pl do |n,d=0.2,pan=0,v=0.8|
 st = 0
 if [:b5,:c6].include? n then #bodge for :b5 and :c6. Too breathy so cut the start
 st = 0.2
 end
 sample (sam.assoc(n)[1]),rate: (sam.assoc(n)[2]),attack: d/50,sustain: d*0.88,release: d*0.1,amp: v,pan: pan,start: st
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 / 20 #s is speed multiplier
#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,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

#set up the note and duration arrays here
tn = [:r,:d5,:g5,:fs5,:e5,:d5,:c5,:b4,:c5,:b4,:a4,:b4,:g4,:a4,:d4,:r]
td = [c,c,m,q,q,q,q,q,q,q,q,q,q,c,c,c]
#bar 3
tn.concat [:r,:d5,:g5,:fs5,:g5,:a5,:b5,:c6,:b5,:a5,:b5,:c6,:a5,:g5,:fs5,:g5,:a5,:g5,:fs5,:g5,:a5,:fs5,:d5,:e5,:fs5,:g5,:a5,:b5,:c6,:b5,:a5,:b5,:c6,:a5,:r]
td.concat [c,c,m,q,q,q,q,c,q,q,q,q,cd,q,q,q,c,q,q,q,q,q,sq,sq,q,q,q,q,c,q,q,q,q,m,c]
#bar 7
tn.concat [:r,:d5,:d5,:d5,:d5,:d5,:d5,:d4,:d5,:d4,:d5,:c5,:b4,:a4,:b4,:c5,:b4,:a4,:b4,:g4,:c5,:a4,:b4,:c5,:b4,:a4,:b4,:g4,:a4,:fs4]
td.concat [q,q,q,q,q,q,q,q,q,q,sq,sq,sq,sq,sq,sq,sq,sq,q,q,q,q,sq,sq,sq,sq,q,q,q,q]
#bar 9
tn.concat [:g4,:g5,:a5,:fs5,:g5,:b4,:c5,:a4,:b4,:c5,:b4,:a4,:b4,:e5,:fs5,:ds5,:e5,:fs5,:e5,:ds5,:e5,:g5,:fs5,:e5,:ds5,:fs5,:b4,:r,:b4,:g5,:b4,:r]
td.concat [cd,q,q,q,cd,q,q,q,sq,sq,sq,sq,q,q,q,q,sq,sq,sq,sq,q,q,q,q,q,q,c,c,q,q,c,c]
#bar 12
tn.concat [:b4,:a5,:b5,:a5,:g5,:fs5,:g5,:b5,:a5,:g5,:fs5,:e5,:fs5,:b4,:ds5,:e5,:e4,:r,:r,:r,:a5,:gs5,:a5,:b5]
td.concat [q,c,sq,sq,q,q,q,c,sq,sq,q,q,c,c,c,c,c,c,c,c,m,q,q,c]
#bar 15
tn.concat [:e5,:c6,:b5,:c6,:a5,:b5,:e5,:d5,:c5,:b4,:a4,:r,:r,:r,:gs5,:a5,:e5,:cs5,:a4,:e5,:d5,:cs5,:d5,:a4,:fs4,:d4,:d5,:cs5,:b4]
td.concat [c,q,q,q,q,c,c,c,q,q,c,c,c,c,c,q,q,q,q,q,sq,sq,q,q,q,q,q,sq,sq]
#bar 18
tn.concat [:cs5,:e5,:g5,:fs5,:e5,:d5,:r,:r,:a4,:a4,:a4,:a4,:b4,:cs5,:a4,:d5,:d4,:d5,:d5,:d5,:e5,:fs5,:d5,:g5,:r,:g5,:fs5,:e5,:d5,:c5]
td.concat [c,c,c,q,q,c,c,q,q,q,q,sq,sq,sq,sq,q,q,q,q,sq,sq,sq,sq,c,c,m,q,q,q,q]
#bar 21
tn.concat [:b4,:c5,:b4,:a4,:b4,:g4,:a4,:d4,:r,:r,:d5,:g5,:fs5,:g5,:a5,:b5,:c6,:b5,:a5,:b5,:c5,:a5,:g5,:fs5,:g5]
td.concat [q,q,q,q,q,q,c,c,c,c,c,m,q,q,q,q,c,q,q,q,q,cd,q,q,q]
#bar 24
tn.concat [:a5,:g5,:fs5,:g5,:a5,:fs5,:d5,:e5,:fs5,:g5,:a5,:b5,:c6,:b5,:a5,:b5,:c6,:a5,:r,:r,:d5,:d5,:d5,:d5,:d5,:d5,:d4,:d5,:d4,:d5,:c5,:b4,:a4]
td.concat [c,q,q,q,q,q,sq,sq,q,q,q,q,c,q,q,q,q,m,c,q,q,q,q,q,q,q,q,q,q,sq,sq,sq,sq]
#bar 27
tn.concat [:b4,:c5,:b4,:a4,:b4,:g4,:c5,:a4,:b4,:c5,:b4,:a4,:b4,:g4,:a4,:fs4,:g4,:g5,:a5,:fs5,:g5,:b4,:c5,:a4,:as4,:c5,:a4,:as4,:r,:b4,:c5,:a4,:b4,:r]
td.concat [sq,sq,sq,sq,q,q,q,q,sq,sq,sq,sq,q,q,q,q,cd,q,q,q,cd,q,q,q,qt,qt,qt,c,c,qt,qt,qt,c,c]
#bar 30
tn.concat [:g5,:a5,:fs5,:g5,:a5,:fs5,:g5,:a5,:fs5,:g5,:fs5,:e5,:d5,:c6,:b5,:c6,:a5,:b5,:c6,:a5,:b5,:c6,:a5,:b5,:a5,:g5,:a5,:fs5]
td.concat [qt,qt,qt,qt,qt,qt,qt,qt,qt,cd,sq,sq,q,q,qt,qt,qt,qt,qt,qt,qt,qt,qt,q,sq,sq,c,c]
#bar 32
tn.concat [:g5,:d5,:g5,:d5,:g5,:d5,:g5,:b4,:c5,:a4,:g4,:b4,:a4,:g4,:b4,:a4,:g4,:b4,:a4,:g4,:a4,:fs4,:g4,:r,:r]
td.concat [q,q,q,q,q,q,q,q,c,c,q,sq,sq,q,sq,sq,q,sq,sq,c,c,c,m,c,m]
puts tn.length #debugging check notes and duration arrays have same lengths
puts td.length

#play the canon here ==================================
in_thread do #start the second part of the canon after 6 beat rest in thread transposed 2 semitones down
 sleep 6*c
 plarray(tn,td,-2,-0.8) #transpose -2 pan -0.8 
end
plarray(tn,td,-2,0.8) #play the main canon part transposed 2 semitones down pan +0.8
#end==============================

This has been an interesting project to do. I have concentrated on developing the code to generate the new flute voice, and have not in this instance added the ornamentation in the form of trills written into the flute part. Maybe you would like to have a go at developing this using the techniques used in some of my previous pieces. It should not be too difficult.

You can listen to the finished piece recorded on Sonic Pi 2 here.

You can download a zip file containing all the samples and the program and a brief README file here

The amended version can be downloaded here

I never cease to be amazed at what a versatile and creative platform Sonic Pi 2 is. I have tgended to concetrate on transcribing music and performing it accurately and with a good sound. At the other end of the spectrum people like xavriley are doing amazing stuff with live coding and creating great percussion rhythms. There is something for everyone to enjoy!

4 thoughts on “Adding a new sample based flute voice for Sonic Pi

  1. I use sonic-pi version v2.11.1 .
    I had problems as “use_sample_pack” is not working any more.
    But I could solve this by using 2 things instead:

    1.) I changed use_sample_pack to a folder variable called “samps” (by the way I have sonic-pi running on windows.)
    samps = ‘C:/Users/musician/Downloads/TelemannFlute2/TelemannFlute/flute/’

    2.) I added this variable to the line where you called the samples with sample:
    Before the code was:

    sample (sam.assoc(n)[1]),rate: (sam.assoc(n)[2]),attack: d/50,sustain: d*0.88,release: d*0.1,amp: v,pan: pan,start: st

    I changed it to:

    sample samps, (sam.assoc(n)[1]),rate: (sam.assoc(n)[2]),attack: d/50,sustain: d*0.88,release: d*0.1,amp: v,pan: pan,start: st

    • Yes this needs updating to work with new syntax following the dropping of use_sample_pack. As you have discovered, you can set a path to the samples and then call the samplename preceeded by that pathname. I have used this with other programs which I have updated, but have not updated this article (yet).

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s