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

Amendements to TVRouter article of July 2013

One of the first articles I wrote on this site concerned using a Raspberry Pi to act as a wifi to ethernet router to enable my (then) TV set to connect to the internet via my main WiFi router. The tv set had an ethernet port, but it was not convenient to wire it to the router by cable. It turned out to be the second most popular post I have written, and has attracted many comments since it was first written in July 2013. Since then the Raspian distro has been improved and altered quite a bit.
Recently I received a comment from a reader who was having problems getting the project to work on the latest distro, and since I had not looked at it for some time, I decided to try a fresh install (on a Pi2) using the latest distro available. Sure enough I found that I too had problems. It appeared that the wlan0 interface did not initialise correctly.
I did some research on the internet and came across an interesting article at http://raspberrypi.stackexchange.com/questions/9678/static-ip-failing-for-wlan0 which appeared to be very relevant. I tried out the wlan0-restart script which was one of the suggested solutions and found that it cured the problem.

I reproduce the script here

 #!/bin/sh
#
#

### BEGIN INIT INFO
# Provides:          wlan0-restart
# Required-Start:    $network
# Required-Stop:     $network
# Should-Start:
# Should-Stop:
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Restarts wlan0 interface
# Description:       Restarts wlan0 interface to fix problem with static address in WiFi AP
# X-Start-Before:    hostapd isc-dhcp-server
### END INIT INFO

PATH=/sbin:/usr/sbin:$PATH

do_start() {
  ifdown wlan0
  ifup wlan0
}

case "$1" in
  start)
    do_start
    ;;
  restart|reload|force-reload)
    echo "Error: argument '$1' not supported" >&2
    exit 3
    ;;
  stop)
    ;;
  status)
    ;;
  *)
    echo "Usage: $0 start|stop" >&2
    exit 3
    ;;
esac
exit 0

To set this up, use the nano editor and create the file
sudo nano /etc/init.d/wlan0-restart
with the content above.
You should then set it to be executable
sudo chmod 755 /etc/init.d/wlan0-restart
It is important that this is executed BEFORE the isc-dhcp server is started
so to ensure that this is the case, if you have already installed isc-dhcp-server as per the original article, you should first disable it by typing:
sudo update-rc.d isc-dhcp-server remove
then set up the new wlan0-restart script with
sudo update-rc.d wlan0-restart defaults
before setting up the isc-dhcp server again with
sudo update-rc.d isc-dhcp-server defaults

If you follow the quick install method using the tvrouter.zip file, you can setup the wlan0-restart script at point 8a, BEFORE installing the isc-dhcp-server, then you will not have to remove it with update-rc.d and reset it again, but merely use update-rc.d to setup the wlan0-reset script.

Hopefully this should enable the project to work with the latest Raspian distribution again as it is currently doing on my Pi2. You will see some error messages on the screen when the script is invoked, but you can ignore these as it does work.! When the boot process has completed you should see two ip addresses listed on the screen. The first will be the ethernet address (192.168.2.1) and the second the wlan0 address (192.168.1.98)
All being well, you should be able to log on and ping your mail wireless router (probably on 192.168.1.1) and your tv (or laptop) connected to the ethernet port which will probably have been served the ip address 192.168.2.10. Remember if you use a different address for the wlan0 port you will have to rebuild the iptables as discussed in the original article.

Two final points. First, the quick install does NOT point out that you SHOULD alter the interfaces file once you have installed it to reflect your own ssid and password

Secondly, many people have asked me if it is possible to use the project with dlna devices. to access media content. Unfortunately it was not designed with this in mind, and dlna will not easily work across a router (which is what this project is!) as it is designed for use on a single local network, relying on broadcast signals. I have read many articles looking at this problem but have not seen a satisfactory solution to this yet, so don’t ask me! Of course if you know how it can be done, I and many others would love to know.

 

Sonic Pi: streamlined sample voice creation, with a medley of 26 rounds!

Over the last fortnight I have been doing further work on generating sample based voices for Sonic Pi. This arose following the recent Raspberry Pi Birthday meet in Cambridge where I ran a workshop on Sonic Pi and gave a talk on creating sample voices. This got me thinking on the subject again, and a revisited the ideas that I had used previously. Although they worked, it was quite tedious and laborious to set up sample based voices. It soon became apparent that to create sample voices based on a single sample it was possible to do this in a much more streamlined manner. The result was the program attached to this post, which illustrates the process using two of the built in samples in Sonic Pi.
A catalyst to the process was the introduction of a new function in Sonic Pi 2.5dev by Sam Aaron which he calls pitch_ratio This is built upon the ratio of frequencies between two notes which are a semitone apart, which is the rather frightening number, the twelfth root of 2, which is expressed in Ruby as 2**(1.0/12) or 2 raised to the power of 1/12 {Note the 1.0 to get floating point division rather than integer division}.
I had used this factor already in my programs in calculating the rates to play samples to get different notes, but Sam’s function made it a bit more versatile. The definition is:

  define :pitch_ratio do |n|
    return 2**(n.to_f/12)
  end

Basically it tells you that if you have two midi notes differing in value by say n semitones then the ratio between the frequency of the second note and the first note is pitch_ration(n) to 1. So what? This seems very esoteric and not very useful at first sight.
But consider this. If n=12 then the notes are an octave apart. In this case pitch_ratio(12) gives 2. In other words the second note has twice the frequency of the first. This frequency ratio is also reflected in the ratio by which we have to multiply the rate at which a sample is played, to make its pitch or frequency change. If we play a sample at rate: 2 then it plays up an octave at double the original frequency. So far from being esoteric, this function is critical to producing a sample based voice. All we need to ascertain is the native frequency of the sample, when played at rate: 1. This is fairly easy to find by trial and error, by playing the sample and comparing it to the frequency of a synth note which can be adjusted until the two sound at the same pitch. Try this code snippet which tests the frequencies of the:amibi_glass_rub and :ambi_glass_hum samples.

sample :ambi_glass_rub,sustain: 1
sleep 1
play :fs5 #adjust to sound the same pitch as the sample
sleep 2
sample :ambi_glass_hum,sustain: 1
sleep 1
play :a3 #adjust to sound the same pitch as the sample

Here we see the natural frequency of the first sample is :fs5 and of the second :a3. Having worked out the sample pitch, we can then define a very simple function pl to play any other note we want, using the sample.

