Deux Miniatures coded on Sonic Pi

Following the last post in which I detailed how to set up the Sonatina Symphonic Orchestra for use with Sonic Pi, this post contains details of a pice that I have just coded with Sonic Pi which utilises the SSO library to produce the required instrument voices. The pice was written by the composer Victor Kalinnikov in 1986 and was originally scored for 2 Violins, Viola and Cello, but here I am utilising an Oboe for Violin 1 and samples for violins (plural) violas and Basses  for the other three parts,as I think the texture of the sounds produced by Sonic Pi suit this combination better. As described in the last post, the voices are set up to be played by the plarray function. However I have made several changes. First, to save space, only the instrument voices required are loaded, rather than the larger number used previously. Secondly, both the viola and bass parts need to be able to play chords. Logically one would alter the pl function which played a single note to accommodate this, but as this function is quite complex it is practically easier to make the alteration in the plarray function. This is done by testing each entry in the notes list and seeing it will respond to the .each Ruby method, using the test if n.respond_to?(:each)
If this is true, then it means that n (which is a member of the list of notes being fed to the plarray) is itself a list, and so each note in this list is sent to the pl function without any sleep commands in between, thus causing them to be played at the same time.
The second alteration I made to the pl and plarray routines was to expose the envelope sustain and decay fractions used when the sample is played, which can be used to alter the overall sound. Previously I had sustain: 0.9*d, release: d which suited the Frere Jaques example, but now I have sustain: s*d,release: r*d and s and r are included in the parameter list for each function, with default values set at 0.9 and 0.1 respectively. In the event I used the default values for this piece.
The third change made was because of the length limitations currently in force in Sonic Pi. There is a maximum buffer length which can be sent to the server, and if this is exceeded no sound results. On-going work in taking place to over come this, but at present it is possible to split programs over more than one buffer and link the parts using Sonic Pi’s cue and sync commands. Functions defined in one buffer are available for other buffers in Sonic Pi. So if the functions are defined in the first buffer used, then you only have to duplicate definitions of any variables required in subsequent parts for this to work. In fact, once the first section has been run once, the other two can be used independently to play each of the two movements are required, by commenting out the sync commands at the beginning of each of them. To play them linked together thee parts should be run in reverse order, 3,2 then 1.

There is more that can be done to the prgram, for example to add expression in the way of changes in loudness p,f,ff etc and also to add a couple of rits in the score. I may do this ;ater, but it doesn’t sound bad as it is at present.

If you are interested in seeing the music, it can be downloaded from http://imslp.org/wiki/2_Miniatures_for_String_Quartet_(Kalinnikov,_Viktor)

The Deux Miniatures can be heard on soundcloud.com here

The amended pl and plarray functions are shown below, and the three parts of the program can be downloaded here.

#define routine to play sample
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
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
Advertisements

Sonatina Symphonic Orchestra revisited to give 55 sample voices for Sonic Pi

I have visited the Sonatina Symphonic Library several times over the last couple of years, but following recent comments on the Sonic-Pi Google Group I decided to do so again, and to make an effort to utilise the majority of the instrument voices available in the library with minimum setup effort required by the user. In the event I have managed to develop a program which enables you to utilise 55 different voices, merely by choosing the name of the voice that you wish to use.

The library supports the SFZ format, but rather than try and parse the relevant files, I decided to go directly to the samples folder and see what was necessary to select and use the various samples stored there. I wanted to be able to used the unzipped Library Folder “as is” with no alterations required to its structure.

Looking at the structure, the samples are all contained in a main folder “Samples” which contains subfolders for each instrument class, eg Violin, (solo), Violins, Flutes etc. Inside these folders are individual samples for the instruments concerned. I some cases there are separate ranges for samples for pizzicato, sustained or staccato renditions signified by
-piz- -sus- and -stc- additions to the sample names. The piz and stc classes also seem to have two sets of samples differentiated by an rr1 or rr2 suffix. These sound similar but appear to differ slightly in pitch. In the event I used the rr1 samples which seem to play at the same pitch as the note specified in the note name, eg f, f#, a  etc.

Each instrument voice range is covered by a selection of samples, usually spaced apart by 3 semitones. There appear to be 4 types. Type 0 use samples based on c, d#, f# and a. Type 1 based on c#, e, g and a#. Type 2 based on b,d,f and g# and Type 3 have individual samples for all notes in the voice range. The idea for Types 0-2 is that the missing notes can be played by using an adjacent pitched sample played slightly slower or faster, so each sample is responsible for playing at three different pitches; at its recorded pitch and one semitone higher and lower than this pitch.

