Sonic Pi discussion on rendering dynamic levels, illustrated by Hassler’s Dixit Maria ad Angelum

I never tire of using Sonic Pi to play a variety of music. I have experimented with live coding, and also with using SP to control Minecraft, but I really enjoy rendering early 16th century music using the program.
When I first started using the program (especially with version 1), it was an achievement to be able to play any polyphonic music. However, as the program has been developed it is now fairly easy to play the notes of four five or six part music using the program, although it is still quite tedious to put in all the notes. Over the last year I have developed techniques which makes this aspect of of producing music on Sonic Pi quite routine. What is NOT so easy is to play the music in a musical manner, particularly as regards articulation and dynamic expression, and in making the music sound authentic.
As regular readers of this blog will know, I have spent considerable time in developing sample based voices for use with Sonic Pi, and again this aspect is now fairly routine for me, and certain instruments work very well, including a Piano, Flute, Clarinet and Trumpet to name a few. One “instrument” however is very elusive. That is the human voice. Even the best sound production systems have difficulty in synthesising this or in using sampled versions.
One reason I like transcribing 16th century choral music for Sonic Pi is that often the lines are very smooth, and the pieces rely on the interaction between the various voices, with a strict rhythmical metre. Such lines lend themselves to a smooth synth, and I find that the :tri and :pulse synths in SP can be used to good effect. However these pieces also rely on changes in dynamic level, making use not only of crescendos and diminuendos, but also sudden changes in level, producing for example answering quiet echoes of a phrase.

The piece developed in this article employs various techniques to make this possible. Smooth legato notes are achieved by setting up the note envelope appropriately. Basically you want the note to be sustained for a large proportion of its duration. You also need to adjust the attack and release times to achieve the effect you want. I have found that you need to do this on a note by note basis. You can set the overall shape, but should then scale it with the note duration required. In this piece I use the following settings:

attack: s*d*0.02,decay: s*d*0.02,attack_level: 0.6,sustain_level: 1,sustain: s*d*0.9,release: s*d*0.06

s is a scale factor set by the desired tempo. d is the duration of the note given in terms of units, where a quaver is 2 units, a crotchet 4, a minim 8 etc. You can see that I build up to an attack level of 0.6 in a time which is 2% of the overall duration, then”decay” upwards again to a sustain level of 1 in a further 2% of the duration time. There the note remains for 90% of the duration time then finally decays to 0 level in the release time which is 6% of the duration. This gives a shape which is quite smooth and legato with a little articulation, but not too percussive. Regular readers of this blog will know that I enter notes for each part into a separate list (n1,n2,n3 and n4) with corresponding durations in lists d1,d2,d3 and d4. I define duration variables q,c,m etc representing quavers, crotchets, minims to enable quick entry of these lists. The notes are played using a function pl which incorporates the envelope discussed above, and the lists are parsed and actioned in pairs (note +duration) by the function plarray. These function also allow for the pan setting to be used.

define :pl do |n,d,s,i,v,p=0| #play a note n= note, d duration,s tempo scalefactor, i synth,v amplitude, p pan
  if d !=:r
    with_synth i do
      #use adsr enveloped to get desired note shape:
      play n,attack: s*d*0.02,decay: s*d*0.02,attack_level: 0.6,sustain_level: 1,sustain: s*d*0.9,release: s*d*0.06,amp:v,pan: p
    end
  end
  sleep d*s #duration of note scaled for tempo
end

define :plarray do |na,da,s,i,v,p=0| #play arrays of notes and durations na,da arrays, s tempo scalefactor, i synth,v amplitude,p pan
  na.zip(da) do |n,d|
    pl(n,d,s,i,v,p)
  end
end

My more recent occupation has been in developing ways to adjust the dynamic level of the parts as they play. One approach is to specify the dynamic level of every single note as it plays, using the amp: parameter of the play command. Although this is possible, it produces a huge extra amount of data, and it is very tedious to adjust the levels of a range of notes. However, there is a with_ fx effect :level which can be used to adjust the level at which notes are played, and, more, importantly, it can be adjusted over time by using the control command. This is the technique I have developed and implemented in this piece, and it allows for both immediate and gradual level changes.

A separate with_fx :level command is used for each part, and associated with each one are three lists, containing the level required (a) the time taken to change to the new level (as) and the time before the next level change is to be implemented (ad). Two functions are defined to implement the level changes: ct and plct (short for control (ct) and part level control (plct))
These are shown below:

define :ct do |ptr,lev,slid=0,timetonext=0,s,flag| #controls level
  control ptr,amp: lev,amp_slide: slid*s #times scalefactor for tempo
  if flag > 0 then #lets you print level variations
    puts "Part "+flag.to_s+" Level change:"
    puts "level "+levlookup.assoc(lev)[1]
    puts "slide time "+ (slid*s).to_s
    puts " " #blank line
  end
  sleep timetonext*s #scale for tempo