define :pl do |inst,samplepitch,nv,dv|
 shift=note(nv)-note(samplepitch)
 sample inst,rate: (pitch_ratio shift),sustain: 0.8*dv,release: 0.2*dv
end

The function takes four parameters. inst is the sample name e.g. :ambi_glass_rub. samplepitch is the natural pitch of the sample e.g. :fs5. nv is the desired pitch to play e.g. :c4. The final parameter dv is the duration for which you wish the sample to sound. The function first converts the pitches to numbers using the built in note function in Sonic Pi and subtracts the samples pitch from the desired pitch to get the number of semitones difference. (Note sometimes it may be negative as in this example). note(:c4)-note(:fs5) = 60 – 78 = -18. The answer is stored in the local variable shift. The function then plays the sample at a rate given by pitch_ratio(shift) in this case pitch_ratio(-18) or
0.3535533905932738. The duration is set by using a sustain: value of dv*0.8 and a release: value of dv*0.2

That is all that is necessary to play different notes with a single sample. The remaining functions give greater flexibility in using sample notes. The first additional function is plarray. This lets you play a list of notes, with an associated list of the note durations. It can also handle a :r symbol for “playing” a rest. It is a bit like the equivalent play_pattern_timed command for a synth, but also gives control of the envelope of the sample notes.

define :plarray do |inst,samplepitch,narray,darray,shift=0|
 narray.zip(darray) do |nv,dv|
 if nv != :r
 pl(inst,samplepitch,note(nv)+shift,dv)
 end
 sleep dv
 end
end

It takes 5 parameters. inst the sample name, samplepitch the native pitch of the sample, narray and darray the two lists of notes and their durations to be played, and an option shift parameter for transposing as we can’t use the with_transpose function which works with synths.
It zips together the two lists so that an iteration loop can work through each associated pair of elements processing them in turn. Thus on the first pass of the loop nv will hold the first note symbol and dv its duration. It checks whether nv is a rest symbol :r and if not plays nv using the play function previously described. It incorporates adding in any transpose shift by converting nv to a note number and adding in the shift before calling the pl function.
Before looping back for the next note/duration pair, the loop sleeps for dv, the duration of the note.

The second additional function plchord lets you play a chord of notes together.

define :plchord do |inst,samplepitch,nlist,d,shift=0|
 nlist.each do |n|
 pl(inst,samplepitch,note(n)+shift,d)
 end
 sleep d
end

This too uses five parameters, very similar to the previous plarray function. The only differences are that nlist is a list of the notes to be played in the chord, and that d is the single duration value for the length the chord is to sound. In this case a loop iterates around the note values in the list nlist, and for each one uses the pl command to play the note. As there is no sleep command within this function, the notes all sound together. Once they are all started, a single sleep dv command is issued to give the chord time to complete.

To keep the program relatively simple I don’t include the code I usually use for defining note durations in terms of my own set-bpm function, but instead set up variables for the four different note durations needed for the 4 part round ‘Row Row Row the Boat’ which is used to illustrate the voices in action. This is followed by defining the notes and duration lists for this piece before using four threads to play the round with an offset delay between each part. In fact I put this in a 2.times loop so that the round plays twice. Then following a final sleep to allow the final part of the round to complete, the plchord function is called four times to play four chords to finish off.

Since two voices are available, I put their details (inst and samplepitch) in two lists and run the playing section twice using each of the voices in turn.

The complete program is shown below

#streamlined program to produce sample voices, by Robin Newman , March 2015
#uses two built in samples :ambi_glass_rub, and :ambi_glass_hum as examples

inst=[] #array of instruments
samplepitch=[] #array of natural pitches of the samples
inst[0]=:ambi_glass_rub#first chosen instrument
samplepitch[0]=:fs5 #natural pitch of the sample instrument
inst[1]= :ambi_glass_hum #second chosen instrument
samplepitch[1]=:a3 #natural pitch of second instrument

#prior to the release of version 2.5 you need the next function
#if you get an error message saying it exists then delete it or comment it out
comment do
 define :pitch_ratio do |n|
 return 2**(n.to_f/12)
 end
end

#this function plays the sample at the relevant pitch for the note desired
#the note duration is used to set up the envelope parameters
define :pl do |inst,samplepitch,nv,dv|
 shift=note(nv)-note(samplepitch)
 sample inst,rate: (pitch_ratio shift),sustain: 0.8*dv,release: 0.2*dv
end
puts note(:c4)
puts note(:fs5)
puts note(:c4) - note(:fs5)
puts pitch_ratio(-18)
#this function plays an array of notes and associated array of durations
#Uses sample name (inst), sample (normal) pitch, and shift (transpose) parameters
define :plarray do |inst,samplepitch,narray,darray,shift=0|
 narray.zip(darray) do |nv,dv|
 if nv != :r
 pl(inst,samplepitch,note(nv)+shift,dv)
 end
 sleep dv
 end
end

#this function will play a list of notes as a chord for the duration d specified. Optional transpose shift
define :plchord do |inst,samplepitch,nlist,d,shift=0|
 nlist.each do |n|
 pl(inst,samplepitch,note(n)+shift,d)
 end
 sleep d
end
#to illustrate the use of the voice we will play the round Row, Row,Row the Boat, and finish it with a chord
#first create some variable containing note durations
q=0.15 #quaver. Adjust value to change tempo
c=2*q #crotchet
cd=c*1.5 #dotted crotchet
b=4*c #breve

#set up a list of notes and of durations for the tune
notes=[:c4,:c4,:c4,:d4,:e4,:e4,:d4,:e4,:f4,:g4,:c5,:c5,:c5]
notes.concat [:g4,:g4,:g4,:e4,:e4,:e4,:c4,:c4,:c4,:g4,:f4,:e4,:d4,:c4]
dur=[cd,cd,c,q,cd,c,q,c,q,cd*2,q,q,q,q,q,q,q,q,q,q,q,q,c,q,c,q,cd*2]
0.upto(1) do |i|
2.times do #play the round twice. Put one part up an octave to make it stand out
 in_thread {plarray(inst[i],samplepitch[i],notes,dur,5)} #transposed up 5 semitones
 sleep 4*cd #round part separation
 in_thread {plarray(inst[i],samplepitch[i],notes,dur,17)} #transposed up 5 semitones plus an octave
 sleep 4*cd #round part separation
 in_thread {plarray(inst[i],samplepitch[i],notes,dur,5)} #transposed up 5 semitones
 sleep 4*cd #round part separation
