Two new techniques for Sonic Pi

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

ct(x,mf,m)

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.

The program example can be heard and accessed on soundcloud.com here
This also contains a link to a gist containing the code here

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.

Advertisements

Leave a Reply

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

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s