Sonic Pi Grand Piano voice revisited

Following on from a post detailing how I have revised and streamlined the generation of a single sample based voice for Sonic Pi, I have taken a fresh look at the Grand Piano voice which utilises multiple samples spaced apart by 3 semitones. I have revised this in the same way, giving a much more streamlined program, which is shorter and more automated in the way in which the voice is generated. The same technique can easily be applied to other voices which have samples arranged 3 semitones apart, as is the case for many of the orchestral instruments in the Sonatina Symphonic Orchestra repository of samples, which can be freely used under a CCL licence. The resulting program described in this article is much more efficient that the first version here.
The program is fully documented, and I illustrate the use of the finished voice by means of code to generate contrary scales. This also utilises a technique to allow for dynamic shaping of the scales with crescendoes and diminuendos as they play.

The main change in the streamlined voice is to move away from a symbol based representation of the note and sample based information to one that is numerically based. This makes it much easier to deal with say :cs4 and :db4 both of which have the same numeric representation 61.

The program starts with the user selection the location of the piano sample folder, and choosing whether to use the softer piano samples or the louder forte samples.

#user adjustable variables======================
#use_sample_pack '/Users/rbn/Desktop/samples/GrandPiano' #select and adjust as necessary
use_sample_pack '/home/pi/samples/GrandPiano'
pianotype= "piano_p_" #comment out ONE of these two choices for piano or forte piano samples
#pianotype="piano_f_"
#end of user adjustable variables===============

Two variables load_flag and s are set up. The former allows time for the samples to be loaded in on the first run, the latter is used as a scaling factor to adjust the tempo when playing notes.

load_flag=0 #used to allow time for samples to load
s=0 #scale factor for tempo setting set as a global variable

There follows a function setbpm to adjust the bpm to be used when playing with the samples. We need our own, as we have our own equivalent to the synth play command, but for samples, and this will not respond to the build in use_bpm command.
A further function ntosym converts from a note numeric value to the corresponding symbol eg 48=>:c3. The inverse is available with the buiilt in note(:c3) => 48

define :setbpm do |n| #sets value of scale factor s according to bpm
 s = (1.0 / 8) *(60.0/n.to_f)
end

define :ntosym do |n| #converts note number to symbol
 note=n % 12
 octave = n / 12 - 1
 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

The samples for both piano and forte samples range from :c1 up to :c8 every 3 semitones. We check whether the samples are already loaded from a previous run by checking whether the c8 sample is loaded. If so the load_flag is changed from 0 to 1
Now a loop traverses the range of sample note values note(:c1) to note(:c8) in steps of 3 and for each value preloads the samples into memory. If the load_flag is not set to 1 the program waits for 8 seconds to allows the samples to be read in.

#range :c1 to :c8
if sample_loaded? (pianotype+"c8").intern then #check if loaded and adjust load_flag
 load_flag = 1
end

slist=[] #array to hold sample info
(note(:c1)..note(:c8)).step 3 do |i| #samples spaced 3 semitones apart
 load_sample (pianotype+ntosym(i).to_s).to_sym #load current sammple
end
if load_flag==0 then #if load required then sleep
 sleep 7 #may need to increase on older Pi models
end

The pl function is at the heart of the program, and is responsible for playing notes using the appropriate sample. To do this it utilises the pitch_ratio function, which is introduced in Sonic Pi version 2.5dev. So that the program can be utilised in earlier versions of Sonic Pi, the definition is reproduced in this program. If you have a version where it is built in you can comment out this definition.

uncomment do #built into 2.5dev For earlier versions leave uncommented, otherwise comment
  define :pitch_ratio do |n|
    return 2**(n.to_f/12)
  end
end

The pl function first works out which sample should be used to play a particular note. It works out the offset relative to the note sample which will either be -1, 0 or 1 and then plays the appropriate sample, calculating the rate: using the pitch_ratio function. The sustain: and release: times are calculated in terms of the note duration required.

define :pl do |n,d=0.2,pan=0,v=0.8| #play a note n for duration d
 #work out offset to get sample to play
 offset=note(n)%3 #sample every third semitone
 if offset==2 then
 offset=-1 #final offset of note from correct sample will be -1, 0 or +1
 end
 sample (pianotype+ntosym(note(n)-offset).to_s).to_sym,rate: pitch_ratio(offset),sustain: 0.95*d,release: 0.05*d,pan: pan,amp: v,start: 0
end

