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],\
        ['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],\
        ['Chorus female','Chorus','chorus-female-',3,:as4,:gs5],\
        ['Chorus male','Chorus','chorus-male-',3,:as2,:gs3],\
        ['Cor Anglais','Cor Anglais','cor_anglais-',2,:f3,:f5],\
        ['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],\
        ['Horns stc','Horns','horns-stc-rr1-',1,:e2,:e5],\
        ['Horns sus','Horns','horns-sus-',1,:e2,:e5],\
        ['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],\
        ['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],\
        ['Timpani roll','Percussion','timpani-roll-',0,:c1,:c2],\
        ['Timpani roll cresc','Percussion','timpani-roll-crsc-',0,:c1,:c2],\
        ['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|
  live_loop :t do
    sleep 0.3 #can reduce to 0.2 on Mac and Windows
    if trigger== 1
      cue :start
  load_samples path+voices[i][1],voices[i][2]
  sync :start

llist.each do |i|

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]
puts voices.length.to_s+' voices'
#setup global variables

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|
  #amend path for instrument sampledir

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|
  #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
  n=np+tp #note allowing for transposition
  if n.is_a?(Numeric) #allow frac tp or np
  if note(np)+tp < note(low) #calc adjustment for low note
  if note(np).to_i+tp > note(high) #calc adjustment for high note
    change = note(np).to_i+tp-note(high)
  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
    oc = note(n) #do in 2 stages because of alignment bug
    oc=oc/12 -1
    #find first part of sample note
    #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
      offset=[ 0,1,-1,0,1,-1,0,1,-1,0,1,-1]
    when 1
    when 2
      oc -= 1 if base == 0 #adjust if sample needs previous octave
    when 3
      snumber=[0,1,2,3,4,5,6,7,8,9,10,11] #this class has sample for every note
    #generate sample name
    #play sample with appropriate rpitch value
    sample paths,sname,rpitch: offset[base]+change+frac,sustain: 0.9*d,release: d,pan: pan

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 do |n,d|
    pl(n,d,offsetclass,tp,pan) if ![nil,:r,:rest].include? n#allow for rests
    sleep d

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
#now play the round, each part in a thread, spaced by duration of 1st line repeated (4*c)
in_thread do
sleep 4*c
in_thread do
  plarray(notes,durations,'1st Violins piz',12,0.6)
sleep 4*c
in_thread do
  plarray(notes,durations,'Violas piz',0,-0.6)
sleep 4*c
in_thread do
  plarray(notes,durations,'Basses stc',-12,0.4)
sleep 4*c
in_thread do
sleep 4*c
in_thread do
sleep 4*c
in_thread do
  plarray(notes,durations,'Grand Piano f',-24)
sleep 4*c
in_thread do
  plarray(notes,durations,'Trombones stc',-12,0.8)
sleep 4*c
in_thread do
sleep 4*c
in_thread do
  plarray(notes,durations,'2nd Violins piz',0,0.7)
sleep 4*c
in_thread do
sleep 4*c
in_thread do
sleep 4*c
in_thread do
  plarray(notes,durations,'Tuba stc',-12,-0.7)
sleep 4*c
in_thread do
  plarray(notes,durations,'Bass Clarinet',-12,0.5)
sleep 4*c
in_thread do
  plarray(notes,durations,'Bass Trombone',-24,-0.6)
sleep 4*c
in_thread do
  plarray(notes,durations,'Horns stc',0,-0.9)
sleep 4*c
in_thread do
  plarray(notes,durations,'Trumpets stc',12,0.7)
sleep 4*c
in_thread do
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)

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


4 thoughts on “Sonatina Symphonic Orchestra revisited to give 55 sample voices for Sonic Pi

  1. Really useful (especially after listening to the Two Miniatures). Still fuzzy on several details, but these two posts help quite a bit.
    Maybe a silly question: is there a `.zip()` method in notes? Was digging into the way you had “packaged” lines as arrays and thought for a second that you might be using a compressed file. But it sounds like this is a way to get access to data from a parallel array or some such. Was thinking about using two dimensional arrays but this might work better.

    Thanks for the code and the inspiration!

    • Yes I use the .zip method frequently for notes also. eg

      define :pl do |notes,durations|
 do |n,d|
              play n,d,sustaion: d*0.9,release: d*0.1
              sleep d

      followed by


      where notes and durations are corresponding lists of notes and their durations

      Unlike with playing samples,you don’t have to deal with chords [n1,n2] entries explicitly with notes as the play command does that for you already. Also play knows about rests so you don;t have to deal with these explicitly either (:r, :rest, nil)

      You can add other parameter, eg vol, pan, tp (transpose) if you wish
      I have also zipped three lists together before now notes,durations,volumes, although I usually control the volume using the with_fx :level command now,volumes.each do |n,d,v|

    • There are several other sample orchestras that can be used. I’ve also utilised samples from the Virtual Playing Orchestra However, I developed my code to link into the structure of the SSO automatically, and it would need rewritten code to do so with other orchestra layouts. It can be easier just to extract a selected instrument’s samples and use those, or even to work with just one sample for a given instrument.
      OK about simpler examples. I’ll bear that in mind!

Leave a Reply

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

You are commenting using your 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