From the analysis of the samplesI decided that six pieces of information would be necessary for each instrument voice in order for the program to work out which sample to play for any give note.
1 A name by which the voice could be specified, eg 1st Violins piz, Flute, Horns stc etc
2 The name of the parent folder containing the samples, eg Violins, Flute, Horms
3 the sample prefix containing the common start of the sample names
eg violins-piz-rr1-, tuba-stc-, piccolo-
4 the Type to which the voice belongs 03 as discussed above
5 The lowest note for the voice with sample support eg :c2
6 The highest note for the voice with sample support eg :c6

I built an associative array named voices containing 55 entries with the details for each of the voices supported, which can be seen near the beginning of the program. This together with a path variable containing the path of the Sonatina Samples folder would enable the loading information for a specified voice set of samples to be accessed.

#path to library samples folder (including trailing /)
path='~/Desktop/Sonatina Symphonic Orchestra/Samples/'

#create array of instrument details
voices=[['1st Violins piz','1st Violins','1st-violins-piz-rr1-',1,:g3,:b6],\
        ['1st Violins sus','1st Violins','1st-violins-sus-',1,:g3,:b6],\
        ['1st Violins stc','1st Violins','1st-violins-stc-rr1-',1,:g3,:b6],\
        ['2nd Violins piz','2nd Violins','2nd-violins-piz-rr1-',1,:g3,:b6],\
        ['2nd Violins sus','2nd Violins','2nd-violins-sus-',1,:g3,:b6],\
        ['2nd Violins stc','2nd Violins','2nd-violins-stc-rr1-',1,:g3,:b6],\
        ['Alto Flute','Alto Flute','alto_flute-',1,:g3,:g6],\
        ['Bass Clarinet','Bass Clarinet','bass_clarinet-',2,:d2,:d5],\
        ['Bass Trombone','Bass Trombone','bass_trombone-',1,:e1,:g4],\
        ['Basses piz','Basses','basses-piz-rr1-',0,:c1,:c4],\
        ['Basses sus','Basses','basses-sus-',0,:c1,:c4],\
        ['Basses stc','Basses','basses-stc-rr1-',0,:c1,:c4],\
        ['Bassoon','Bassoon','bassoon-',1,:as1,:d5],\
        ['Bassoons','Bassoons','bassoons-sus-',1,:as1,:d5],\
        ['Celli piz','Celli','celli-piz-rr1-',0,:a2,:bb5],\
        ['Celli sus','Celli','celli-sus-',0,:a2,:bb5],\
        ['Celli stc','Celli','celli-stc-rr1-',0,:a2,:bb5],\
        ['Cello','Cello','cello-',0,:a2,:bb5],\
        ['Chorus female','Chorus','chorus-female-',3,:as4,:gs5],\
        ['Chorus male','Chorus','chorus-male-',3,:as2,:gs3],\
        ['Clarinet','Clarinet','clarinet-',2,:d3,:d6],\
        ['Clarinets','Clarinets','clarinets-sus-',2,:d3,:d6],\
        ['Contrabassoon','Contrabassoon','contrabassoon-',1,:as0,:as3],\
        ['Cor Anglais','Cor Anglais','cor_anglais-',2,:f3,:f5],\
        ['Flute','Flute','flute-',0,:b3,:a4],\
        ['Flutes sus','Flutes','flutes-sus-',0,:c3,:bb5],\
        ['Flutes stc','Flutes','flutes-stc-rr1-',0,:c3,:bb5],\
        ['Grand Piano p','Grand Piano','piano-p-',0,:b0,:cs8],\
        ['Grand Piano f','Grand Piano','piano-f-',0,:b0,:cs8],\
        ['Harp','Harp','harp-',0,:c2,:c7],\
        ['Horn','Horn','horn-',1,:e2,:e5],\
        ['Horns stc','Horns','horns-stc-rr1-',1,:e2,:e5],\
        ['Horns sus','Horns','horns-sus-',1,:e2,:e5],\
        ['Oboe','Oboe','oboe-',1,:as3,:c6],\
        ['Oboes','Oboes','oboes-sus-',1,:as3,:c6],\
        ['Xylophone','Percussion','xylophone-',1,:a2,:b5],\
        ['Piccolo','Piccolo','piccolo-',0,:b3,:g6],\
        ['Tenor Trombone','Tenor Trombone','tenor_trombone-',1,:ds2,:b4],\
        ['Trombones sus','Trombones','trombones-sus-',1,:ds2,:b4],\
        ['Trombones stc','Trombones','trombones-stc-rr1-',1,:ds2,:b4],\
        ['Trumpet','Trumpet','trumpet-',1,:e3,:f6],\
        ['Trumpets sus','Trumpets','trumpets-sus-',1,:e3,:f6],\
        ['Trumpets stc','Trumpets','trumpets-stc-rr1-',1,:e3,:f6],\
        ['Tuba sus','Tuba','tuba-sus-',1,:e1,:d4],\
        ['Tuba stc','Tuba','tuba-stc-rr1-',1,:e1,:d4],\
        ['Violas piz','Violas','violas-piz-rr1-',0,:c3,:c6],\
        ['Violas sus','Violas','violas-sus-',0,:c3,:c6],\
        ['Violin','Violin','violin-',1,:g3,:d7],\
        ['Timpani roll','Percussion','timpani-roll-',0,:c1,:c2],\
        ['Timpani roll cresc','Percussion','timpani-roll-crsc-',0,:c1,:c2],\
        ['Glockenspiel','Percussion','glockenspiel-',0,:c3,:c6],\
        ['Timpani f lh','Percussion','timpani-f-lh-',0,:c1,:c2],\
        ['Timpani f rh','Percussion','timpani-f-rh-',0,:c1,:c2],\
        ['Timpani p lh','Percussion','timpani-p-lh-',0,:c1,:c2],\
        ['Timpani p rh','Percussion','timpani-p-rh-',0,:c1,:c2]]