end

define :plct do |pt,am,amsl,amd,s,flag=0| #performs level change for a part
  #params pt conrtol pointer,am amp level,amsl amp_slide:,amd delay to next command,s tempo scale factor,flag to print info
  am.zip(amsl,amd) do |amv,amslv,amdv|
    ct(pt,amv,amslv,amdv,s,flag)
  end
end

The levlookup.assoc(lev) in the ct function lets you output the level as a letter, p,mp,ff etc rather than a numeric value, by using an associative array defined as

levlookup=[[p,"p"],[mp,"mp"],[mf,"mf"],[f,"f"],[ff,"ff"]] #allows printing in ct function

 ct has 5 parameters. ptr points to  the relevant with_fx :level command. lev is the level to be set, slid is the time taken to reach this level, timetonext is the delay before the next call to ct is implemented, s is a tempo scale factor, and flag is used to control output printed messages from the function. Basically this function just saves typing in the full control command each time it is required. The meat of the function is the command control ptr,amp: lev,amp_slide: slid*s
Flag is then used to switch on a printed message (if flag >0) and by adjusting the parameter value to 1..4  printed message can be displayed in real time for each part as it plays. The final parameter gives a sleep command to prevent the next call to ct taking place until the required time interval (scaled by s) has elapsed.
The plct function takes the three lists a,as,ad referred to previously as the parameters am,amsl and amd and uses the Ruby zip command to enable them to be read in sync with each other passing on the values read to the ct command in a loop. It also passes on the s and flag parameters. Using this mechanism, the fx level of each part can be fully controlled as the part plays.
The setup to play and control each part in a thread is shown below:

s=set_bpm(130) #set scaled multiplier for desired tempo 130 crotchets / minute
with_transpose 2 do #change key form F Major to G major
  #set up and play with reverb
  with_fx :reverb,room: 0.8,mix: 0.6 do
    in_thread do
      with_fx :level do |amp1| #set dynamic level
        in_thread do
          plct(amp1,a1,as1,ad1,s,1)
        end
        plarray(n1,d1,s,i1,v1,-0.7)
      end
    end
    in_thread do
      with_fx :level do |amp2| #set dynamic level
        in_thread do
          plct(amp2,a2,as2,ad2,s,2)
        end
        plarray(n2,d2,s,i2,v2,0.7)
      end
    end
    in_thread do
      with_fx :level do |amp3| #set dynamic level
        in_thread do
          plct(amp3,a3,as3,ad3,s,3)
        end
        plarray(n3,d3,s,i3,v3,-0.5)
      end
    end
    with_fx :level do |amp4| #set dynamic level
      in_thread do
        plct(amp4,a4,as4,ad4,s,4)
      end
      plarray(n4,d4,s,i4,v4,0.5)
    end
  end
end

After calls to set the value of s for the tempo required (using my own set_bpm function) and to set up an overall reverb, a thread is set up for each part (except the last one) which in turn contains the with_fx :level do |amp|…….end loop to control the level of the part. Inside this there is a thread which uses the plct function to adjust the amp settings for the level. There is also a call to the plarray function to play the notes for the relevant part.
The last part differs only in the fact that it doesn’t have to be placed in an outer thread, although it has its own with_fx :level loop and associated plct thread.