end
sleep 12*cd #allows final part to finish

#play four chords
plchord(inst[i],samplepitch[i],[:c4,:e4,:g4,:c5],c,5) #all transposed up 5 semitones to match the tune
plchord(inst[i],samplepitch[i],[:e4,:g4,:c5,:e5],c,5)
plchord(inst[i],samplepitch[i],[:g4,:c5,:e5,:g5],c,5)
plchord(inst[i],samplepitch[i],[:c5,:e5,:g5,:c6],b,5)
end
#from this starter program I have developed one which sets up 5 voices and which plays 25 different rounds!

You can hear what it sounds like here
You can download the program from a gist file here

Following on from this program, I developed a much larger program (over 450 lines) which sets up five sample based volces using five external samples from the Sonatina Symphonic Library of CCL licensed samples. I was becoming fed up with use Frere Jaques as a round to illustrate these voices so, having Googled musical rounds I came across a fantastic site  http://www-personal.umich.edu/~msmiller/rounds.html which contained a large number of rounds. Using many of these and one or two from other sources, I set up the program to play 26 different rounds (and counting!). I did indluge myself a bit here, but I think they sound great, and many are old pieces that you don’t hear very often. I was further aided in this by a great breakthrough in the Sonic Pi achieved by Joseph Wilk, one of the core team of developers, who has managed to get tcp communication going between the server and the Super Collider Synth in Sonic Pi, instead of the previous UDP based system used for communication. This (UDP) set a limit on the messages that could be sent from a single workspace in Sonic Pi, limiting the maximum program size in a workspace. Happily this limit has just been removed in Sonic Pi 2.5dev, enabling larger programs to be used. Unfortunately this enhancement has just been reversed as some stability issues have arisen. Hopefully it will return before too long. This means that the program cannot handle as many Rounds at once. 

It takes 25 minuites or so to play them all which is a long time, so the program can also be set up to play a random selection of 5 rounds selected from the 26. Also, it is possible to remove some of the rounds giving a shorter program, which will work on existing released versions of Sonic Pi which still suffer from the UDP limit.

Configuring the program for your system
The program was developed on a Pi2 which is much more powerful than the previous B and B+ models. On these models the Pi cannot cope with the reverb effect which is applied when a round plays, and so  the relevant use_fx :reverb line should be commented out if you are using one of these machines, together with the associated end statement further down, both of which are marked. Also the Round ‘SUMER IS ACUMIN IN’ which has a 5th drone part will only work on the more powerful machines without casusing sound breakup. This is placed last in the list of rounds, so If you are using a Pi B or model B+ then set the variable num in line to one LESS than the number of rounds, which will exclude this one.
If using a version of Sonic Pi less then 2.5dev, then you will have to reduce the number of rounds in the data list. Simply delete a round section by removing the lines from roundlist[v]=’ROUND NAME’ up to the corresponding line shiftlist[v]=<number>.
Also remove the PRECEDING v +=1 line.

The overall structure for a round data entry is

v +=1
roundlist[v]=''<NAME>'
notes[v]=[list of notes]
dur[v]=[list of durations]
tempolist[v]=<number>
gapslist[v]=<duration>
partslist[v]=<number>
shiftlist[v]=-<number>

The FIRST round has v=0 preceding it instead of v +=1
If you include ‘SUMER IS ACUMIN IN’ it is a good idea to put it last in the list so that it can be excluded by adjusting the num variable as discussed above. In any case, if you alter the number of rounds you MUST manually adjust the line num = <number> at the start of the program in the initial configuration section, to reflect the number of Rounds you actually have in the program. Changing num adjusts all the other sections that depend on the number of Rounds.

On a Pi or Windows PC with an an early version of Sonic PI, I think that up to about 20 rounds will probably work. On a MAC with an early version of Sonic PI then only about FOUR rounds will work. The symptom is that there will be no response or output from the program if you have too many present. You can of course have different versions of the program each loaded with a different selection of rounds. But this will all be solved when 2.5 is released (or you can build your own 2.5dev version right now).

The final configuration you can do is to alter whether you want to listen to all the rounds in sequence, or to listen to a selection of 5 chosen at random (don’t choose this option on a MAC with a pre 2.5dev Sonic Pi as you will only have 4 rounds there!)
This is selected by adjusting the the lines below in the configuration section at the start of the program.

selectionmode=true #set to true for random selection of 5 choices or false to play all 26 rounds
use_random_seed(11111) #change seed number to get a different selection of choices if using selection mode

Setting the selectionmode to false will play all 26 rounds. About 20 minutes worth!
If you set the selectionmode to true, you will always get the same selection made, unless you alter the random seed in the line below. I like to use a 5 digit number here. Try 11111,22222,33333,12345,56789 etc Different numbers will give different selections.

The end is in sight!
Well if you have managed to read so far, below is a listing of the complete program, with a link below which will enable you to download it.

#Program demonstrating the streamlined method of setting up a sample based voice
#from a single sample with a medley of rounds and voices
#written by Robin Newman, March 2015
#Works best with a Pi2 and Sonic Pi 2.5dev or later, but can be adjusted for other configurations
#See article on https://rbnrpi.wordpress.com/
#most of the rounds from http://www-personal.umich.edu/~msmiller/rounds.html
#the rest from Wikipedia en.wikipedia.org/wiki/Round_(music)
# if using version 2.5dev or later remove or comment out function pitch_ratio

#The program is arranged to deal with 3 or 4 part rounds. The three or four parts
#are played twice, the second time up an octave.
#The program utilises just 5 single note audio samples for 5 different instruments
#The function pl enables note to be played using each of these instruments
#Transposition, allocation of instruments and tempo can be adjusted for each round
#Most of the four part rounds are transposed down, to allow for the pitch range of the trumpet voice
#used for the fourth part.
#One round, 'Sumer Is Icumen In' has a drone bass part added. This is played
#by the bassoon voice which is only used for this round.
#The remaining three voices are shuffled at random between each of the first three
#parts of each round.
#Each round has its notes stored in a notes array, and the corresponding
#note durations in a related array
#These are processed by the function plarray
#Note durations are expressed in terms of variables defining each note value
#These are set up by the function setupround, which first sets the tempo,
#redefining the note duration variables, and then the arrays holding each round
#CONFIGURATION CHOICES=============================================================
#location of samples
use_sample_pack_as '/home/pi/samples/Rounds',:rounds #adjust location as necessary
#program needs sample name and normal pitch of that sample
selectionmode=true #set to true for random selection of 5 choices or false to play all 26 rounds
use_random_seed(11111) #change seed number to get a different selection of choices if using selection mode
num=26 #number of rounds to choose from (set to one LESS than then number of rounds if using a Pi B or B+
#to exclude the round 'SUMER IS ACUMIN IN" which will not play on these models
#num MUST be adjusted if you change the number of rounds in the data section
#on a Pi model B or B+ comment out fx_reverb and associated end towards end of program
#END OF CONFIGURATION CHOICES======================================================
use_debug false