The program has several sections. After the initial setup of the path and voices array the next section deals with preloading the samples to be used. Particularly on a Raspberry Pi which has both a slower processor but more importantly a longer access access time for data files because of using an SD card for storage, this will be essential if the samples are going to be accessible to the program in a timely fashion as they are required. This turned out to be quite problematical, but eventually I came up with a solution that seems to work on Pi, Mac and Windows, where I use a live_loop to control the loading of each section of samples giving cues to the loading section to proceed as each section completes. Without this I found that the program would time out and halt. The sleep time at the beginning of the live_loop is fairly critical. I find a minimum value of 0.3 seconds on a Pi2 or Pi 3  and 0.2 seconds on a Mac or Windows PC is desirable. For the 19 voices employed in the Frere Jaques demo the load time is about 52.4 seconds on a Pi2 or Pi3, but around 15.4 seconds on my Mac. On a subsequent run it checks out the samples in about 5.9 seconds on a Pi 3 or 3.8 seconds on my Mac, or you can comment out the load code and reduce this to 0. A list of the voice numbers required is used to control the loading, the numbers being the position within the voices array.

llist=[50,0,45,11,33,35,28,39,36,3,29,20,44,7,8,31,42,22,52] #voicenumbers used

define :load do |i|
  trigger=0
  live_loop :t do
    sleep 0.3 #can reduce to 0.2 on Mac and Windows
    if trigger== 1
      cue :start
      stop
    end
  end
  load_samples path+voices[i][1],voices[i][2]
  trigger=1
  sync :start
end

llist.each do |i|
  load(i)
end

The second section merely prints out a list of the available voices, together with their index position within the array. Following that, some variables are declared so that they can be used globally, rather than defining them inside functions.

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=''

The third sections contains three functions which are at the heart of the program.
The first function  setup extracts the information from the voices array for a specified instrument name and path

#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

The second function pl plays a single note for a specified instrument. It has four parameters, the note np which can be symbolic or numerical, including fractional parts, eg :c3, 72 or 72.1 The duration d of the note, any transpose (tp) to be applied which defaults to 0, and the pan position of the note. It first calls the setup function to extract information about the voice. To make the system more flexible I allow notes to be generated outside the normally defined range of a voice. The user can specify separate values (common to all voices) for the maximum excursion below the the lowest note or above the highest note in the range. Such notes will be generated from the lowest and highest samples available to that voice. Be aware of two things. They may not sound all that good, depending upon the specific voice, and secondly they may produce notes which cannot actually be produced by the “real” instrument concerned. The change variable will hold the correction to be applied to the rpitch: parameter when the sample is played to produce these out of range notes. The frac variable is used when a non-integer note value is specified. The sample to use is calculated from the integer part of the note, and the frac value is again applied as a correction to the rpitch: parameter when the note is played. If a note is outside the range allowed for in the extra range, then the program will print this information, and will play a rest instead for the duration of the note. This will enable any piece playing to continue, without causing a terminating error.
The actual sample to be used is calculated by splitting the note information into a note number base 0..11 its position in the scale list slookup c,c#….a#,b together with an octave number oc. The calculation of the octave part oc is done in two stages because of a “bug” in the alignment function when you carry it out in 1 stage.
Now depending upon the instrument type number 0 to 3 one of four snumber lists are chosen which contains the number of the actual sample name to be used in the slookup list, bearing in mind that each sample may play any of three different note names if the type number is 0..2 An associated offset list contains the rpitch: information used to adjust the sample rate as necessary.  Note also that for types 0 and 2 an octave adjustment has to be made for the last and first notes in the scale as a sample is “borrowed” from the adjacent octave in these cases.
Once the sample note name and rpitch values have been calculated the sample name is assembled sname=sampleprefix+(slookup[snumber[base]]).to_s+oc.to_s
and the sample is played
sample paths,sname,rpitch: offset[base]+change+frac,sustain: 0.9*d,release: d,pan: pan
The user can alter the envelope parameters to taste, but these are applied globally to all voices

