Over the last couple of days, I have worked on two new techniques for use with Sonic Pi 2.1.1
Adding dynamic shaping: p,f,ff and crescendos and diminuendos
First, I have for a long time wanted a better way to add dynamic shaping to the pieces I have coded for Sonic Pi. There is of course an amp: option for all play based commands which lets you set the amplitude or volume of the synth playing the sound. However, this is quite tedious to use if you have to apply an amp setting to every note that you play. Also, there are difficulties if you want to program a crescendo or diminuendo, as you have to calculate the values required using the starting and ending levels and the number of notes over which the increase or decrease is to take place.
The solution to this dilemna is to make use of the with_fx level: command. This allows you to apply a wrapper around a section of notes to be played, and to set a multiplier level which is applied to all the notes within the wrapper. So if the individual note amp: is set to say 0.8 and the level: amp: is set to 0.5, then the overall effect is to play the note at amp: 0.8 * 0.5 = 0.4
The other thing about the fx_level: command is that it can also accept a sliding parameter which allows you to set the time over which the level is to change. This allows you to implement crescendos and diminuendos very easily.
So how does this work in practice? It is perhaps best to look at a working example.
Click the link below to see it.
#In Dulci Jubilo transcribed for Sonic Pi 2.1.1 by Robin Newman December 2014 #this piece features the use of a separate dynamics track adjusting the amplitude of the parts #This utilises the use of the fx_level effect, and also uses the amp_slide: parameter to achieve #crescendos and diminuendos # set_sched_ahead_time! 2 use_debug false use_synth :tri s=0 #set s here with dummy value so that its scope is global define :setbpm do |n| #set bpm equivalent s = (1.0 / 8) *(60.0/n.to_f) end setbpm(200) dsq = 1 * s #note length definitions (not all used) sq = 2 * s q= 4 * s qd = 6 * s c = 8 * s cd = 12 * s m = 16 * s md = 24 * s b = 32 * s bd = 48 * s p = 0.1 #set dynamic levels here mp = 0.2 mf = 0.3 f = 0.5 ff = 0.7 ps=m #adjust elongation for pauses define :pl do |notes,durations,vol=1| #default volume 1, multiplied by level setting notes.zip(durations).each do |n,d| play n,attack: dsq*0.2,sustain: 0.9*d-dsq*0.2,release: 0.1*d,amp: vol sleep d end end #define the four pairs of note and duration parts n1=[:f4,:f4,:f4,:a4,:bb4,:c5,:d5,:c5,:c5,:f4,:f4,:a4,:bb4,:c5,:d5,:c5] d1=[c,m,c,m,c,m,c,m,c,m,c,m,c,m,c,md+ps] #b9 n1.concat [:c5,:d5,:c5,:bb4,:a4,:f4,:f4,:g4,:g4,:a4,:g4,:f4,:g4,:a4,:a4,:c5,:d5,:c5,:bb4,:a4] d1.concat [m,c,m,c,md,m,c,m,c,m,c,m,c,m,c,m,c,m,c,md] #b20 n1.concat [:f4,:f4,:g4,:g4,:a4,:g4,:f4,:g4,:a4,:d4,:d4,:e4,:e4,:f4,:c5,:a4,:bb4,:g4,:g4,:f4] d1.concat [m,c,m,c,m,c,m,c,md,m,c,m,c,md,md,m,c,m,c,m+ps] #end n2=[:c4,:d4,:c4,:f4,:e4,:d4,:c4,:f4,:e4,:f4,:d4,:c4,:f4,:e4,:d4,:c4,:f4,:e4] d2=[c,m,c,cd,q,c,m,c,m,c,m,c,cd,q,c,m,c,md+ps] #b9 n2.concat [:f4,:f4,:e4,:g4,:c4,:f4,:f4,:f4,:f4,:f4,:e4,:f4,:f4,:f4,:f4,:e4,:g4,:c4] d2.concat [m,c,m,c,md,m,c,m,c,m,c,md+m,c,m,c,m,c,md] #b20 n2.concat [:f4,:f4,:f4,:f4,:f4,:e4,:f4,:d4,:d4,:e4,:e4,:d4,:e4,:f4,:f4,:f4,:e4,:f4] d2.concat [m,c,m,c,m,c,md*2,m,c,m,c,md,md,m,c,m,c,m+ps] #end n3=[:a3,:bb3,:a3,:c4,:d4,:a3,:bb3,:g3,:a3,:bb3,:a3,:c4,:d4,:a3,:bb3,:g3] d3=[c,m,c,m,c,m,c,m,c,m,c,m,c,m,c,md+ps] #b9 n3.concat [:c4,:bb3,:g3,:e3,:f3,:a3,:a3,:d4,:d4,:c4,:d4,:bb3,:a3,:bb3,:c4,:d4,:c4,:bb3,:g3,:e3,:f3] d3.concat [m,c,m,c,md,m,c,m,c,cd,q,c,m,c,m,c,m,c,m,c,md] #b20 n3.concat [:a3,:a3,:d4,:d4,:c4,:d4,:bb3,:a3,:bb3,:c4,:a3,:a3,:g3,:g3,:a3,:bb3,:g3,:f3,:bb3,:d4,:c4,:a3] d3.concat [m,c,m,c,cd,q,c,m,c,md,m,c,m,c,m,c,md,m,c,m,c,m+ps] #end n4=[:f3,:f3,:f3,:f3,:f3,:f3,:f3,:f3,:f3,:f3,:f3,:f3,:c3] d4=[c,m,c,m,c,md+m,c,m,c,m,c,md,md+ps] #b9 n4.concat [:a2,:bb2,:c3,:c3,:f3,:d3,:d3,:bb2,:bb2,:c3,:c3,:f3,:d3,:a2,:bb2,:c3,:c3,:f3] d4.concat [m,c,m,c,md,m,c,m,c,m,c,md+m,c,m,c,m,c,md] #b20 n4.concat [:d3,:d3,:bb2,:bb2,:c3,:c3,:f3,:f3,:f3,:e3,:e3,:d3,:c3,:f3,:d3,:bb2,:c3,:f2] d4.concat [m,c,m,c,m,c,md*2,m,c,m,c,md,md,m,c,m,c,m+ps] #end #These check that the note and duration pairs all match in length: uncomment for debugging comment do puts n1.length puts d1.length puts n2.length puts d2.length puts n3.length puts d3.length puts n4.length puts d4.length end define :ct do |ptr,lev,slid=0| #this reduces the typing required for the control commands control ptr,amp: lev,amp_slide: slid end with_fx :reverb,room: 0.4,mix: 0.5 do with_fx :level do |x| #use fx level to give dynamic volume setting 1.upto(4) do |i| #four verses. Index i used to change dynamic ending for the last verse puts "verse "+i.to_s sleep m #gives slight gap between verses in_thread do #volume control thread ct(x,p,0) #set the first dynamic level sleep c+md*2 #these sleep commands space out the dynamic changes to the correct points ct(x,mp,md+m) #b3 sleep md+m ct(x,p,0) #b4 (3rd beat) sleep c+md*2 ct(x,mf,md) #b7 sleep md*2+ps ct(x,p,0) #b9 sleep md*6 ct(x,mf,md*3) #b15 sleep md*3 ct(x,p,md*2) #b18 sleep md*5 ct(x,f,md*2) #b23 sleep md*4 ct(x,ff,md*2) #b27 if i<4 then sleep md*2 ct(x,mp,md*2) #b29 sleep md*3+ps else sleep md*5+ps end end #end of volume level control thread #all 4 parts play together in seperate threads in_thread do pl(n1,d1) end in_thread do pl(n2,d2) end in_thread do pl(n3,d3) end pl(n4,d4) end #of 4 times loop end #end with fx_level end #end with fx_reverb
This program plays In Dulci Jubilo in four parts, for which it uses a technique I have utilised many times in my programs, namely to put the notes and note durations of each part into a pair of arrays e.g. n1 and d1, and then to use a defined function pl to traverse these arrays zipped together and to extract in turn the corresponding note and duration values and to use the standard play command to play them, sleeping thereafter for the duration of the note before returning to process the next pair. The notes are entered symbolically, and the durations in terms of defined variables such as q,c,and m for quaver, crotchet and minim. As usual, I put the individual parts into threads so that they can be played together.
The new addition is to wrap the whole playing process inside a loop
with fx_level: do |x| .......... end
and also to put inside that loop an extra thread containing commands of the form
control x,amp: nn, amp_slide: nnn
Such commands are used to alter the value of the amp: level to be applied.
To save typing I actually use a defined function
define :ct do |ptr,lev,slid=0| #this reduces the typing required for the control commands control ptr,amp: lev,amp_slide: slid end
to do the settings, so the commands take the form
Which would apply the control x to set the amp: to mf (mezzo-forte given a defined value at the start of the program), and moving or sliding to that level from the previously defined one over a time m (the time duration of a minim, again defined previously in the program)
If the change is instantaneous, the third parameter can be set at 0, or indeed missed out, when 0 will be the default parameter value supplied from the definition of the ct function.
If you study the listing, you will see the volume control thread, consisting of multiple ct(…) statements, separated by sleep statements, so that the dynamic changes that the ct commands supply take place at the right time as the music plays. To aid the checking of this, I put in as comments bar numbers at relevant places, e.g. #b18
In the example, the tune is repeated four times as their are four verses in the carol. The last time, an if statement is used to change the dynamic settings supplied by the ct commands so that the final diminuendo is omitted, and the piece finishes ff. Finally the entire playing section also has a with_fx :reverb wrapper so that some reverb is applied to make it sound a bit more interesting.
Linking Workspaces together using cue: and sync:
Large scale example audio recording of Sonic Pi playing seven workspaces automatically end to end has been added here
I am very excited about the second technique which I discovered this morning. I have for a long time wondered if it would be possible to link together teh code in differnent workspaces and to get them to play continuously one after another, without having to manually switch and then press run, with an inevitable gap whilst this was done. I thought I would try and see if teh cue: and sync: commands introduced in version 2 might be helpful, and I was amazed to find that it was possible to send a cue from one workspace and have it give a sync in another one. This enables you to play a series of programs together without a gap, and is also very useful if you hit (as I have) the limitation on the maximum code size that can be contained in one workspace because of the limitations in the present method whereby sound “messages” are sent from the workspace to the synthesizer scsynth producing the final sound. I was able to take one of my earliest pieces, a rendition of a movement from Bach’s 2nd Brandenburg Concerto which I had had previously to split into to halves and play manually one after the other with a slight gap, and was now able to link together so that they played automatically one after the other.
Again an example is probably the best way to explain how to use this technique.
You don’t have to use adjacent workspaces, and indeed the second workspace can be physically at a lower number that the first one if you want. However in this case I will use workspaces one and two.
Place the following short program in workspace 1
#WS1 #The function below reads two arrays zipped together and uses the play #command to play them one by one, sleeping for the note's duration #after each one is played define :pl do |notes,durations| notes.zip(durations).each do |n,d| play n, sustain: 0.9*d, release: 0.1*d sleep d end end q=0.1 #defined the duration of a quaver n1=scale(:c4,:major,num_octaves: 2) #n1 contains teh notes for 2 octaves of C4 major d1= [q]*16 #all 16 notes have quaver duration 0.1 seconds pl(n1,d1) #play the notess using the defined pl function cue :two #cue the program in workspace 2
and the next one in workspace two
#WS2 #don't need to define pl Still active from WS1 sync :two #when the cue is received the program progresses from here q=0.1 #DO need to define q again n1=scale(:g4,:major,num_octaves: 2).reverse #2 octaves descending this time d1= [q*2]*16 #each note lasts 2 quavers, so plays more slowly pl(n1,d1) #play the descending scale (uses the definition from program 1)
Now select workspace 2 and press run. The program will start but will not progress as it is waiting for a sync signal.
Switch to workspace 1 and press run again. The program in workspace one runs, playing an ascending scale. However, as soon as it finishes it sends a cue named :two which is picked up by the waiting program in workspace 2 by the sync :two command, and immediately this program runs, playing a descending scale at a slower pace.
So you still have to switch to the second workspace and start the program running, but when you run the program in workspace one it automatically links to the waiting program in the second workspace and runs that.
There are two further points to note. If you have one or more functions defined in the first workspace, then when the program is run, these are stored in memory, and the remain available to the second program. So if this uses the same function, you do not need to redefine it. The same does NOT appear to be true for variables defined in the first program. Their scope is limited to that program, so if they are required in the second program, then they have to be redefined there. You CAN pass variables if they are defined as global by starting the name of the variable with a $ sing. Thus Sq would work and be available in the second program but q would not.