#first deal with selecting and setting up the samples

inst0=:rounds__bassoon_g3
samplepitch0=:g3
inst1=:rounds__flute_ds5
samplepitch1=:ds5
inst2=:rounds__clarinet_gs4
samplepitch2=:gs4
inst3=:rounds__harp_a4
samplepitch3=:a4
inst4=:rounds__trumpet_cs5
samplepitch4=:cs5

#preload all the samples
load_flag=0
if sample_loaded? inst0 then #check if sample already loaded
  flag=1
end
load_samples [inst0,inst1,inst2,inst3,inst4]
if load_flag==0 then #samples not loaded so allow time
  sleep 3
end

#put the sample names and pitches into an array i
i=[[inst0,samplepitch0],[inst1,samplepitch1],[inst2,samplepitch2],[inst3,samplepitch3]]
i.concat [[inst4,samplepitch4]]

#build list of instrument choices for each round.
instlist=[]
num.times do #first three voices allocated at random
  instlist.concat [[i[2],i[1],i[3]].shuffle+[i[4],i[0]]]
end

#define variables that need to be used globally
s=dsq=sq=sqd=q=qt=qd=qdd=c=cd=cdd=m=md=mdd=b=bd=0 #note duration variables
roundlist=[] #round names
notes=[] #notes list for a round (an array of arrays)
dur=[] #note durations for a round (an array of arrays)
notesbass=[] #etra part notes for round 3
durbass=[] #extra part durations for round 3
gapslist=[] #gap between parts coming in
tempolist=[] #tempo of a round
partslist=[] #number of part entries in round
shiftlist=[] #transpose shift to play round

#prior to the release of version 2.5 you need the next function
#if you get an error message saying it exists then delete it or comment it out
comment do
  define :pitch_ratio do |n|
    return 2**(n.to_f/12)
  end
end

#this function plays the sample at the relevant pitch for the note desired
#the note duration is used to set up the envelope parameters
define :pl do |inst,samplepitch,nv,dv|
  shift=note(nv)-note(samplepitch)
  sample inst,rate: (pitch_ratio shift),sustain: 0.8*dv,release: 0.2*dv
end

#this function plays an array of notes and associated array of durations
#also uses sample name (inst), sample normal pitch, and shift (transpose) parameters
define :plarray do |inst,samplepitch,narray,darray,shift=0|
  narray.zip(darray) do |nv,dv|
    if nv != :r
      pl(inst,samplepitch,note(nv)+shift,dv)
    end
    sleep dv
  end
end

#set_bpm sets bpm required adjusting note duration variables accordingly
define :set_bpm do |n|
  s=1.0/8*60/n.to_f
  dsq = 1 * s #demisemiquaver
  sq = 2 * s #semiquaver
  sqd = 3 * s #semi quaver dotted
  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
end

#this section sets the bpm then defines the note and duration arrays
#for all the differnt rounds
define :setuprounds do |bpmvalue|
  set_bpm(bpmvalue) #define notes and durations in terms of currrent bpm value