#define routine to play sample
define :pl do |np,d,inst,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 < -3 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: 0.9*d,release: d,pan: pan
  end
end

The final function of the three, plarray enables you to process corresponding lists of notes and durations to a given instrument voice, and it also accepts transpose (tp with a 0 default) and pan (with zero default) parameters. The function traverses the two liked lists using the Ruby construct

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

To illustrate the use of the voices, the program creates a notes and durations list for Frere Jaques and then uses 19 calls to the plarray function, each inside a separate thread and separated from its neighbours by a sleep command of value equal to the time to play the first repeated line of the tune. This results in a 19 part round being played.

#set up notes and duration for Frere Jaques
notes=[:c4,:d4,:e4,:c4,:c4,:d4,:e4,:c4,:e4,:f4,:g4,:e4,:f4,:g4,\
       :g4,:a4,:g4,:f4,:e4,:c4,:g4,:a4,:g4,:f4,:e4,:c4,:c4,:g3,:c4,:c4,:g3,:c4]
sq=0.15
q=2*sq
c=2*q
durations=[q,q,q,q,q,q,q,q,q,q,c,q,q,c,sq,sq,sq,sq,q,q,sq,sq,sq,sq,q,q,q,q,c,q,q,c]
#now play the round, each part in a thread, spaced by duration of 1st line repeated (4*c)
in_thread do
  plarray(notes,durations,'Glockenspiel')
end
sleep 4*c
in_thread do
  plarray(notes,durations,'1st Violins piz',12,0.6)
end
sleep 4*c
in_thread do
  plarray(notes,durations,'Violas piz',0,-0.6)
end
sleep 4*c
in_thread do
  plarray(notes,durations,'Basses stc',-12,0.4)
end
sleep 4*c
in_thread do
  plarray(notes,durations,'Oboe',12)
end
sleep 4*c
in_thread do
  plarray(notes,durations,'Xylophone',12,-0.5)
end
sleep 4*c
in_thread do
  plarray(notes,durations,'Grand Piano f',-24)
end
sleep 4*c
in_thread do
  plarray(notes,durations,'Trombones stc',-12,0.8)
end
sleep 4*c
in_thread do
  plarray(notes,durations,'Piccolo',12,-0.6)
end
sleep 4*c
in_thread do
  plarray(notes,durations,'2nd Violins piz',0,0.7)
end
sleep 4*c
in_thread do
  plarray(notes,durations,'Harp',12,-0.6)
end
sleep 4*c
in_thread do
  plarray(notes,durations,'Clarinet',0,0.6)
end
sleep 4*c
in_thread do
  plarray(notes,durations,'Tuba stc',-12,-0.7)
end
sleep 4*c
in_thread do
  plarray(notes,durations,'Bass Clarinet',-12,0.5)
end
sleep 4*c
in_thread do
  plarray(notes,durations,'Bass Trombone',-24,-0.6)
end
sleep 4*c
in_thread do
  plarray(notes,durations,'Horns stc',0,-0.9)
end
sleep 4*c
in_thread do
  plarray(notes,durations,'Trumpets stc',12,0.7)
end
sleep 4*c
in_thread do
  plarray(notes,durations,'Contrabassoon',-24,0.7)
end
sleep 4*c
in_thread do
  notes1=notes[0..-6]+[:g4,:c4,:c4,:g4,:c4]#adjust :g3 to :g4
  plarray(notes1,durations,'Timpani f rh',-36,0.7)
end

For other uses, the program can be reduced in length, but only including the voices required in the initial voices array, and adjusting the load list accordingly.

One or two final points.

The Sonatina Symphonic Library can be downloaded here
It is over 460Mb for the zip file, and when you expand it over 500Mb so make sure you have enough room. It will also take some time to expand on a Raspberry Pi.
The best place to locate the library is in the logon user’s home directory. This is /home/pi for the standard pi user on a Raspberry Pi, /Users/nnn/ on a Mac where nnn is the logon user, and c:\Users\<username> on Windows. In these locations the path setup in the program listing will work without alteration on all three platforms.

The program requires features added to SonicPi er 2.10dev in relation to the way that samples are accessed and will NOT work with earlier versions. There is the possibility that changes in 2.10dev may affect the operation of the program before version 2.10 is released, although I doubt it.

The full program listing can be downloaded here