A couple of examples will illustrate the process:
To play the note :gs5, the note value note(:gs5) is 80
80%3 =2 so the offset is set to -1
The sample chosen, pianotype+ntosym(note(n)-offset).to_s will be :piano_p_a5 and the rate: will be pitch_ratio(-1) = 0.94387. In other words the :a5 sample will be played slower than normal and will sound as a :gs5
To play the note :e6 the note value note(:e6) is 88
88%3 =1 so the offset is set to 1
The sample chosen, pianotype+ntosym(note(n)-offset).to_s will be :piano_p_ds6 and the rate: will be pitch_ratio(1) = 1.05946. In other words the :ds6 sample will be played faster than normal and will sound as an :e6

define :tr do |nv,shift| # for transpose if required
 return ntosym(note(nv)+shift)
end

The tr function can be used instead of with_transpose if you need to transpose notes.
The plarray function takes a list of notes and a corresponding list of their durations and passes them to the pl function to be played. It is equivalent to the play_pattern_timed command for synths, although it also adjusts the sustain and release times of the notes in proportion to their durations.

define :plarray do |nt,dur,shift=0,vol=0.6,pan=0| #play associated array of notes and durations
 nt.zip(dur).each do |n,d|
 if n != :r then
 #puts n
 pl(tr(n,shift),d*s,pan)
 end
 sleep d*s
 end
end

In order to make it easier to enter note durations, these can be expressed in terms of the variables dsq,sq,q,c…..md which set the relative durations of demisemiquavers, semiquavers,quavers,crotchets….dotted minims. When multiplied by the s variable set by the setbpm function they give the duration of notes for the specified tempo. Other variables can be added, eg cdd=14 (double dotted crotchet), b=48 (breve). Variables are also set up to contain amplitude levels for different dynamic settings.

#relative note durations
dsq = 1
sq = 2
q = 4
qd = 6
c = 8
cd = 12
m = 16
md = 24
#dynamic settings
p=0.15
mp=0.2
mf=0.4
f=0.8
ff=1.6

The ct function is used to adjust the amplitude level of notes played within a
with_fx :level  do |x|….end loop. It enables you to make changes in level, over a given time period, and thus to introduce dynamic variation in the form of crescendos and diminuendos.

define :ct do |ptr,lev,slid,slp| #used to set dynamic levels
 #parameters ptr to control,lev required amp,
 #slid slide time to get there,slp sleep time before next change.
 control ptr,amp: lev,amp_slide: slid*s #adjusted with *s for tempo
 sleep slp*s
end

Its operation is illustrated in the function contrary which is defined to play contrary scales. It uses the scale function to define the notes for a three octave major scale, and defines a corresponding list of durations for the notes in the scale.
It plays the scale ascending, and then plays it descending (minus the first note), using a duration list which is one entry shorter.
These two scales are played in a thread at the same time as a descending and ascending pair of scales are played whose top note is the same as the starting note of first pair of scales.
The ct function is used to set up a crescendo followed by a diminuendo while the scales play.

define :contrary do |nt| #define 3 octave contrary scale function
 down=([q]*6+[c])*3 #down
 up=[c]+down #up durations 26*q total
 down[-1]=m #having set up, now adjust last duration in down. down duration is 26*q
 with_fx :level,amp: p do |x| #used to adjust dynamic level
 in_thread do #set up crescendo and decrescendo as scales play
 ct(x,ff,q*26,q*26) #cresc over 26 quavers duration to ff, wait 26 quavers before the next command
 ct(x,p,q*26,q*26) #decresc over 26 quavers duration to mp, wait 26 quavers before the next command
 end
 in_thread do
 plarray(scale(nt,:major,num_octaves: 3),up) #right hand 3 octaves up
 plarray(scale(nt,:major,num_octaves: 3).reverse[1..-1],down) # then 3 octaves down
 end
 plarray(scale(note(nt)-36,:major,num_octaves: 3).reverse,up) #left hand 3 octaves down
 plarray(scale(note(nt)-36,:major,num_octaves: 3)[1..-1],down) #then 3 octaves up
 end
end

The program is heard in operation playing the contrary scales with the starting note changing from :c4 to :c5 over 12 semitone steps.

setbpm(200) #setpbm sets s scale factor for note durations
lasttime_flag=false
note(:c4).upto(note(:c5)) do |x| #do an octave range of contrary scales
 puts ntosym(x)
 contrary(x)

The full listing is shown below