#DATA SECTION DETAILING THE ROUNDS==============================
  v=0 #index counter for rounds
  roundlist[v]='Three Blind Mice'
  notes[v]=[:fs4,:e4,:d4,:r,:a4,:g4,:r,:g4,:fs4,:r,:a4,:d5,:d5,:cs5,:b4,:cs5,:d5,:a4,:a4,:a4,:d5,:d5,:d5,:cs5,:b4,:cs5]
  notes[v].concat [:d5,:a4,:a4,:a4,:d5,:d5,:cs5,:b4,:cs5,:d5,:a4,:a4,:a4,:g4,:fs4,:e4,:d4,:r]
  dur[v]=[cd,cd,cd+q,c,cd,q,q,q,c,c,c,c,q,q,q,q,c,q,c,q,q,q,q,q,q,q]
  dur[v].concat [c,q,c,q,c,q,q,q,q,q,q,q,c,q,cd,cd,cd,cd]
  tempolist[v]=180
  gapslist[v]=4*cd
  partslist[v]=4
  shiftlist[v]=-5
  v +=1
  roundlist[v]='Row the Boat'
  notes[v]=[:c4,:c4,:c4,:d4,:e4,:e4,:d4,:e4,:f4,:g4,:c5,:c5,:c5]
  notes[v].concat [:g4,:g4,:g4,:e4,:e4,:e4,:c4,:c4,:c4,:g4,:f4,:e4,:d4,:c4]
  dur[v]=[cd,cd,c,q,cd,c,q,c,q,cd*2,q,q,q,q,q,q,q,q,q,q,q,q,c,q,c,q,cd*2]
  tempolist[v]=180
  gapslist[v]=4*cd
  partslist[v]=4
  shiftlist[v]=0
  v +=1
  roundlist[v]='Frere Jaques'
  notes[v]=[:c4,:d4,:e4,:c4]*2+[:e4,:f4,:g4]*2+[:g4,:a4,:g4,:f4,:e4,:c4]*2+[:c4,:g3,:c4]*2
  dur[v]=[c,c,c,c,c,c,c,c,c,c,m,c,c,m,q,q,q,q,c,c,q,q,q,q,c,c,c,c,m,c,c,m]
  tempolist[v]=160
  gapslist[v]=8*c
  partslist[v]=4
  shiftlist[v]=0
  v +=1
  roundlist[v]='Wind, gentle evergreen'
  notes[v]=[:d5,:e5,:d5,:c5,:b4,:c5,:d5,:e4,:g4,:c5,:b4,:c5,:b4,:a4,:d5,:d5,:cs5,:c5,:c5,:b4,:g4,:b4,:a4,:g4,:fs4,:g4,:g4]
  notes[v].concat [:b4,:c5,:b4,:g4,:a4,:fs4,:g4,:a4,:b4,:c5,:b4,:a4,:g4,:g4,:fs4,:a4,:gs4,:b4,:e4,:a4,:a4,:a4,:fs4,:g4,:d5,:c5,:b4,:a4,:g4,:g4]
  notes[v].concat [:g4,:g4,:d4,:g4,:g4,:c4,:e4,:fs4,:g4,:d4,:b3,:a3,:fs4,:g4,:b3,:c4,:c4,:d4,:d4,:g4]
  dur[v]=[qd,sq,c,c,cd,q,c,qd,sq,c,qd,sq,c,c,c,c,c,c,c,c,q,q,qd,sq,cd,q,md]
  dur[v].concat [qd,sq,q,q,q,q,cd,q,c,qd,sq,c,c,c,q,q,q,q,c,c,c,qd,sq,c,c,qd,sq,cd,q,md]
  dur[v].concat [c,c,c,m,c,qd,sq,c,c,m,c,m,c,m,c,qd,sq,cd,q,md]
  tempolist[v]=160
  gapslist[v]=8*md
  partslist[v]=3
  shiftlist[v]=0
  v +=1
  roundlist[v]='Upon Christ Church Bells in Oxford'
  notes[v]=[:c5,:c5,:c5,:c5,:c5,:c5,:c5,:e5,:d5,:c5,:b4,:a4,:g4,:g4,:g4,:g4,:e4,:g4,:c4,:g4,:c5,:f4,:g4,:g5,:f5,:e5,:a5,:d5,:e5,:f5,:e5,:d5,:c5]
  notes[v].concat [:e5,:e5,:e5,:e5,:e5,:e5,:e5,:g5,:f5,:e5,:f5,:d5,:c5,:d5,:g4,:d5,:d5,:e5,:d5,:e5,:d5,:e5,:d5,:d5,:c5,:b4,:c5,:a4,:d5,:b4,:c5,:d5,:e5]
  notes[v].concat [:g5,:g5,:g5,:g5,:g5,:g5,:g5,:g5,:g5,:g5,:g5,:g4,:a4,:a4,:b4,:c5,:b4,:r,:a4,:b4,:c5,:b4,:c5,:b4,:c5,:d5,:b4,:a4,:g4,:a4,:f4,:g4,:g4,:c4]
  dur[v]=[cd,q,c,c,c,c,c,c,c,c,c,c,cd,q,c,c,c,c,c,c,c,c,c,q,q,c,c,q,q,c,cd,q,m]
  dur[v].concat [cd,q,c,c,c,c,c,c,c,q,q,c,c,c,c,c,c,c,c,c,c,c,c,c,q,q,c,c,c,c,cd,q,m]
  dur[v].concat [q,q,q,q,c,q,q,c,q,q,c,c,cd,q,c,c,m,c,q,q,c,c,c,c,c,c,c,q,q,c,c,cd,q,b]
  tempolist[v]=160
  gapslist[v]=8*b
  partslist[v]=4
  shiftlist[v]=-7
  v +=1
  roundlist[v]='To The Greenwood'
  notes[v] = [:c5,:b4,:b4,:a4,:a4,:g4,:g4,:f4,:f4,:e4,:c4,:f4,:g4,:c4]
  notes[v].concat [:c4,:d4,:e4,:f4,:g4,:e4,:f4,:d4,:e4,:c4,:a4,:b4,:c5,:d5,:g4,:c5,:c5,:b4,:c5]
  notes[v].concat [:e5,:e5,:d5,:e5,:c5,:c5,:b4,:c5,:a4,:a4,:g4,:e5,:d5,:c5,:c5]
  dur[v]=[m,c,c,c,c,c,c,c,c,c,c,c,c,m,q,q,q,q,c,c,c,c,c,c,q,q,q,q,c,c,c,c,m,cd,q,c,c,cd,q,c,c,c,c,c,c,cd,q,m]
  tempolist[v]=160
  gapslist[v]=4*b
  partslist[v]=3
  shiftlist[v]=0
  v +=1
  roundlist[v]='Viva La Musica!'
  notes[v]=[:d5,:c5,:b4,:a4,:g4,:fs4,:g4,:fs4,:b4,:c5,:d5,:c5,:b4,:a4,:g4,:a4,:g4,:g4,:c4,:d4,:e4,:d4]
  dur[v]=[cd,q,c,q,q,c,c,m,cd,q,c,q,q,c,c,m,c,m,c,c,c,m]
  tempolist[v]=160
  gapslist[v]=2*b
  partslist[v]=3
  shiftlist[v]=0
  v +=1
  roundlist[v]='Shalaom Chavarim'
  notes[v]=[:b3,:e4,:e4,:fs4,:g4,:e4,:g4,:g4,:a4,:b4,:b4,:e5,:d5,:b4,:b4,:e5,:b4,:a4,:g4,:a4,:b4,:g4,:fs4,:e4,:b3,:e4,:fs4,:g4,:e4]
  dur[v]=[c,c,q,q,c,c,c,q,q,c,c,md,c,md,c,c,q,q,c,c,c,q,q,c,c,md,q,q,md]
  tempolist[v]=160
  gapslist[v]=4*c
  partslist[v]=3
  shiftlist[v]=0
  v +=1
  roundlist[v]='Oh My Love Lovst Thou Me II'
  notes[v]=[:g4,:a4,:bb4,:bb4,:c5,:d5,:g5,:d5,:g5,:g5,:f5,:g5,:d5,:c5,:bb4,:a4,:g4]
  dur[v]=[m,m,b,m,m,md,c,c,c,c,c,m,cd,q,m,m,b]
  tempolist[v]=180
  gapslist[v]=2*b
  partslist[v]=4
  shiftlist[v]=-5
  v +=1
  roundlist[v]='Celebrons sans cesse'
  notes[v]=[:c5,:bb4,:a4,:bb4,:g4,:f4,:f4,:c4,:d4,:bb3,:c4,:c5,:bb4,:a4,:g4,:f4,:f4,:e4,:f4,:f5,:e5,:d5,:c5,:a4]
  dur[v]=[md,c,m,m,b,m,b,m,m,m,m,md,c,b,md,c,b,m,m,b,m,b,b,b]
  tempolist[v]=220
  gapslist[v]=4*b
  partslist[v]=4
  shiftlist[v]=-5
  v +=1
  roundlist[v]='Gaudeamus Hodie'
  notes[v]=[:c4,:c4,:e4,:g4,:f4,:g4,:f4,:e4,:d4,:c4,:c4,:e4,:g4,:f4,:e4,:d4,:c4,:c4,:e4,:g4,:c5,:b4,:a4,:g4,:f4,:e4,:d4,:c4]
  notes[v].concat [:c4,:d4,:e4,:f4,:g4,:a4,:g4,:e4,:f4,:d4,:c4]
  notes[v].concat [:g4,:g4,:a4,:c5,:g4,:g4,:a4,:f4,:g4,:c5,:e5,:d5,:c5,:d5,:g4,:c5,:b4,:c5]
  dur[v]=[c,c,c,c,c,c,q,q,c,c,c,c,c,c,c,m,c,c,c,c,c,c,q,q,c,m,m,b]
  dur[v].concat [b,b,b,b,b,b,c,c,c,c,b,m,m,c,md,m,m,c,md,m,m,c,c,c,c,c,m,c,b]
  tempolist[v]=200
  gapslist[v]=8*b
  partslist[v]=3
  shiftlist[v]=0
  v +=1
  roundlist[v]='Come Let us All A-Maying Go'
  notes[v]=[:g5,:fs5,:e5,:d5,:e5,:d5,:c5,:b4,:g4,:a4,:g4,:g5,:fs5,:d5,:g5,:fs5,:d5,:g5,:fs5,:e5,:g5,:fs5,:g5]
  notes[v].concat [:e5,:d5,:c5,:b4,:a4,:b4,:c5,:b4,:a4,:g4,:d5,:c5,:b4,:b4,:c5,:d5,:b4,:g4,:d5,:b4,:g4,:c5,:a4,:r,:b4,:g4]
  notes[v].concat [:g4,:g4,:g4,:g4,:g4,:g4,:g4,:g4,:d4,:g4,:fs4,:e4,:d4,:g4,:fs4,:e4,:d4,:c4,:d4,:g4]
  dur[v]=[c,c,c,cd,q,q,q,cd,q,c,m,c,c,c,c,c,c,q,q,cd,q,c,md]
  dur[v].concat [c,c,c,q,q,q,q,q,q,c,c,c,m,q,q,q,cd,c,q,cd,c,q,cd,c,m,c]
  dur[v].concat [m,c,m,c,m,c,m,c,c,q,q,c,c,q,q,q,q,m,c,md]
  tempolist[v]=180
  gapslist[v]=4*bd
  partslist[v]=3
  shiftlist[v]=-4
  v +=1
  roundlist[v]='Come Follow Me Merrily'
  notes[v]=[:d5,:b4,:c5,:d5,:e5,:e5,:d5,:d5,:c5,:b4,:a4,:b4,:c5,:c5,:b4,:a4,:g4,:a4,:b4,:g4]
  notes[v].concat [:g4,:d4,:a4,:d4,:g4,:g4,:d4,:g4,:c4,:g4,:d4,:d4,:g4,:r,:b4]
  notes[v].concat [:d5,:e5,:fs5,:e5,:fs5,:g5,:g5,:g5,:fs5,:d5,:e5,:fs5,:g5,:fs5,:e5,:fs5,:g5,:r]
  dur[v]=[c,cd,q,c,c,c,c,c,c,c,m,c,c,c,c,cd,q,c,md+m,c,m,c,m,c,m,c,m,c,m,c,m,c,md,m,c]
  dur[v].concat [cd,q,c,m,c,c,c,c,m,c,cd,q,c,cd,q,c,md+m,c]
  tempolist[v]=180
  gapslist[v]=4*bd
  partslist[v]=3
  shiftlist[v]=-4
  v +=1
  roundlist[v]='Now We Are Met'
  notes[v]=[:c4,:e4,:f4,:g4,:a4,:b4,:c5,:a4,:g4,:f4,:e4,:e4,:d4,:c4,:f4,:g4,:c4]
  notes[v].concat [:r,:c5,:c5,:c5,:b4,:r,:c5,:c5,:c5,:b4,:r,:g4,:g4,:g4,:a4,:b4,:c5,:b4,:c5]
  notes[v].concat [:r,:b4,:c5,:d5,:e5,:r,:a4,:b4,:c5,:d5,:c5,:c5,:f5,:e5,:d5,:d5,:c5]
  dur[v]=[c,q,q,c,q,q,c,c,m,c,q,q,c,c,c,c,m,q,q,q,q,c,cd,q,q,q,c,cd,q,q,q,q,q,m,c,m]
  dur[v].concat [m+q,q,q,q,c,cd,q,q,q,c,q,q,c,c,c,c,m]
  tempolist[v]=180
  gapslist[v]=4*b
  partslist[v]=3
  shiftlist[v]=0
  v +=1
  roundlist[v]='Merrily, Merrily'
  notes[v]=[:eb4,:eb4,:eb4,:eb4,:eb4,:eb4,:g4,:f4,:eb4,:r,:g4,:g4,:g4,:g4,:g4,:g4,:bb4,:ab4,:g4,:r]
  notes[v].concat [:eb5,:eb5,:eb5,:eb5,:bb4,:eb5,:d5,:eb5,:c5,:bb4,:c5,:bb4,:eb5,:g4,:bb4,:bb4,:r]
  dur[v]=[q,q,q,q,q,q,c,q,c,q,q,q,q,q,q,q,c,q,c,q,c,sq,sq,q,c,c,q,c,q,c,q,c,q,c,q,c,q]
  tempolist[v]=140
  gapslist[v]=2*md
  partslist[v]=4
  shiftlist[v]=-4
  v +=1
  roundlist[v]='The Huntsmen'
  notes[v]=[:c4,:e4,:e4,:e4,:f4,:f4,:f4,:e4,:e4,:e4,:e4,:d4,:d4,:d4,:d4,:c4,:d4,:e4,:c4,:g4]
  notes[v].concat [:g4,:g4,:g4,:g4,:g4,:g4,:g4,:g4,:a4,:a4,:a4,:b4,:b4,:b4,:c5,:c5,:r]
  notes[v].concat [:c5,:r,:d5,:r,:e5,:c5,:e4,:e4,:f4,:f4,:f4,:g4,:g4,:g4,:c4,:r]
  dur[v]=[q,q,q,q,q,q,q,c,q,c,q,q,q,q,q,q,q,cd,c,q,q,q,q,q,q,q,cd+c,q,q,q,q,q,q,q,cd,c,q]
  dur[v].concat [c,q,c,q,q,m,sq,sq,c,s,sq,c,sq,sq,cd+c,q]
  tempolist[v]=150
  gapslist[v]=4*md
  partslist[v]=3
  shiftlist[v]=0
  v +=1
  roundlist[v]='Hey We to the Other World'
  notes[v]=[:a4,:c5,:bb4,:a4,:g4,:f4,:e4,:c4,:a4,:c5,:f4,:c5,:d5,:c5,:bb4,:a4,:g4]
  notes[v].concat [:c5,:c5,:a4,:f4,:f4,:bb4,:c5,:d5,:e5,:d5,:e5,:f5,:a4,:g4,:c5,:c5]
  dur[v]=[m,m,q,q,q,q,c,c,c,c,cd,q,q,q,q,q,m,cd,q,c,q,q,q,q,c,c,q,q,md,c,cd,q,m]
  tempolist[v]=180
  gapslist[v]=2*b
  partslist[v]=3
  shiftlist[v]=-2
  v +=1
  roundlist[v]='Dona Nobis Pacem'
  notes[v]=[:g4,:d4,:b4,:a4,:d4,:c5,:b4,:a4,:g4,:g4,:fs4,:e5,:d5,:c5,:b4,:a4,:d5,:c5,:b4,:b4,:a4,:g4,:fs4,:g4]
  notes[v].concat [:d5,:d5,:d5,:c5,:b4,:b4,:a4,:e5,:e5,:d5,:d5,:d5,:c5,:b4,:a4,:g4]
  notes[v].concat [:g4,:fs4,:g4,:a4,:b4,:c5,:d5,:d4,:c5,:c5,:b4,:b4,:fs4,:a4,:d5,:d4,:g4]
  dur[v]=[q,q,m,q,q,m,c,c,c,c,m,c,q,q,q,q,cd,q,c,q,q,c,c,md]
  dur[v].concat [md,md,c,c,c,c,m,c,m,c,m,q,q,c,c,md,md,md,cd,q,q,q,c,m,c,m,c,m,q,q,c,c,md]
  tempolist[v]=140
  gapslist[v]=8*md
  partslist[v]=3
  shiftlist[v]=-2
  v +=1
  roundlist[v]='Fox and Geese'
  notes[v]=[:f4,:a4,:bb4,:c5,:d5,:e5,:f5,:g5,:e5,:f5,:c5,:c5,:bb4,:a4,:g4,:g4,:f4,:c4,:c4,:c4,:f4,:a4,:f4,:g4,:a4,:b4,:a4]
  notes[v].concat [:g4,:a4,:g4,:a4,:c5,:c5,:f5,:c5,:f5,:d5,:e5,:c5,:c5]
  dur[v]=[c,cd,q,c,cd,q,c,m,c,m,c,cd,q,c,c,c,c,c,c,c,m,c,cd,q,c,m,c,cd,q,c,m,q,q,c,m,c,m,m,c,md]
  tempolist[v]=180
  gapslist[v]=4*md
  partslist[v]=4
  shiftlist[v]=-5
  v +=1
  roundlist[v]='Hey, Ho, Nobody Home'
  notes[v]=[:d4,:c4,:d4,:d4,:d4,:a3,:a3,:d4,:d4,:e4,:e4,:f4,:f4,:f4,:f4,:e4,:a4,:g4,:a4,:g4,:a4,:g4,:a4,:g4,:f4,:e4]
  dur[v]=[m,m,c,q,q,c,c,c,c,c,c,q,q,q,q,m,cd,q,cd,q,cd,q,q,q,q,q]
  tempolist[v]=180
  gapslist[v]=b*2
  partslist[v]=4
  shiftlist[v]=3
  v +=1
  roundlist[v]='White Sand and Grey'
  notes[v]=[:a4,:b4,:a4,:gs4,:a4,:cs5,:d5,:cs5,:b4,:cs5,:a4,:fs4,:d4,:e4,:a3]
  dur[v]=[b,m,m,b,b,b,m,m,b,b,b,m,m,b,b]
  tempolist[v]=210
  gapslist[v]=4*b
  partslist[v]=3
  shiftlist[v]=-4
  v +=1
  roundlist[v]='Hava Nashira, Shir "Alleluia"'
  notes[v]=[:g3,:d4,:e4,:d4,:g3,:c4,:b3,:a3,:g3,:a3,:g3,:g4,:fs4,:g4,:a4,:b4,:g4,:g4,:a4,:b4,:c5,:b4]
  notes[v].concat [:b4,:a4,:g4,:fs4,:g4,:e4,:d4,:g4,:g4,:fs4,:e4,:fs4,:g4]
  dur[v]=[m,c,c,m,m,m,c,q,q,m,m,m,c,c,m,m,m,c,q,q,m,m,m,c,c,m,m,m,c,c,q,q,q,q,m]
  tempolist[v]=160
  gapslist[v]=4*b
  partslist[v]=3
  shiftlist[v]=2
  v +=1
  roundlist[v]='Oaken Leaves'
  notes[v]=[:f4,:c4,:f4,:f4,:f4,:f4,:e4,:f4,:g4,:f4,:g4,:g4,:fs4,:g4,:d4,:g4]
  notes[v].concat [:a4,:a4,:c5,:a4,:g4,:a4,:bb4,:a4,:g4,:a4,:bb4,:a4,:g4,:fs4,:g4]
  notes[v].concat [:f4,:c5,:a4,:bb4,:c5,:c5,:c5,:d5,:eb5,:d5,:c5,:c5,:bb4,:c5,:d5,:c5,:bb4,:a4,:b4]
  dur[v]=[md,c,m,c,c,cd,q,c,c,b,m,m,m,m,b,b,m,m,b,cd,q,c,c,b,c,c,m,m,m,b,b,m,m,cd,q,m,c,c,c,c,c,c,m,c,c,m,m,m,b,b]
  tempolist[v]=180
  gapslist[v]=8*b
  partslist[v]=3
  shiftlist[v]=0
  v +=1
  roundlist[v]='The Spring is Come'
  notes[v]=[:bb4,:ab4,:g4,:ab4,:bb4,:eb5,:ab4,:g4,:ab4,:bb4,:eb5,:bb4,:ab4,:g4,:f4,:eb4,:r,:eb4,:r,:eb4,:r,:eb4,:bb3,:bb3,:eb4]
  notes[v].concat [:g4,:f4,:eb4,:f4,:g4,:g4,:f4,:eb4,:f4,:g4,:ab4,:g4,:f4,:eb4,:d4,:eb4]
  dur[v]=[c,q,c,q,c,c,q,c,q,c,c,c,c,c,c,md,c,m,m,m,m,m,c,c,md,c,q,c,q,c,c,q,c,q,c,c,c,c,c,c,md]
  tempolist[v]=160
  gapslist[v]=4*b
  partslist[v]=3
  shiftlist[v]=0
  v +=1
  roundlist[v]='The Hillside'
  notes[v]=[:c5,:b4,:b4,:a4,:a4,:g4,:g4,:f4,:f4,:e4,:c4,:f4,:g4,:c4,:c4,:d4,:e4,:f4,:g4,:e4,:f4,:d4,:e4,:c4,:f4,:g4,:a4,:b4,:c5,:c5,:c5,:b4,:c5]
  notes[v].concat [:e5,:e5,:d5,:e5,:c5,:d5,:b4,:c5,:a4,:b4,:c5,:d5,:e5,:e5,:f5,:d5,:d5,:e5]
  dur[v]=[m,c,c,c,c,c,c,c,c,c,c,c,c,m,q,q,q,q,c,c,c,c,c,c,q,q,q,q,c,c,c,c,m]
  dur[v].concat [cd,q,c,c,cd,q,c,c,q,q,q,q,c,q,q,c,c,m]
  tempolist[v]=180
  gapslist[v]=8*m
  partslist[v]=3
  shiftlist[v]=-4
  v +=1
  roundlist[v]='SUMER IS ICUMEN IN'
  notes[v]=[:f5,:e5,:d5,:e5,:f5,:f5,:e5,:d5,:c5,:a4,:a4,:bb4,:g4,:a4,:r,:f4,:a4,:g4,:bb4,:a4,:a4,:g4,:f4,:a4,:c5,:d5,:d5,:c5,:r]
  notes[v].concat [:f5,:d5,:f5,:r,:c5,:a4,:bb4,:g4,:a4,:c5,:bb4,:a4,:f4,:a4,:g4,:e4,:f4,:r,:a4,:a4,:g4,:bb4,:c5,:c5,:d5,:e5]
  notes[v].concat [:f5,:e5,:d5,:e5,:f5,:r,:c5,:d5,:c5,:bb4,:a4,:f4,:a4,:bb4,:g4,:a4,:bb4,:c5,:a4,:c5,:g4,:e4,:f4,:r]
  dur[v]=[c,q,c,q,c,q,q,q,q,c,q,c,q,cd,cd,c,q,c,q,c,q,c,q,c,q,c,q,cd,cd]
  dur[v].concat [cd,cd,cd,cd,c,q,c,q,c,q,c,q,c,q,c,q,cd,cd,c,q,c,q,c,q,c,q,c,q,c,q,cd,cd,cd,cd,cd,c,q,c,q,c,q,cd,c,q,c,q,c,q,cd,cd]
  notesbass=[:f3,:g3,:f3,:g3,:a3,:c4,:bb3,:c4,:r]*8+[:f3,:g3,:f3]
  durbass=[cd,cd,cd,c,q,cd,cd,cd,cd]*8+[cd,cd,cd]
  tempolist[v]=160
  gapslist[v]=4*cd
  partslist[v]=4
  shiftlist[v]=-5