One of the problems is in how to debug the entry of the various lists of note, durations and level controls. I use various techniques to help. A simple one is to count the number of entries in each list. Obviously the number of notes and number of durations in corresponding lists should be the same. Also I calculate the total duration of each duration list, and these should be the same for all the parts. Discrepencies in these values help to track down entry errors which may have occured. Another useful technique is to enter the notes and durations in sections of a few bars at a time. When each new section is started I redifine n1=[… d1=[…. etc and only substitute the .concat commands to string them all together once each section has been individually checked.

Final tweaking of the piece involves listening to each and adjusting settings such as the basic volume setting for each part, the amount of reverb and mix thereof, the individual levels for each dynamic setting p,mp,f,ff etc and an overall scaling factor for these (sf). Also if necessary adjusting the overall ADSR envelope. Also in this piece I insert one or two very short rests in several places, shortening the preceding note appropriately to give extra articulation or short breaks where necessary. It is this final adjusting which can take a very long time. It is a little subjective, and several of these factors interact, but I find it is worth the effort to get an overall pleasing sound to the finished piece. It is also affected by the final audio system used to play the piece. In this case it benefits from a good audio stereo audio system. So although in general using a system like Sonic Pi to play such music can give a rather mechanical end result, if you take some care in experimenting and adjusting the various parameters I have discussed you can add a personal creativity to the final performance which can give quite a “buzz” similar to when (if you are fortunate enough) you can participate in a live performance of the actual music being played. It is also great to be able to listen to such pieces as the one featured here which are not performed live all that often.

The full program is listed below, followed by downloadable links to the code and to a recording of Sonic Pi playing the piece.

#Hans Leo Hassler (1564-1612) Dixit Maria ad Angelum coded for Sonic Pi by Robin Newman April 2015

#This piece utilises the fx level to alter the dynamic level of each part.
#Each part is wrapped inside a with_fx :level command and the level is controlled by a thread
#which adjusts the level amp: setting via a control function. Since the part playing is inside the fx loop
#its volume is controlled as it plays. The ct function alters the amp setting with a control command
#which includes the new level setting and a slide parameter to adjust how long teh change takes
#the plct function reads level settings from three list a,as and ad associated with each part, which contain
#the level values, the slide time and the delay time before the next level change command is activated.
#a parameter at the end of the plct command is set either to 0, or to the part number 1-4. In the latter case
#it will print out level changes and slide times so that you can follow the changes as the music plays

i1=i2=i3=i4=:pulse #synth for all parts

#part volumes set quite low
v1=0.4
v2=0.4
v3=0.5
v4=0.5

s=0 #dummy value to define tempo scale variable. Set later using set_bpm
#note duration relative values (not all used)
dsq = 1
sq = 2
sqd = 3
q = 4
qt = 2.0/3*q
qd = 6
qdd = 7
c = 8
cd = 12
cdd = 14
m = 16
md = 24
mdd = 28
b = 32
bd = 48
sf=0.3 #scale factor to adjust overall level for best performance
#dynamic settings
# p mp mf f ff
p=0.06*sf
mp=0.2*sf
mf=0.5*sf
f=1.0*sf
ff=1.3*sf
levlookup=[[p,"p"],[mp,"mp"],[mf,"mf"],[f,"f"],[ff,"ff"]] #allows printing in ct function

#set_bpm sets bpm required adjusting note duration variables accordingly
define :set_bpm do |n|
  s=1.0/8*60/n.to_f
  return s
end

define :pl do |n,d,s,i,v,p=0| #play a note n= note, d duration,s tempo scalefactor, i synth,v amplitude, p pan
  if d !=:r
    with_synth i do
      #use adsr enveloped to get desired note shape:
      play n,attack: s*d*0.02,decay: s*d*0.02,attack_level: 0.6,sustain_level: 1,sustain: s*d*0.9,release: s*d*0.06,amp:v,pan: p
    end
  end
  sleep d*s #duration of note scaled for tempo
end

define :plarray do |na,da,s,i,v,p=0| #play arrays of notes and durations na,da arrays, s tempo scalefactor, i synth,v amplitude,p pan
  na.zip(da) do |n,d|
    pl(n,d,s,i,v,p)
  end
end

define :ct do |ptr,lev,slid=0,timetonext=0,s,flag| #controls level
  control ptr,amp: lev,amp_slide: slid*s #times scalefactor for tempo
  if flag > 0 then #lets you print level variations
    puts "Part "+flag.to_s+" Level change:"
    puts "level "+levlookup.assoc(lev)[1]
    puts "slide time "+ (slid*s).to_s
    puts " " #blank line
  end
  sleep timetonext*s #scale for tempo
end

define :plct do |pt,am,amsl,amd,s,flag=0| #performs level change for a part
  #params pt conrtol pointer,am amp level,amsl amp_slide:,amd delay to next command,s tempo scale factor,flag to print info
  am.zip(amsl,amd) do |amv,amslv,amdv|
    ct(pt,amv,amslv,amdv,s,flag)
  end
end

define :len do |d| #for checking duration of each part
  tl=0
  d.each do |d|tl += d
  end
  return tl
end

#define note and duration arrays for the four parts
n1=[:r,:f4,:f4,:f4,:g4,:a4,:f4,:a4,:g4,:a4,:bb4,:c5,:b4,:c5,:a4,:c5,:bb4,:a4,:g4,:g4,:r,:g4,:r,:g4,:r,:g4,:r,:a4]
d1=[4*b,m,c,c,m,c,c,cd,q,q,q,m,c,c,c,cd,q,c,c,m,c,  qd,sq,qd,sq,qd,sq  ,m]
#b13
n1.concat [:g4,:a4,:bb4,:a4,:g4,:f4,:f4,:e4,:f4,:r,:f4,:f4,:f4,:g4,:a4,:f4,:a4,:g4,:a4,:bb4,:c5,:b4,:c5,:a4,:f4,:e4,:d4,:e4,:f4,:g4,:g4]
d1.concat [c,c,cd,q,q,q,m,c,m,c,c,c,c,m,c,c,cd,q,q,q,m,c,c,m,cd,sq,sq,q,q,c,c]
#b21
n1.concat [:g4,:a4,:a4,:g4,:f4,:e4,:d4,:e4,:f4,:e4,:f4,:r,:a4,:a4,:g4,:g4,:fs4,:g4,:g4,:g4,:r,:c5,:c5,:bb4,:bb4,:a4,:a4,:a4,:a4,:r,:d5,:c5,:bb4,:a4,:g4,:c5]
d1.concat  [c,c,q,q,q,q,q,q,m,c,  c+qd,q    ,b,m,m,c,c,md,c,  c+qd,q    ,b,m,m,c,c,md,c,m,c,cd,q,q,q,c,c]
#b33
n1.concat [:c5,:bb4,:a4,:g4,:f4,:f4,:r,:d5,:d5,:c5,:bb4,:a4,:g4,:a4,:r,:bb4,:bb4,:a4,:g4,:f4,:e4,:e4,:c5,:c5,:bb4,:a4,:g4,:f4,:e4,:f4,:r]
d1.concat [q,q,q,q,m,m,c,c,c,c,c,c,m,m,3*b+c,c,c,c,c,c,m,c,c,c,c,c,m,m,c,    c+qd,q]
#b45
n1.concat [:a4,:a4,:g4,:g4,:fs4,:g4,:g4,:g4,:r,:c5,:c5,:bb4,:bb4,:a4,:a4,:a4,:a4,:r,:d5,:c5,:bb4,:a4,:g4,:c5,:c5,:bb4,:a4,:g4,:f4,:f4,:r,:d5,:d5,:c5]
d1.concat [b,m,m,c,c,md,c,  c+qd,q    ,b,m,m,c,c,md,c,m,c,cd,q,q,q,c,c,q,q,q,q,m,m,c,c,c,c]
#b57
n1.concat [:bb4,:a4,:g4,:a4,:r,:bb4,:bb4,:a4,:g4,:f4,:e4,:e4,:c5,:c5,:bb4,:a4,:g4,:f4,:e4,:f4,:r,:d5,:d5,:c5,:bb4,:a4,:bb4,:a4]
d1.concat [c,c,m,m,3*b+c,c,c,c,c,c,m,c,c,c,c,c,m,m,c,m,m+c,c*1.1,c*1.2,c*1.2,c*1.3,c*1.3,m*2,b*3]
#end

n2=[:r,:c4,:c4,:c4,:d4,:e4,:c4,:d4,:c4,:d4,:e4,:f4,:e4,:f4,:d4,:f4,:a4,:g4,:f4,:e4,:f4,:e4,:f4,:e4,:e4,:d4,:c4,:d4,:d4,:e4,:r,:e4,:r,:d4,:r,:e4,:r,:f4,:e4,:c4,:d4]
d2=[2*b,m,c,c,m,c,c,cd,q,q,q,m,c,c,c,md,c,cd,q,c,m,c,q,q,q,sq,sq,c,c,   q,q   ,qd,sq,qd,sq,qd,sq  ,q,q,q,q]
#b13
n2.concat [:e4,:c4,:d4,:d4,:d4,:c4,:bb3,:a3,:d4,:f4,:e4,:d4,:c4,:b3,:c4,:d4,:c4,:bb3,:c4,:a3,:d4,:r,:c4,:c4,:c4,:d4]
d2.concat [c,c,m,c,c,cd,q,c,c,cd,q,c,c,c,m,m,q,q,c,c,m,c,c,md,c,m]
#b21
n2.concat [:e4,:f4,:c4,:d4,:f4,:d4,:c4,:c4,:c4,:r,:f4,:f4,:e4,:d4,:c4,:d4,:d4,:e4,:r,:g4,:a4,:f4,:g4,:f4,:e4,:e4,:fs4,:a4,:g4,:f4,:e4,:d4,:d4,:e4,:d4,:e4]
d2.concat [c,c,c,c,c,c,cd,q,  c+qd,q    ,b,m,m,c,c,md,c,  c+qd,q    ,b,m,m,c,c,md,c,c,cd,q,q,q,c,c,cd,sq,sq]
#b33
n2.concat [:f4,:c4,:d4,:d4,:c4,:d4,:f4,:f4,:c4,:d4,:e4,:f4,:e4,:f4,:a4,:a4,:g4,:f4,:e4,:d4,:e4,:a4,:a4,:g4,:f4,:e4,:d4,:e4,:f4,:e4,:d4,:c4,:g4,:g4,:f4,:e4,:d4,:c4,:c4,:r]
d2.concat [c,c,c,c,m,c,c,cd,q,q,q,m,c,c,c,c,c,c,c,m,c,c,c,c,c,c,cd,q,c,c,m,c,c,cd,q,md,c,b,   c+qd,q]
#b45
n2.concat [:f4,:f4,:e4,:d4,:c4,:d4,:d4,:e4,:r,:g4,:a4,:f4,:g4,:f4,:e4,:e4,:fs4,:a4,:g4,:f4,:e4,:d4,:d4,:e4,:d4,:e4,:f4,:c4,:d4,:d4,:c4,:d4,:f4,:f4,:c4]
d2.concat [b,m,m,c,c,md,c,  c+qd,q    ,b,m,m,c,c,md,c,c,cd,q,q,q,c,c,cd,sq,sq,c,c,c,c,m,c,c,cd,q]
#b57
n2.concat [:d4,:e4,:f4,:e4,:f4,:a4,:a4,:g4,:f4,:e4,:d4,:e4,:a4,:a4,:g4,:f4,:e4,:d4,:e4,:f4,:e4,:d4,:c4,:g4,:g4,:f4,:e4,:d4,:c4,:c4,:r,:a4,:a4,:g4,:f4,:e4,:d4,:e4,:f4,:f4]
d2.concat [q,q,m,c,c,c,c,c,c,c,m,c,c,c,c,c,c,cd,q,c,c,m,c,c,cd,q,md,c,m,m,c,c,c,c,c+q*1.1,q*1.1,c*1.2,c*1.2,m*1.3+m*2,b*3]
#end

n3=[:f3,:f3,:f3,:g3,:a3,:f3,:a3,:g3,:a3,:bb3,:c4,:b3,:c4,:a3,:bb3,:a3,:bb3,:c4,:d4,:a3,:bb3,:c4,:f3,:bb3,:c4,:f4,:d4,:c4,:c4,:c4,:g3,:g3,:c3,:r,:c4,:r,:b3,:r,:c4,:r,:f3]
d3=[m,c,c,m,c,c,cd,q,q,q,m,c,c,c,cd,q,q,q,q,q,c,c,c,c,md,c,m,m,m,m,cd,q,   q,q   ,qd,sq,qd,sq,qd,sq   ,m]
#b13
n3.concat [:c4,:a3,:g3,:f3,:g3,:a3,:bb3,:a3,:g3,:g3,:f3,:bb3,:a3,:bb3,:a3,:r,:f3,:f3,:f3,:g3,:a3,:f3,:a3,:g3,:a3,:bb3,:c4,:b3]
d3.concat [c,c,q,q,q,q,cd,q,c,c,c,c,c,c,m,b+c,c,c,c,m,c,c,cd,q,q,q,m,c]
#b21
n3.concat [:c4,:bb3,:a3,:a3,:bb3,:a3,:g3,:g3,:a3,:r,:c4,:c4,:c4,:c4,:b3,:c4,:b3,:a3,:b3,:b3,:c4,:r,:e4,:e4,:f4,:d4,:d4,:d4,:cs4,:b3,:cs4,:cs4,:d4,:d4,:c4,:bb3,:a3,:g3,:c4,:c4,:bb3]
d3.concat [cd,q,c,c,cd,q,c,c,  c+qd,q         ,m,md,c,m,c,m,q,q,c,c,  c+qd,q    ,m,m,m,m,c,m,q,q,c,c,m,cd,q,q,q,c,c,q,q]
#b33
n3.concat [:a3,:g3,:f3,:g3,:a3,:f3,:bb3,:a3,:bb3,:bb3,:bb3,:a3,:g3,:f3,:c4,:f3,:c4,:c4,:g3,:a3,:bb3,:c4,:b3,:a3,:b3,:c4,:c4,:g3,:a3,:bb3,:d4,:d4,:c4,:bb3,:a3,:g3,:c3,:e3,:a3,:bb3,:c4,:bb3,:a3,:g3,:f3,:g3,:a3,:r]
d3.concat [q,q,q,q,q,q,m,c,c,c,c,c,c,c,m,c,c,cd,q,q,q,cd,sq,sq,c,c,m,c,m,c,c,c,c,c,c,m,c,c,c,c,c,c,cd,sq,sq,m,   c+qd,q]
#b45
n3.concat [:c4,:c4,:c4,:c4,:b3,:c4,:b3,:a3,:b3,:b3,:c4,:r,:e4,:e4,:f4,:d4,:d4,:d4,:cs4,:b3,:cs4,:cs4,:d4,:d4,:c4,:bb3,:a3,:g3,:c4,:c4,:bb3,:a3,:g3,:f3,:g3,:a3,:f3,:bb3,:a3,:bb3,:bb3,:bb3,:a3]
d3.concat [m,md,c,m,c,m,q,q,c,c,  c+qd,q    ,m,m,m,m,c,m,q,q,c,c,m,cd,q,q,q,c,c,q,q,q,q,q,q,q,q,m,c,c,c,c,c]
#b57
n3.concat [:g3,:f3,:c4,:f3,:c4,:c4,:g3,:a3,:bb3,:c4,:b3,:a3,:b3,:c4,:c4,:g3,:a3,:bb3,:d4,:d4,:c4,:bb3,:a3,:g3,:c3,:e3,:a3,:bb3,:c4,:bb3,:a3,:g3,:f3,:g3,:a3,:bb3,:c4,:r,:f4,:f4,:e4,:d4,:c4,:d4,:c4]
d3.concat [c,c,m,c,c,cd,q,q,q,cd,sq,sq,c,c,m,c,m,c,c,c,c,c,c,m,c,c,c,c,c,c,cd,sq,sq,m,cd,q,m,c,c*1.1,c*1.2,c*1.2,c*1.3,c*1.3,m*2,b*3]
#end

n4=[:r,:f3,:f3,:f3,:g3,:a3,:f3,:a3,:g3,:a3,:bb3,:c4,:b3,:c4,:r]
d4=[6*b,m,c,c,m,c,c,cd,q,q,q,m,c,m,b]
#b13
n4.concat [:r,:bb2,:bb2,:bb2,:c3,:d3,:bb2,:d3,:c3,:d3,:e3,:f3,:e3,:f3,:r,:f3,:f3,:a3,:g3]
d4.concat [m,m,c,c,m,c,c,cd,q,q,q,m,c,b,b+m,m,c,c,m]
#b21
n4.concat [:c4,:f3,:f3,:e3,:d3,:c3,:bb2,:c3,:f3,:r,:f3,:f3,:c3,:g3,:a3,:g3,:g3,:c3,:r,:c4,:f3,:bb3,:g3,:d3,:a3,:a3,:d3,:r]
d4.concat [c,c,q,q,q,q,m,m,  c+qd,q    ,b,m,m,c,c,md,c,  c+qd,q    ,b,m,m,c,c,md,c,m,bd]
#b33
n4.concat [:f3,:e3,:d3,:bb2,:f3,:bb2,:r,:f3,:f3,:e3,:d3,:c3,:g3,:c3,:f3,:f3,:e3,:d3,:c3,:bb2,:bb2,:c3,:c3,:c3,:c3,:c3,:c3,:f3,:r]
d4.concat [cd,q,c,c,m,m,b+md,c,c,c,c,c,m,c,c,c,c,c,c,b,m,m,m,m,m,m,m,   c+qd,q]
#b45
n4.concat [:f3,:f3,:c3,:g3,:a3,:g3,:g3,:c3,:r,:c4,:f3,:bb3,:g3,:d3,:a3,:a3,:d3,:r,:f3,:e3,:d3,:bb2,:f3,:bb2,:r]
d4.concat [b,m,m,c,c,md,c,  c+qd,q    ,b,m,m,c,c,md,c,m,bd,cd,q,c,c,m,m,m]
#b57
n4.concat [:r,:f3,:f3,:e3,:d3,:c3,:g3,:c3,:f3,:f3,:e3,:d3,:c3,:bb2,:bb2,:c3,:c3,:c3,:c3,:c3,:c3,:f3,:f3,:f3,:e3,:d3,:c3,:bb2,:f3,:bb2,:f3]
d4.concat [b+c,c,c,c,c,c,m,c,c,c,c,c,c,b,m,m,m,m,m,m,m,c,c,c,c,c+q*1.1,q*1.1,m*1.2+1.3*c,c*1.3,m*2,b*3]
#end

#parts dynamic data
a1= [mp,   mf,      p,    mp,   mf,     mp,     mf,   p,    mf,   f,      mp,  mf,    f,    ff    ] #levels
as1=[0,    3*b,     0,    0,    6*b,    0,      b,    0,    6*b,  6*b,    0,   0,     6*c,   m    ] #slide times
ad1=[6*b,  15*b+m,  4*b,  4*b,  7*b+c,  3*b+c,  3*b,  4*b,  4*b,  6*b,    3*b, 4*b+m, 6*c,   2*b  ] #sleep durations at level

a2= [mp,   mf,      p,    mp,   mf,     mp,     mf,   p,    mf,   f,      mp,  mf,    f,     ff   ]
as2=[0,    3*b,     0,    0,    6*b,    0,      b,    0,    6*b,  7*b,    0,   4*b,   b,     m    ]
ad2=[6*b,  15*b+m,  4*b,  4*b,  7*b+c,  3*b+c,  3*b,  4*b,  4*b,  7*b+c,  7*c, 4*b,   2*b,   2*b  ]

a3= [mp,   mf,      p,    mp,   mf,     mp,     mf,   p,    mf,   f,      mp,  mf,    f,     ff   ]
as3=[0,    3*b,     0,    0,    6*b,    0,      b,    0,    6*b,  7*b,    0,   5*b,   b,     m    ]
ad3=[6*b,  15*b+m,  4*b,  4*b,  7*b+c,  3*b+c,  3*b,  4*b,  4*b,  7*b+c,  7*c, 5*b,   b,     2*b  ]

a4= [mp,   mf,      p,    mp,   mf,     mp,     mf,   p,    mf,   f,      mp,  mf,    f,     ff   ]
as4=[0,    3*b,     0,    0,    6*b,    0,      b,    0,    6*b,  7*b,    0,   4*b+c, b,     m    ]
ad4=[6*b,  15*b+m,  4*b,  4*b,  7*b+c,  3*b+c,  3*b,  4*b,  4*b,  7*b+c,  7*c, 4*b+c, b+3*c, 2*b  ]

uncomment do #uncomment for debugging checking lengths
  #n and d lengths for each part should be the same
  puts n1.length
  puts d1.length
  puts n2.length
  puts d2.length
  puts n3.length
  puts d3.length
  puts n4.length
  puts d4.length
  #total durations for all parts should be the same
  puts len(d1)
  puts len(d2)
  puts len(d3)
  puts len(d4)
  #these duration totals should be the same. Note NOT adjusted for the rit, but as at end of piece this doesn't matter
  #these durations will be less than that of the parts because the rit is not included
  puts len(ad1)
  puts len(ad2)
  puts len(ad3)
  puts len(ad4)
end
s=set_bpm(130) #set scaled multiplier for desired tempo 130 crotchets / minute
with_transpose 2 do #change key form F Major to G major
  #set up and play with reverb
  with_fx :reverb,room: 0.8,mix: 0.6 do
    in_thread do
      with_fx :level do |amp1| #set dynamic level
        in_thread do
          plct(amp1,a1,as1,ad1,s,1)
        end
        plarray(n1,d1,s,i1,v1,-0.7)
      end
    end
    in_thread do
      with_fx :level do |amp2| #set dynamic level
        in_thread do
          plct(amp2,a2,as2,ad2,s,2)
        end
        plarray(n2,d2,s,i2,v2,0.7)
      end
    end
    in_thread do
      with_fx :level do |amp3| #set dynamic level
        in_thread do
          plct(amp3,a3,as3,ad3,s,3)
        end
        plarray(n3,d3,s,i3,v3,-0.5)
      end
    end
    with_fx :level do |amp4| #set dynamic level
      in_thread do
        plct(amp4,a4,as4,ad4,s,4)
      end
      plarray(n4,d4,s,i4,v4,0.5)
    end
  end
end

You can download the program here

You can listen to the piece here

Musical Fireworks with Sonic Pi and Minecraft!

A major new feature is being introduced with Sonic Pi version 2.5 shortly due to be released, which enables it to control Minecraft on a Raspberry Pi. This is particularly effective on a Pi 2 which has the power to comfortably run both programs in tandem.

Over the last couple of weeks I have been playing with the interface using SP 2.5dev, and it has been really great fun getting to grips with producing graphic on Minecraft created from Sonic Pi. I am still learning a lot, and there is quite a lot of experimentation necessary to get the best effects. For example, if you want to produce patterns and to move around freely to view them, then it is a good idea to create a blank world to start with. This is also helpful as it means that Minecraft has less to do to update the screen if you are producing an animation. Secondly, I discovered that you need to put in a delay to synchronise music with Minecraft. Somewhat surprisingly it is Minecraft which has to be delayed to sync with music from Sonic Pi.
I wanted to be able to use circular patterns and spirals, so I also looked at using Math function from Ruby, which can be accessed in Sonic Pi. I had never really used Minecraft before, but I have found the immediacy of using it with Sonic Pi preferable to first writing code in Python and then running that….. Now I’d better put my helmet on to protect it from the brickbats! :-)

The first three programs I wrote have been published on youtube, with accompanying gists for the code, You can see them here.

I also posted a brief video from my phone of the explosions used in this current program I am discussing (  https://youtu.be/WJZ8KLNXZ10  ) but I have now modified the program to add the musical accompaniment.

In many previous posts, I have discussed the use of sample based voices in Sonic Pi, and in this program I have utilised three sample based voices, for Trumpet, Tenor Trombone and Bass Trombone to play the minuet from Handel’s Royal Fireworks Music, which was originally played in Green Park in London to accompany a fireworks display on 27th April 1748. It is an appropriate piece to put into this modern setting to accompany this fireworks display.

This is written in a program in its own workspace in Sonic Pi, completely separate from the code which produces the fireworks. It is possible in Sonic Pi to send a cue from one workspace to synchronise something happening in another. I actually do this twice. The music program is started, but then stops and waits for a cue from the graphics code producing the fireworks, so that they start together. Also when the music finishes playing it sends a cue back to the graphic program to stop it from producing any further fireworks.

I have discussed in previous post how to create sample based voices, These also utilise a new function in Sonic Pi 2.5dev called pitch_ratio which lets you calculate the rate at which to play a sample to play a note which is a certain number of semitones away from the natural pitch at which the sample was recorded. An equivalent to the play function for synths which I call pl is coded to play a note using a sample. A similar function to the play_pattern_timed function which I call plarray is defined which enables you to play a pair of lists of notes and their durations, although it has the added feature of setting an appropriate envelope for each note to give a sustained output.
One or two further tricks are employed. A function  part is defined which plays arrays for each of the instruments together (two trumpets, a tenor and bass trombone) utilising threads. Because there are different sections to the music including repeats and first and second time bars, this is written in such a way that the it can be utilised many times, by feeding in the data for the lists to be played from an array lar (short for list of arrays). Each “play” requires 8 lists: 2 each (for the notes and durations) for each of the parts. By using an offset into this array, different sets of the 8 lists can be selected, so when we play the piece we have in turn part(0) part(8) part(16) part(8) part(24) part(32) part(0) part(8) part(24) selecting the appropriate lists to play.
This program is placed in a workspace and is the first to be run in Sonic Pi. It then sits and waits for a cue form the graphics program to start it.

The Minecraft driver program which produces the explosions sits in its own workspace.
As discussed, it first clears out any existing world and lays down a grass layer. The first time you run this, it may take a long time to complete this program, and in fact I usually stop and start the program 2 or 3 times until the Minecraft display comes up with blue sky and a grass layer underneath. On subsequent runs, the 6 second pause allows things to settle, and give you time to switch to the Minecraft screen before the program starts to animate.
Incidentally, if you create a new world when Minecraft has already been running, then I find that you have to restart Minecraft for Sonic Pi to pick up the correct world with which to communicate. If in doubt and nothing much seems to be happening it is best to quit and restart Minecraft: don’t just go back to the menu screen. It is quite a quick process on a Pi2!
Next the program sends a cue :Fireworks to start the music program playing.
Because the firework explosion is based on a circular pattern we need to be able to use some trigonometry. Happily Sonic Pi is based on the Ruby language, and we can utilise the Math module built in to the language. If you Google sites about Ruby you will discover that for example you can access the value of Pi as

Math::PI

This is a bit a a mouthful so I assign it to a variable using

pi=Math::PI

(note the capitalisation is important)
Likewise I utilise the sine and cosine functions supplied by the Math library but define my own functions to make it slightly easier to use them.

define :sin do |v|
  return Math:sin(v)
end

and

define :cos do |v|
  return Math:cos(v)
end

Also, because these functions like their arguments in radians I define a further function to convert degrees to radians

define :rad do |v|
  return v * Math::PI/180
end

The equation for a circle is given by x=r*sin(i),  y=r*sin(i) where i goes from 0 to 2Pi (or 0 to 360 degrees) where r is the desired radius
You can add offsets xs and xs  to change the position of the origin.

Putting this altogether we can define a circle function as

define :circle do |brick,xs,ys,zs,r,updown,ang=angresolution|
  if updown == 1 then
    0.step(360,ang) do |i|
      mc_set_block brick,xs + r*sin(rad(i)),ys+r*cos(rad(i)),zs
      sleep s
    end
  else
    360.step(0,-ang) do |i|
      mc_set_block :air,xs + r*sin(rad(i)),ys+r*cos(rad(i)),zs
      sleep s
    end
  end
  #sleep 0.1
end

brick is the required material eg :gold, xs,ys,zs position the centre of the circle, r is the circle radius, and ang is the resolution (in degrees) i.e. the angle through which you rotate between points on the circle. The circle can either be drawn with a solid brick (:gold, :iron or :diamond) when updown is set to 1, or wiped when updown is set to 0 by drawing it with air bricks. In this program the variable s is set to 0 so that the circle is drawn at maximum speed.

To build the exploding firework, I made a pattern which I called target (because it looked a bit like one). This utilised 13 circles, adjusted so that each one was 1 unit larger in radius than the last, with the colours cycling through three brick materials. Also the resolution or ang parameter of the circles is adjusted as the radius increases to give minimum build time. The smaller circles don’t need as many points to draw them. The pattern is drawn twice. On the first pass v is set to 1, which is passed to the updown parameter. On the second pass v is set to 0 and the circles are removed.

Having set up the various bits, the program can now do something.
We teleport to 0,30,0 which is an appropriate position from which to observe, and then prime the variable flag setting it to 0. This is used to stop the program at the end. A thread sits and waits for a cue named :finish to be received from the music program. When this happens flag is set to 1
Until then, a loop starts which repeatedly performs the target function, each time drawing an exploding pattern like a firework burst. This is accompanied by the sample :ambi_lunar_land the first 3.5 seconds of which played at 1.5 times normal rate gives an explosive sound, especially when played at amp: 2

When the finish flag is set to 1 the loop stops and a final credit notice is displayed.

You can see a video of the program in operation

a gist containing the two programs is here

you can download the three samples required here

You should place them in a folder named Fireworks inside a folder named samples in the Pi home directory.

I hope this will inspire you to try out Sonic Pi with Minecraft. it is a great combination!