#Setup of sample based Piano Voice for Sonic Pi. Resultant voice illustrated playing contrary scales
#written by Robin Newman, March 2015
#covers range :c1 up to :c8
#samples from Sonatina Symphonic Orchestra CCL licence

#set_sched_ahead_time! 4 #may need to uncomment on Mac if Garbage Collection casues breaks in play
#user adjustable variables======================
#use_sample_pack '/Users/rbn/Desktop/samples/GrandPiano' #select and adjust as necessary
use_sample_pack '/home/pi/samples/GrandPiano'
pianotype= "piano_p_" #comment out ONE of these two choices for piano or forte piano samples
#pianotype="piano_f_"
#end of user adjustable variables===============

load_flag=0 #used to allow time for samples to load
s=0 #scale factor for tempo setting set as a global variable

define :setbpm do |n| #sets value of scale factor s according to bpm
  s = (1.0 / 8) *(60.0/n.to_f)
end

define :ntosym do |n| #converts note number to symbol
  note=n % 12
  octave = n / 12 - 1
  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

#range :ds1 to :c8
if sample_loaded? (pianotype+"c8").intern then #check if loaded and adjust load_flag
  load_flag = 1
end

slist=[] #array to hold sample info
(note(:c1)..note(:c8)).step 3 do |i| #samples spaced 3 semitones apart
  load_sample (pianotype+ntosym(i).to_s).to_sym #load current sammple
end
if load_flag==0 then #if load required then sleep
  sleep 7 #may need to increase on older Pi models
end

uncomment do #built into 2.5dev For earler versions leave uncommented, otherwise comment
  define :pitch_ratio do |n|
    return 2**(n.to_f/12)
  end
end

define :pl do |n,d=0.2,pan=0,v=0.8| #play a note n for duratio n
  #work out offset to get sample to play
  offset=note(n)%3 #sample every third semitone
  if offset==2 then
    offset=-1 #final offset of note from correct sample will be -1, 0 or +1
  end
  sample (pianotype+ntosym(note(n)-offset).to_s).to_sym,rate: pitch_ratio(offset),sustain: 0.95*d,release: 0.05*d,pan: pan,amp: v,start: 0
end

define :tr do |nv,shift| # for transpose if required
  return ntosym(note(nv)+shift)
end

define :plarray do |nt,dur,shift=0,vol=0.6,pan=0| #play associated array of notes and durations
  nt.zip(dur).each do |n,d|
    if n != :r then
      #puts n
      pl(tr(n,shift),d*s,pan)
    end
    sleep d*s
  end
end
#relative note durations
dsq = 1
sq = 2
q = 4
qd = 6
c = 8
cd = 12
m = 16
md = 24
#dynamic settings
p=0.15
mp=0.2
mf=0.4
f=0.8
ff=1.6

define :ct do |ptr,lev,slid,slp| #used to set dynamic levels
  #parameters ptr to control,lev required amp,
  #slid slide time to get there,slp sleep time before next change.
  control ptr,amp: lev,amp_slide: slid*s #adjusted with *s for tempo
  sleep slp*s
end
#=============end of voice definition bits=============

define :contrary do |nt| #define 3 octave contrary scale function
  down=([q]*6+[c])*3 #down
  up=[c]+down #up durations 26*q total
    down[-1]=m #having set up, now adjust last duration in down. down duration is 26*q
  with_fx :level,amp: p do |x| #used to adjust dynamic level
    in_thread do #set up crescendo and decrescendo as scales play
      ct(x,ff,q*26,q*26) #cresc over 26 quavers duration to ff, wait 26 quavers before the next command
      ct(x,p,q*26,q*26) #decresc over 26 quavers duration to mp, wait 26 quavers before the next command
    end
    in_thread do
      plarray(scale(nt,:major,num_octaves: 3),up) #right hand 3 octaves up
      plarray(scale(nt,:major,num_octaves: 3).reverse[1..-1],down) # then 3 octaves down
    end
    plarray(scale(note(nt)-36,:major,num_octaves: 3).reverse,up) #left hand 3 octaves down
    plarray(scale(note(nt)-36,:major,num_octaves: 3)[1..-1],down) #then 3 octaves up
  end
end

#===========play the scales============
setbpm(200) #setpbm sets s scale factor for note durations
lasttime_flag=false
note(:c4).upto(note(:c5)) do |x| #do an octave range of contrary scales
  puts ntosym(x)
  contrary(x)
end

You can download the file here

You can listen to the voice in action here

You can download the samples required here

Advertisements

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