#END OF ROUNDS DATA SECTION==============================

end
define :printlist do |num| #prints round info on screen
  0.upto(num-1) do |i|
    puts "round "+(i+1).to_s+" '"+roundlist[i]+"'"
  end
  puts "" #blank line
end
define :adjustedchoices do |rlist| #bump choices in list by 1 for printing purposes
  alist=[]
  0.upto(4) do |i|
    alist << (rlist[i]+1)
  end
  return alist
end
setuprounds(180) #prime the tempolist so it can be used as a parameter for subsequent calls
printlist(num)
#now play the rounds
if selectionmode then
  rlist=range(0,num).shuffle #choose 5 rounds from the list in random order
  puts "Selection choices: "+adjustedchoices(rlist).to_s #print them
  limit=4
else limit=num
end
0.upto(limit) do |i| #play the choices
  if selectionmode then
    ch=rlist[i]
  else
    ch=i
  end
  #adjust duration arrays for correct tempo
  setuprounds((tempolist[ch]))
  #print info on the screen
  puts "round "+(ch+1).to_s+" of "+num.to_s+" now playing:"
  puts "'"+roundlist[ch]+"'"
  puts partslist[ch].to_s+" voices"
  #allocate choices for the current round
  notes=notes[ch]
  dur=dur[ch]
  gaps=gapslist[ch]
  shift=shiftlist[ch]
  with_fx :reverb,room: 0.6 do #comment out reverb and corresponding end line if on model B or B+
    if roundlist[ch]=='SUMER IS ICUMEN IN' then #deal with the special case of bass part for this round
      in_thread {plarray(instlist[ch][4][0],instlist[ch][4][1],notesbass,durbass,shift)}
    end
    in_thread {plarray(instlist[ch][0][0],instlist[ch][0][1],notes,dur,shift)}
    sleep gaps
    in_thread {plarray(instlist[ch][1][0],instlist[ch][1][1],notes,dur,shift)}
    sleep gaps
    in_thread {plarray(instlist[ch][2][0],instlist[ch][2][1],notes,dur,shift)}
    sleep gaps
    if partslist[ch]==4 then #add 4th part if 4 part round
      in_thread {plarray(instlist[ch][3][0],instlist[ch][3][1],notes,dur,shift)}
      sleep gaps
    end
    in_thread {plarray(instlist[ch][0][0],instlist[ch][0][1],notes,dur,12+shift)}
    sleep gaps
    in_thread {plarray(instlist[ch][1][0],instlist[ch][1][1],notes,dur,12+shift)}
    sleep gaps
    if partslist[ch]==4 then  #4 part round
      in_thread {plarray(instlist[ch][2][0],instlist[ch][2][1],notes,dur,12+shift)}
      sleep gaps
      plarray(instlist[ch][3][0],instlist[ch][3][1],notes,dur,12+shift)
    else
      plarray(instlist[ch][2][0],instlist[ch][2][1],notes,dur,12+shift)
    end
    sleep b #gap before next round
  end #comment out this end if reverb commented out
end
puts "Finished!"

Finally here are some download links

gist link for the program code here
link to zip file of the samples used. Install (unzipped) on your computer and set the location in the configuration section of the program.
listen to a selection audio file here