Sonic Pi remote gui to control play/stop and the starting position within a piece

Following on from my previous post, and some “playing” with the midi facilities in the latest Sonic Pi 2.12.0-midi-alpha1 I developed a processing script to control the transport mechanism in Logic Pro which I was using to play the Sonic Pi produced midi. I have now further developed this, and applied its use to the existing release version Sonic Pi 2.11.1 so that it can provide a remote control functionality. (You can also use it on the version 2.11 with a slight modification to two or three lines. This is the current release version on the Raspberry Pi). With the addition of a few lines to any existing Sonic Pi music program file it can provide play and stop functions, which can be used repeatedly without touching Sonic Pi . Effectively the stop function stops the program from running and then re-runs it so that it awaits a subsequent “play” command from the remote control. If the music is a linear piece of say 200 bars, then with some more pervasive changes, the music code can be written in such a way that it is possible to start at the beginning of any specified bar in the music, and this bar selection can also be done from the remote control.

In this article, I am going to show two separate pieces which utilise the remote control. The first is a rendition of a four part round of Frére Jaques with a twist! At the end of each repeated line the tempo increases until the fourth part has finished playing, then the round plays again, this time starting at the new fast tempo, and decreasing at the end of each repeated line until the fourth entry finishes at the original slow tempo. The piece is 28 bars in length, and you can start playing at the beginning of any designated bar, controlled by the remote interface. To see how this is developed, first here is the code for a play-through of Frére Jaques for a single part, first speeding up, then slowing down again

#FrereJaques-1part.rb
a1=[ ]
b1=[ ]
a1[0]=[:c4,:d4,:e4,:c4,:c4,:d4,:e4,:c4]
a1[1]=[:e4,:f4,:g4,:e4,:f4,:g4]
a1[2]=[:g4,:a4,:g4,:f4,:e4,:c4,:g4,:a4,:g4,:f4,:e4,:c4]
a1[3]=[:c4,:g3,:c4,:c4,:g3,:c4]
a1[4]=[:r]*8
a1[5]=[:r]*8
a1[6]=[:r]*8+a1[0]
a1[7]=a1[1]
a1[8]=a1[2]
a1[9]=a1[3]
a1[10]=a1[4]
a1[11]=a1[5]
a1[12]=[:r]*8
b1[0]=[1,1,1,1,1,1,1,1]
b1[1]=[1,1,2,1,1,2]
b1[2]=[0.5,0.5,0.5,0.5,1,1,0.5,0.5,0.5,0.5,1,1]
b1[3]=[1,1,2,1,1,2]
b1[4]=[1]*8
b1[5]=[1]*8
b1[6]=[1]*8+b1[0]
b1[7]=b1[1]
b1[8]=b1[2]
b1[9]=b1[3]
b1[10]=b1[4]
b1[11]=b1[5]
b1[12]=[1]*8

c1=[100,120,140,160,180,200,220,200,180,160,140,120,100]
use_synth :beep
in_thread do
 for i in 0..a1.length-1
 use_bpm c1[i]
 for j in 0..a1[i].length-1
 play a1[i][j],sustain: b1[i][j]*0.9,release: b1[i][j]*0.1
 sleep b1[i][j]
 end
 end
end

In order to accommodate the tempo changes, the notes are held in two bar sections, inside an array a1[ ]. Thus the first two bars of the tune :c4, :d4, :e4, :c4, :c4, :d4, :e4, :c4 are held in the first entry of this array, a1[0]. The corresponding note lengths, are held in the first entry of array b1[ ] in b1[0] consisting of 8 equal notes of duration 1 (a crotchet). These two bars will be played at the first temp in the list
c1=[100,120,140,160,180,200,220,200,180,160,140,120,100]
namely 100 bpm. At the end of the data section, a synth is chosen (:beep) and the notes are played in a thread, as we will want all four parts to play together later on. Two”for” loops are used to play the two bar sections one after another. The outer loop selects the tempo in bpm for each two bar section, and the inner loop uses a play command to play the note with its accompanying duration, with sustain, release and pan parameters included. Each two lines the tempo is bumped up by 20. When the tune finishes playing (a1[3] and corresponding b1[3]) then three further two bar sections follow, each playing rests as the remaining three parts finish playing. In fact  the last of these three additional sections is 4 bars in length (a3[6] and b3[6]). The additional 2 bars consists of the first line of the tune (a1[0] and b1[0] which is played again, this time at the tempo of 220, and you will see that subsequent two bar sections consist of the remainder of the tune being played again, but this time the value of the tempo is reduced by 20 for each section, until the final section is played at the original tempo of 100. As before three “rest” sections are included, so allowing the other three parts to finish off.

If we now add in the other three parts, you will see that they are very similar to the first part. The only difference is that the tune is shifted for each part so that it actually starts play 2 bars later than for the previous part. So part2 which is held in the arrays a2[ ] and b2[ ] has a rest section for its initial 2 bars before the tune starts playing this time in a2[1] compared to a1[0] for the first part. Similarly the actual tune for part3 start playing in a3[2][and for the 4th part in a4[3]. The complete tune playing code is show below for all 4 parts. Each part has a different synth and pan setting to make them stand out.

#4 Frere Jaques round played twice, speeds up then slows down
p1=-1;p2=-0.33;p3=0.33;p4=1

a1=[]
b1=[]
a1[0]=[:c4,:d4,:e4,:c4,:c4,:d4,:e4,:c4]
a1[1]=[:e4,:f4,:g4,:e4,:f4,:g4]
a1[2]=[:g4,:a4,:g4,:f4,:e4,:c4,:g4,:a4,:g4,:f4,:e4,:c4]
a1[3]=[:c4,:g3,:c4,:c4,:g3,:c4]
a1[4]=[:r]*8
a1[5]=[:r]*8
a1[6]=[:r]*8+a1[0]
a1[7]=a1[1]
a1[8]=a1[2]
a1[9]=a1[3]
a1[10]=a1[4]
a1[11]=a1[5]
a1[12]=[:r]*8
b1[0]=[1,1,1,1,1,1,1,1]
b1[1]=[1,1,2,1,1,2]
b1[2]=[0.5,0.5,0.5,0.5,1,1,0.5,0.5,0.5,0.5,1,1]
b1[3]=[1,1,2,1,1,2]
b1[4]=[1]*8
b1[5]=[1]*8
b1[6]=[1]*8+b1[0]
b1[7]=b1[1]
b1[8]=b1[2]
b1[9]=b1[3]
b1[10]=b1[4]
b1[11]=b1[5]
b1[12]=[1]*8

c1=[100,120,140,160,180,200,220,200,180,160,140,120,100]
use_synth :beep
in_thread do
 for i in 0..a1.length-1
 use_bpm c1[i]
 for j in 0..a1[i].length-1
 play a1[i][j],sustain: b1[i][j]*0.9,release: b1[i][j]*0.1,pan: p1
 sleep b1[i][j]
 end
 end
end

a2=[]
b2=[]
a2[0]=[:r]*8
a2[1]=[:c4,:d4,:e4,:c4,:c4,:d4,:e4,:c4]
a2[2]=[:e4,:f4,:g4,:e4,:f4,:g4]
a2[3]=[:g4,:a4,:g4,:f4,:e4,:c4,:g4,:a4,:g4,:f4,:e4,:c4]
a2[4]=[:c4,:g3,:c4,:c4,:g3,:c4]
a2[5]=[:r]*8
a2[6]=[:r]*8+a2[0]
a2[7]=a2[1]
a2[8]=a2[2]
a2[9]=a2[3]
a2[10]=a2[4]
a2[11]=a2[5]
a2[12]=[:r]*8
b2[0]=[1]*8
b2[1]=[1,1,1,1,1,1,1,1]
b2[2]=[1,1,2,1,1,2]
b2[3]=[0.5,0.5,0.5,0.5,1,1,0.5,0.5,0.5,0.5,1,1]
b2[4]=[1,1,2,1,1,2]
b2[5]=[1]*8
b2[6]=[1]*8+b2[0]
b2[7]=b2[1]
b2[8]=b2[2]
b2[9]=b2[3]
b2[10]=b2[4]
b2[11]=b2[5]
b2[12]=[1]*8

c2=[100,120,140,160,180,200,220,200,180,160,140,120,100]
use_synth :blade
in_thread do
 for i in 0..a2.length-1
 use_bpm c2[i]
 for j in 0..a2[i].length-1
 play a2[i][j],sustain: b2[i][j]*0.9,release: b2[i][j]*0.1,pan: p2
 sleep b2[i][j]
 end
 end
end

a3=[]
b3=[]
a3[0]=[:r]*8
a3[1]=[:r]*8
a3[2]=[:c4,:d4,:e4,:c4,:c4,:d4,:e4,:c4]
a3[3]=[:e4,:f4,:g4,:e4,:f4,:g4]
a3[4]=[:g4,:a4,:g4,:f4,:e4,:c4,:g4,:a4,:g4,:f4,:e4,:c4]
a3[5]=[:c4,:g3,:c4,:c4,:g3,:c4]
a3[6]=[:r]*8+a3[0]
a3[7]=a3[1]
a3[8]=a3[2]
a3[9]=a3[3]
a3[10]=a3[4]
a3[11]=a3[5]
a3[12]=[:r]*8
b3[0]=[1]*8
b3[1]=[1]*8
b3[2]=[1,1,1,1,1,1,1,1]
b3[3]=[1,1,2,1,1,2]
b3[4]=[0.5,0.5,0.5,0.5,1,1,0.5,0.5,0.5,0.5,1,1]
b3[5]=[1,1,2,1,1,2]
b3[6]=[1]*8+b3[0]
b3[7]=b3[1]
b3[8]=b3[2]
b3[9]=b3[3]
b3[10]=b3[4]
b3[11]=b3[5]
b3[12]=[1]*8

c3=[100,120,140,160,180,200,220,200,180,160,140,120,100]
use_synth :tri
in_thread do
 for i in 0..a3.length-1
 use_bpm c3[i]
 for j in 0..a3[i].length-1
 play a3[i][j],sustain: b3[i][j]*0.9,release: b3[i][j]*0.1,pan: p3
 sleep b3[i][j]
 end
 end
end

a4=[]
b4=[]
a4[0]=[:r]*8
a4[1]=[:r]*8
a4[2]=[:r]*8
a4[3]=[:c4,:d4,:e4,:c4,:c4,:d4,:e4,:c4]
a4[4]=[:e4,:f4,:g4,:e4,:f4,:g4]
a4[5]=[:g4,:a4,:g4,:f4,:e4,:c4,:g4,:a4,:g4,:f4,:e4,:c4]
a4[6]=[:c4,:g3,:c4,:c4,:g3,:c4]+a4[0]
a4[7]=a4[1]
a4[8]=a4[2]
a4[9]=a4[3]
a4[10]=a4[4]
a4[11]=a4[5]
a4[12]=[:c4,:g3,:c4,:c4,:g3,:c4]
b4[0]=[1]*8
b4[1]=[1]*8
b4[2]=[1]*8
b4[3]=[1,1,1,1,1,1,1,1]
b4[4]=[1,1,2,1,1,2]
b4[5]=[0.5,0.5,0.5,0.5,1,1,0.5,0.5,0.5,0.5,1,1]
b4[6]=[1,1,2,1,1,2]+b4[0]
b4[7]=b4[1]
b4[8]=b4[2]
b4[9]=b4[3]
b4[10]=b4[4]
b4[11]=b4[5]
b4[12]=[1,1,2,1,1,2]

c4=[100,120,140,160,180,200,220,200,180,160,140,120,100]
use_synth :saw
in_thread do
 for i in 0..a4.length-1
 use_bpm c4[i]
 for j in 0..a4[i].length-1
 play a4[i][j],sustain: b4[i][j]*0.9,release: b4[i][j]*0.1,pan: p4
 sleep b4[i][j]
 end
 end
end

So now that we have the 4 part tune set up, how can we control it, so that we can play and stop at will, and also specify at which bar of the 28 available we start playing? In order to do this we need to supply externally two pieces of information. First a play/stop code (which is set to 1 to play and -1 to stop) and secondly a bar start code bs (which is set to the bar number from 1…28). In fact we can set bs greater than 28, and the program will determine that that number is too big and clamp it to a maximum of 28 (or whatever the relevant value is for the piece we are playing). So we have two problems to look at. First, generating the play/stop code and the bs code and sending them to Sonic Pi from our remote GUI, and secondly receiving and decoding them in the Sonic Pi piece we are playing, and acting upon them.

As a break from Sonic Pi, we will look first at the remote control GUI. Last summer, I was introduced to processing by Hiroshi Tachiban, who had written a script using this application to convert midi files to Sonic Pi code, via an intermediate MusicXML format. I have used and developed this script, and in the process have begun to appreciate the power of Processing. In the conversion script its graphical properties are not utilised, but looking at the examples on the processing site (processing.org) and prompted by others who were using it to generate OSC commands (which are utilised by Sonic Pi to communicate between its various parts, GUI, Server, Scsynth). I decided to try and use it for this purpose. It also has the advantage of being easily installable on Macs, Windows PC, linux and Raspberry Pi, so it can be utilised on all the Sonic Pi platforms. Basically I deiced to use a very small GUI screen which had 5 small clickable rectangles on it. Two of these were used to select Play or Stop, and the remaining three let you increase the current (displayed) bar start, or decrease it, or reset it to bar 1 (the start of the piece). The current bar start was held internally in the GUI code, and transmitted to Sonic Pi whenever the Play or Stop rectangle was clicked. This enabled subsequent Play commands to start at the current bar start setting repeatedly, without having to set it each time. Processing refers to the graphics window it sets up using an xy coordinate system where the origin x=0,y=0 is top left, and x increasing moves right across the screen, and y increasing moves down the screen. I set a small screen size of 100 x 120 pixels, as I didn’t want the GUI to use much screen real estate. Processing has built in routines to determine the mouse position mouseX and mouseY. It can also determine when the mouse button is pressed, and also when the mouse is moved. The program for the processing window is called a sketch. Those of you who have used the Arduino interface on a Mac or Windows will be at home with the programming interface as the Arduino interface is written using processing. Both use JAVA. Unfortunately the code would not render properly in WordPress, so you can see it by using this paste-link instead

To try out the program, you first need to download the processing code from processing.org There are versions available for Mac Windows PC linux and Raspberry Pi (use the linux ARM version). Unzip and run the application. Create a new sketch called StartBarSelector and paste in the code. You will also need to load in the oscP5 library from the Sketch–>Import Library— link on the Edit menu. I suggest you run the app initially from the processing ide. Later when you are sure it is working OK you can create a standalone app using Export Application… from the File Menu.

If you view the code you will see that the first entry is import oscP5.*;
This utilises an external library oscP5 which you must add as detailed above. The script creates a small (100x120pixel) window which contains 5 small rectangles. These are set up as clickable zones, and trigger various operations when they are clicked. The three rectangles in a vertical line, control the value of a variable bs. This is initially set to 1, but can have its value increased by clicking the top rectangle marked bs+. The bottom rectangle marked bs- decreases its value, provided that it is greater than 1, and the central rectangle resets its value to 1. When one of these rectangles is clicked it turns green and remains so until the mouse button is released. The rectangle on the right is filled in red and has the label play. When it is clicked it sends the value 1 to Sonic Pi, together with the current value of the bs (bar start) variable. When it is clicked it will also highlight the left hand rectangle in red and reveal the caption stop, whilst removing its own caption and recolouring the play rectangle to the background colour. When the blue left hand rectangle (∫) is clicked it sends the value -1 to Sonic Pi, together with the current value of the bs variable.
Much of the code deals with the generation of the rectangles and the captions, and also the detection of the mouse pointer position when the mouse is clicked.  The initial function setup creates the window and the rectangles and sets their initial states and colours. It also sets up and OSC udp socket which is used to communicate with Sonic Pi which is assumed to be running on the local machine, although the controller will work with a remote Sonic Pi, provided that it is run with the appropriate address inserted for SonicPi in the program. The display is set to refresh 60 times a second.

The second function sendOscData composes the OSC message to be sent to Sonic Pi. This consists of the “address” /transport followed by two integer arguments tr, which is either 1 for play or -1 for stop, and bs which is an integer specifying the start bar to use.

The third function draw saves the mouse position given by mouseX and mouseY to two variables mx and my and then proceeds through a series of tests which detect whether the mouse was clicked inside one of the five rectangles, and if so, sets the values of the tr and bs variables as appropriate. In the case of the three “bs” rectangles it also shades the rectangle green, by repainting it in that colour, and in the case of the stop and play rectangles it clears the current rectangle colour and caption and enables the colour fill and caption for the other one, again repainting the rectangles and pasting a background filled area to hide the captions and then rewrite the appropriate one. These two sections of code also send OSC messages with the current data to Sonic Pi.

After these sections of code which test where the mouse has been clicked, 6 lines of code paste over the old displayed bs value and then repaint the current value. When the mouse button is released, the bottom section of the draw loop activates when the mouse button is released, and removes the green fill from all of the “bs” rectangles. A flag variable also makes sure that only one OSC message is sent when a click in the play or stop rectangles is detected, even though the mouse button may remain depressed for several passes of the draw loop. Finally a small delay is added, which is set so that the bs value will increase fairly rapidly as the bs+ button is clicked and the mouse held down, whilst allowing a sufficient delay so that it can be quickly clicked to give a single increment in the value.

Now we turn our attention back to Sonic Pi. In order to detect the OSC messages being sent from the BarStartSelector GUI, I have used an undocumented feature of Sonic Pi, which is that it can respond to cues received in the form of OSC messages which are sent to port 4559. There is a slight difference in that response between SP version 2.11 (the current release version on the Raspberry Pi) and SP version 2.11.1 (the current release version on the Mac and Windows PC). However it only requires a minor change to a few lines in the program. Here Sam Aaaron the Lead Developer of Sonic Pi would want me to point out that this is an experimental feature and that it may well change in form, or even disappear in future versions, although currently it is still the same in the latest development version 2.12.0-midi-alpha, and of course is fixed in SP 2.11 and 2.11.1

A simple program which can be used to test the operation of the external GUI is shown below. It also illustrates the small code difference required between SP version 2.11 and 2.11.1 in decoding the received messages.

#test communication with StartBarSelector processing GUI
define :ver do
 return version.to_s.split('.')
end

loop do
 v= version
 tr=0
 until tr==1
 s=sync '/transport'
 if version="v2.11.1" or ver[2].to_i > 11
 tr=s[0]
 bs=s[1]
 else
 tr=s[:args][0]
 bs=s[:args][1]
 end
 
 puts "play/stop tr variable is "+tr.to_s
 puts "bs bar start variable is "+bs.to_s
 end
 
 until tr==-1
 s=sync '/transport'
 if version="v2.11.1" or ver[2].to_i > 11
 tr=s[0]
 bs=s[1]
 else
 tr=s[:args][0]
 bs=s[:args][1]
 end
 
 puts "play/stop tr variable is "+tr.to_s
 puts "bs bar start variable is "+bs.to_s
 end
end

The operative line is s=sync ‘/transport’
This acts like a standard sync command in Sonic Pi, only this time it wait until an OSC message with the address ‘/transport’ is received on port 4559. Depending on the SP version the arguments added to this OSC message are extracted and displayed by puts statements. You can see how tr is set to 1 when play is clicked and -1 when stop is pressed and in each case the bs value is also sent.

So all that remains is to add similar code to the program we wish to control and then to figure out ways of getting the program to stop and to play again when a stop and play command are received in succession. The other problem to solve is how to set the program to start at a specified bar rather than at the beginning. The stop/play problem is solved by means of an external ruby helper program. This utilises the sonic-pi-cli gem which enables you to send commands to sonic pi from a command line. First you have to install it. On a Mac this should be done using the system ruby installation and for the root user, using the command
sudo /usr/bin/gem install sonic-pi-cli
This should install /usr/local/bin/sonic_pi which should automatically be placed in your PATH so that it can be accessed with sonic_pi. If you are using a Raspberry Pi then you can use the procedure below:
Start a terminal window and type the following:

sudo mkdir /var/lib/gems
sudo chown pi /var/lib/gems
sudo chown pi /usr/local/bin
gem install sonic-pi-cli

When the install has completed, reset the ownership of /usr/local/bin

sudo chown root /usr/local/bin

It is a good idea to test it at this stage. Get any piece you like running in Sonic Pi, then from a command line type sonic_pi stop and the sonic pi cli should stop the program from running. Also if you type sonic_pi by itself you should get some information back from the program.
On my system, Sonic Pi programs to play are stored in ~/Documents/SPfromXML and the program we are going to control is called FrereJaquesControlled-RF.rb with a path of
~/Documents/SPfromXML/FrereJaquesControlled-RF.rb
I put the -RF on the end of the file name to remind me that the file is too long to run in a Sonic Pi buffer, and requires to be run from Sonic Pi using the command
run_file “~/Documents/SPfromXML/FrereJaquesControlled-RF.rb”
Similarly the helper program which I use is stored in the same folder and is called FrereJaquesControlled-RFauto.rb It has the contents below

#!/usr/bin/ruby
`/usr/local/bin/sonic_pi stop`
`/usr/local/bin/sonic_pi "run_file '~/Documents/SPfromXML/FrereJaquesControlled-RF.rb'"`

This sends two commands to Sonic Pi via the sonic_pi gem. The first causes Sonic Pi to stop all running programs. The second issues a run_file command which restarts the FrereJaques program again from the beginning.
Now we are in a position to build up the FrereJaquesControlled-RF program. We start with the four part round introduced at the beginning of this post. We have seen how we can wait for and detect a sync OSC message which can send us a command to start the program, but also the bar at which to start. So the next problem is how can we set up the program to react to this?
What we need to do is to determine which of the 13 sections a1[0] to a1[12] we need to select, and whether we are starting at the beginning of that selected section or in the middle of it. In order to help us we add some functions at the beginning of the program. The complete FrereJaquesControlled-RF.rb program is listed below

#FrereJaquesControlled.rb
restart="~/Documents/SPfromXML/FrereJaquesControlled-RFAuto.rb"
use_debug false #turn off log_synths
use_arg_checks false #turn off log_cues
bs=1 #starting bar number: give it an initial value here
bpba=[4]*13 #setup up list of section beats per bar

#puts bpba
st=[] #holds info for start section and remaining bars to process: set global here
#part pan positions
p1=-1;p2=-0.33;p3=0.33;p4=2

############### define functions used in the script
define :numbeats do |durations| #return number of crotchet beats in a note durations list
 l=0.0
 durations.each do |d|
 l+=d
 end
 return l
end

#find starting section, and number of bars in that section to be processed
#to determine the starting note index
define :startDetails do |bn,bNumberSecStart,durations|
 startSecIndex=0
 remainingBars=bn
 #iterate until remaning bn is within the section
 while bn>bNumberSecStart[startSecIndex]
 remainingBars=bn-bNumberSecStart[startSecIndex]
 startSecIndex+=1
 end
 #return the section to start playing and number of bars to determine starting note index
 return startSecIndex-1,remainingBars
end

define :getmatchd do |bn,bpb,durations| #works out the note index for a given bar number
 matchbeat=(bn-1)*bpb #target number of beats to find
 l=0.0;x=0
 until l>=matchbeat || (l-matchbeat).abs < 0.0625 #0.0625 is smallest quantisation to consider
 l+=durations[x]
 x+=1
 end
 return [x ,l-matchbeat] #return the matched beat note index, plus sleep for tied note (if any)
 #nb if the bar start coincides with a tied note, then the part will start with the next
 #note and a sleep command will be issued for the remaining duration of the tied note
end

define :ver do
 return version.to_s.split('.')
end
##########################
#wait for an OSC cue to be received from the Processing GUI sketch
#This sends two parameters: First controls Play (1) Stop (-1)
#second gives requested bar start number
tr=0
until tr==1 #wait for PLAY cue from processing GUI (first parameter will be set to 1)
 s=sync '/transport'
 if version="v2.11.1" or ver[2].to_i > 11
 tr=s[0]
 bs=s[1]
 else
 tr=s[:args][0] #tr governs play/stop value is 1 for play -1 for stop
 bs=s[:args][1] #bs is start bar,the second parameter received
 end
end

puts "BS selected is "+bs.to_s
##########################
#start polling for an OSC cue to stop playing from the Processing GUI sketch
#this runs continuously in a thread
in_thread do #this thread polls for an OSC cue to stop the program
 tr=0
 until tr==-1 #the first parameter will be set to -1 for a STOP signal
 s=sync '/transport'
 if version="v2.11.1" or ver[2].to_i > 11
 tr=s[0]
 bs=s[1]
 else
 tr=s[:args][0] #tr governs play/stop value is 1 for play -1 for stop
 bs=s[:args][1] #bs is start bar,the second parameter received
 end
 end
 #stop command detected
 puts"stopping"
 puts "running sonic pi cli script to restart"
 
 system(restart+" &") #run the auto script to stop and rerun the code
end
##########################
with_fx :reverb, room: 0.8 do
 with_fx :level,amp: 0.7 do
 #part 1 data
 a1=[]
 b1=[]
 a1[0]=[:c4,:d4,:e4,:c4,:c4,:d4,:e4,:c4]
 a1[1]=[:e4,:f4,:g4,:e4,:f4,:g4]
 a1[2]=[:g4,:a4,:g4,:f4,:e4,:c4,:g4,:a4,:g4,:f4,:e4,:c4]
 a1[3]=[:c4,:g3,:c4,:c4,:g3,:c4]
 a1[4]=[:r]*8
 a1[5]=[:r]*8
 a1[6]=[:r]*8+a1[0]
 a1[7]=a1[1]
 a1[8]=a1[2]
 a1[9]=a1[3]
 a1[10]=a1[4]
 a1[11]=a1[5]
 a1[12]=[:r]*8
 b1[0]=[1,1,1,1,1,1,1,1]
 b1[1]=[1,1,2,1,1,2]
 b1[2]=[0.5,0.5,0.5,0.5,1,1,0.5,0.5,0.5,0.5,1,1]
 b1[3]=[1,1,2,1,1,2]
 b1[4]=[1]*8
 b1[5]=[1]*8
 b1[6]=[1]*8+b1[0]
 b1[7]=b1[1]
 b1[8]=b1[2]
 b1[9]=b1[3]
 b1[10]=b1[4]
 b1[11]=b1[5]
 b1[12]=[1]*8
 c1=[100,120,140,160,180,200,220,200,180,160,140,120,100]
 ###################### calculate starting data

 #calc bar offset for start of each tempo change. Held in bNumberSecStart list
 bNumberSecStart=[]
 bNumberSecStart[0]=0
 bNumber=0
 b1.length.times do |z|
 bNumber+= numbeats(b1[z])/bpba[z]
 bNumberSecStart[z+1]=bNumber
 end
 #puts bNumberSecStart #for debugging
 #calc number of bars inthe piece
 bmax=bNumberSecStart[b1.length]
 puts "Total number of bars="+bmax.to_s
 #adjust requested bar start number if too large
 if bs>bmax
 bs=bmax
 puts "Start bar exceeds piece length: changed to :"+bs.to_s
 end
 #calculate info for starting sector containing bar start requested,
 #and number of remaining bars to process to get starting index
 st=startDetails(bs,bNumberSecStart,b1)
 startSec=st[0]
 remainingBars=st[1]
 puts "Start Section="+st[0].to_s
 puts "Remaining Bars to find starting index="+st[1].to_s
 puts

 ################### now ready to process an play each part in turn (played together in threads)
 #each part is processed in exactly the same way

 #calc starting index and any sleep for tied notes for part 1
 sv1=getmatchd(remainingBars,bpba[startSec],b1[startSec])
 
 puts "1: "+sv1.to_s #print start index and sleep time
 use_synth :beep
 in_thread do
 for i in startSec..a1.length-1
 use_bpm c1[i]
 sleep sv1[1] #sleep for tied note (>0 if tied)
 for j in sv1[0]..a1[i].length-1
 play a1[i][j],sustain: b1[i][j]*0.9,release: b1[i][j]*0.1,pan: p1
 sleep b1[i][j]
 end
 sv1=[0,0] #reset so subsequent iterations of j loop in full and no tied sleep
 end
 end


 a2=[]
 b2=[]
 a2[0]=[:r]*8
 a2[1]=[:c4,:d4,:e4,:c4,:c4,:d4,:e4,:c4]
 a2[2]=[:e4,:f4,:g4,:e4,:f4,:g4]
 a2[3]=[:g4,:a4,:g4,:f4,:e4,:c4,:g4,:a4,:g4,:f4,:e4,:c4]
 a2[4]=[:c4,:g3,:c4,:c4,:g3,:c4]
 a2[5]=[:r]*8
 a2[6]=[:r]*8+a2[0]
 a2[7]=a2[1]
 a2[8]=a2[2]
 a2[9]=a2[3]
 a2[10]=a2[4]
 a2[11]=a2[5]
 a2[12]=[:r]*8
 b2[0]=[1]*8
 b2[1]=[1,1,1,1,1,1,1,1]
 b2[2]=[1,1,2,1,1,2]
 b2[3]=[0.5,0.5,0.5,0.5,1,1,0.5,0.5,0.5,0.5,1,1]
 b2[4]=[1,1,2,1,1,2]
 b2[5]=[1]*8
 b2[6]=[1]*8+b2[0]
 b2[7]=b2[1]
 b2[8]=b2[2]
 b2[9]=b2[3]
 b2[10]=b2[4]
 b2[11]=b2[5]
 b2[12]=[1]*8 
 c2=[100,120,140,160,180,200,220,200,180,160,140,120,100]
 #calc starting index and any sleep for tied notes for part 2
 sv2=getmatchd(remainingBars,bpba[startSec],b2[startSec])

 puts "2: "+sv2.to_s #print start index and sleep time
 use_synth :blade
 in_thread do
 for i in startSec..a2.length-1
 use_bpm c2[i]
 sleep sv2[1] #sleep for tied note (>0 if tied)
 for j in sv2[0]..a2[i].length-1
 play a2[i][j],sustain: b2[i][j]*0.9,release: b2[i][j]*0.1,pan: p2
 sleep b2[i][j]
 end
 sv2=[0,0] #reset so subsequent iterations of j loop in full and no tied sleep
 end
 end

 a3=[]
 b3=[]
 a3[0]=[:r]*8
 a3[1]=[:r]*8
 a3[2]=[:c4,:d4,:e4,:c4,:c4,:d4,:e4,:c4]
 a3[3]=[:e4,:f4,:g4,:e4,:f4,:g4]
 a3[4]=[:g4,:a4,:g4,:f4,:e4,:c4,:g4,:a4,:g4,:f4,:e4,:c4]
 a3[5]=[:c4,:g3,:c4,:c4,:g3,:c4]
 a3[6]=[:r]*8+a3[0]
 a3[7]=a3[1]
 a3[8]=a3[2]
 a3[9]=a3[3]
 a3[10]=a3[4]
 a3[11]=a3[5]
 a3[12]=[:r]*8
 b3[0]=[1]*8
 b3[1]=[1]*8
 b3[2]=[1,1,1,1,1,1,1,1]
 b3[3]=[1,1,2,1,1,2]
 b3[4]=[0.5,0.5,0.5,0.5,1,1,0.5,0.5,0.5,0.5,1,1]
 b3[5]=[1,1,2,1,1,2]
 b3[6]=[1]*8+b3[0]
 b3[7]=b3[1]
 b3[8]=b3[2]
 b3[9]=b3[3]
 b3[10]=b3[4]
 b3[11]=b3[5]
 b3[12]=[1]*8
 c3=[100,120,140,160,180,200,220,200,180,160,140,120,100]
 #calc starting index and any sleep for tied notes for part 3
 sv3=getmatchd(remainingBars,bpba[startSec],b3[startSec])

 puts "3: "+sv3.to_s #print start index and sleep time

 use_synth :tri
 in_thread do
 for i in startSec..a3.length-1
 use_bpm c3[i]
 sleep sv3[1] #sleep for tied note (>0 if tied)
 for j in sv3[0]..a3[i].length-1
 play a3[i][j],sustain: b3[i][j]*0.9,release: b3[i][j]*0.1,pan: p3
 sleep b3[i][j]
 end
 sv3=[0,0] #reset so subsequent iterations of j loop in full and no tied sleep
 end
 end

 a4=[]
 b4=[]
 a4[0]=[:r]*8
 a4[1]=[:r]*8
 a4[2]=[:r]*8
 a4[3]=[:c4,:d4,:e4,:c4,:c4,:d4,:e4,:c4]
 a4[4]=[:e4,:f4,:g4,:e4,:f4,:g4]
 a4[5]=[:g4,:a4,:g4,:f4,:e4,:c4,:g4,:a4,:g4,:f4,:e4,:c4]
 a4[6]=[:c4,:g3,:c4,:c4,:g3,:c4]+[:r]*6 #Tied note added here: to show how its dealt with start at bars 14 then 15
 a4[7]=a4[1]
 a4[8]=a4[2]
 a4[9]=a4[3]
 a4[10]=a4[4]
 a4[11]=a4[5]
 a4[12]=[:c4,:g3,:c4,:c4,:g3,:c4]
 b4[0]=[1]*8
 b4[1]=[1]*8
 b4[2]=[1]*8
 b4[3]=[1,1,1,1,1,1,1,1]
 b4[4]=[1,1,2,1,1,2]
 b4[5]=[0.5,0.5,0.5,0.5,1,1,0.5,0.5,0.5,0.5,1,1]
 b4[6]=[1,1,2,1,1,4]+[1]*6 #Tied note added here: to show how its dealt with start at bars 14 then 15
 b4[7]=b4[1]
 b4[8]=b4[2]
 b4[9]=b4[3]
 b4[10]=b4[4]
 b4[11]=b4[5]
 b4[12]=[1,1,2,1,1,2]
 c4=[100,120,140,160,180,200,220,200,180,160,140,120,100]
 #calc starting index and any sleep for tied notes for part 4
 sv4=getmatchd(remainingBars,bpba[startSec],b4[startSec])

 puts "4: "+sv4.to_s #print start index and sleep time

 use_synth :saw
 in_thread do
 for i in startSec..a4.length-1
 use_bpm c4[i]
 sleep sv4[1] #sleep for tied note (>0 if tied)
 for j in sv4[0]..a4[i].length-1
 play a4[i][j],sustain: b4[i][j]*0.9,release: b4[i][j]*0.1,pan: p4
 sleep b4[i][j]
 end
 sv4=[0,0] #reset so subsequent iterations of j loop in full and no tied sleep
 end
 end

 end #level
end #fx

At the beginning of the program restart is set as a variable holding the command to run the FrereJaquesControlled-RFauto program referred to above. The next two lines turn off some of the output in the log to make it eaieer to see some of the printed statements the program produces. bpba is an array or list which holds the number of beats per bar for each of the 13 sections in the piece. In this example the time signature 4/4 or 4 crotchets per bar is constant throughout, but the code will handle pieces where the time signatures changes between sections, as is the case in the second example detailed later on. In this case each of the 13 entries is set to 4.
st[ ] is an array which will hold details about the starting  section and the bars to process to determine the starting note. p1 to p4 are the pan positions for each of the four parts.

The first additional procedure is numbeats. This calculates the number of crotchet beats in a given section. Thus puts numbeats(b1[0]) would print 8 in the log window, as there are 8 crotchets in the first section, but puts numbeats(b1[6]) would give 16 as this middle section is twice as long as the others.

The second added procedure startDetails determines which section we need to start playing from, and how many bars remain to be processed from the start of this section in order to determine the index or position of the first note to be played. Although it is listed at this point, to keep the procedure definitions together, it requires some data which can only be ascertained after the note and duration data of the first part have been defined. If you look just below that point in the program listing you will see some code which sets up the list bNumberSecStart  This calculates and holds the starting bar number of each section, but counting from 0 rather than 1. Basically it calculates the number of beats in each section and divides it by the number of beats per bar for that section which is held in the bpba array referred to above. Also in this section of the program the maximum number of bars in the program bmax is calculated, and it is used to limit the requested bar bs if this is too high. Returning to the  startDetails function,  this is fed with three parameters. The start bar requested bs, the array list of starting bar number for each section bNumberSecStart and the array b1 which holds the lists of durations for the 13 sections b1[0] to b1[12]
It calculates which section to start from by subtracting the number of bars from each section in turn from the value of bs, until the remaining number is less than the number in the current section. The starting section is then one less than this number, and the remaining bars are those from which to work out what the starting note index for the bar request is.

This task is now passed to the third additional procedure, getmatchd. This will be passed three parameters. bn is the number of bars remaining to be processed, after removing those which have already taken place in the previous sections, the beats per bar bpb for the current section, and the list of durations for that section (durations) eg b1[6] if we are going to start within that section. matchbeat is set to the target beat to find within the section. The -1 adjusts for the fact that the start bar counts from 1 whereas the calculations we have done essentially on elapsed bars starts counting from 0. We now set up two variables l and x. x indexes the position starting from 0 as we move though the section in a loop while l holds a running total of the duration within the loop. We continue going round the loop until the increasing value of l matches the target beat count in matchd within an error less than the smallest note duration we will use, OR until l just exceeds this target. This latter case will occur if there is a tied note over the target beat. For example if we have two bars each with four crotchets and the fourth crotchet is tied to the fifth one “over the bar line” which is the target start bar so that we sound a minim note, then the match will actually be after the fifth crotchet a the start of the sixth crotchet. In this case, we will start that part playing at the sixth crotchet, but we will insert a rest so that it is delayed and will start playing in synchronism with the other 3 parts. So the procedure getmatchd will return two pieces of information. First the starting note index to be played in the section, and secondly the rest value (if any) required, which will allow for a tied note match.

The final small procedure uses some ruby code to split the returned version number of the Sonic Pi being utilised and enable us to determine whether it is version 2.11 or 2.11.1 and so adjust the OSC sync code as shown previously. In the first part, we wait for an OSC sync to be received containing the play command code tr=1. Once this has been received we proceed to the next part which is now placed in a thread, so that it can continue to operate as the rest of the program proceed on its way. This thread waits for another OSC message to be received, this time with tr set to -1 the stop code. When this is received it uses a system command to call the FrereJaquesControlled-RFauto.rb program using the command line variable restart setup at the start of the program. As discussed, this will cause the program to stop and then rerun, awaiting another play command from the remote GUI.

The remainder of the program is largely the same as the 4 part round we discussed towards the beginning of this article. However there are one or two changes. First the whole program is wrapped in two fx calls. The first with_fx :reverb adds some reverb to the round as it is played and the second with_fx :level sets an overall :amp value of 0.7 for the volume. Below the data for the first part is the extra code to work out bNumberSectStart and bmax as discussed above. The startDetails procedure is called to work out the starting section and the number of bars to be processed for this part. In fact the same data can be used for all four parts in this example so it doesn’t have to be calculated more than once. The results are stored in the list st[ ] with startSec being set to st[0] and remainingBars to st[1] From there on each of the four parts is processed in exactly the same way.

First we use the getmachd procedure to find the starting note index and the sleep value (if any) to compensate for a tied note. This piece wouldn’t normally have any tied notes, but to illustrate what happens I have introduced one at the end of the first tune played in part 4. You will see this in the comments alongside the lines for a4[6] and b4[6]. You can compare them with the corresponding lines in the second listing in the article. We will discuss this further when the program is played. The call to the getmatchd procedure is sv1=getmatchd(remainingBars,bpba[startSec],b1[startSec]), the two values this returns are stored in sv1[0] the starting note index and sv1[1] the sleep value (if any)

Now all we have to do to adjust the starting note is to alter the starting point of the two loops i and j which control the playing of the notes. Instead of for i in 0..a1.length-1 we now have for i in startSec..a1.length-1 and instead of for j in 0..a1[i].length-1 we now have for j in sv1[0]..a1[i].length-1 Also just before the j loop we insert sleep sv1[1] If there is a tied note involved, this sleep value will be greater than 0 and will allow for the fact that this loop is starting a bit later than the other parts. Finally we reset the values in sv1 to 0 after the j loop has finished so that for subsequent sections all of the loop contents are used with j starting from 0 and with no sleep value before the section start. If you look at the code you will see that all the three remaining parts are processed in exactly the same way, each with their own sv values sv2, sv3 and sv4 calculated and applied.

So after that mammoth discussion we can try out the code. Becuase the program is so long it will not run directly in a Sonic Pi buffer. Instead, in an empty buffer you need to type

run_file “~/Documents/SPfromXML/FrereJaquesControlled-RF.rb”

obviously alter the path if you have the file stored in another folder. Make sure that the FrereJaquesControlled-RFauto.rb file is in place as well and that the restart variable is pointing to its correct location, and also that the auto file has the correct location of the FrereJaquesControlled-RF.rb file in it. It is easy to get one of these wrong, so check them carefully. Now start the StartBarSelector GUI and then run the Sonic Pi program. It wont do a lot as it is waiting for a play command from the GUI. Click on the red play rectangle and all being well it should start playing. Click on the red stop rectangle and it should stop AND relaunch the Sonic Pi program. All being well you can click on play again to restart it. If that doesn’t happen look at the debugging section later on. You should be able to play and stop at will. The buttons are highlighted to show which should be pressed next. You should also be able to alter the displayed start bar at any time. using the three rectangles provided. The next time you press play, the displayed value will be implemented. To see how the tied bar works, start from bar 14. You should here the tied note held over and playing at the same time as part 1 restarts for the second time. Now change and start from bar 15. You will here part 1 starting, but the second half of the tied note in part 4 is replaced with a sleep command and part four starts from next note (which is a rest) so you will here nothing from part 4 until it comes in (at the correct time) for the second time through. you can have a look at the screen output where you will see

“BS selected is 15”
“Total number of bars=28.0”
“Start Section=6”
“Remaining Bars to find starting index=3.0”

“1: [8, 0.0]”
“2: [8, 0.0]”
“3: [8, 0.0]”
“4: [6, 2.0]”

You will see that part 4 starts section 6 with an index of 6 with a sleep value of 2.0 corresponding to the overrun of the tied note, whereas the other three parts have 0.0 sleep value. Part 4 starts playing with index 6 which is the seventh entry in section 6, on the third beat of the third bar, whereas the other parts which all have  8 crotchet rests at the start of section 6 all start on the 9th entry (index 8) at the start of the third bar in that section. The sleep 2 will delay part 4 so that it starts the third beat in synchronism with the other three parts when they get there.

a4[6]=[:c4,:g3,:c4,:c4,:g3,:c4]+[:r]*6  #Tied note added here: to show how its dealt with start at bars 14 then 1
b4[6]=[1,1,2,1,1,4]+[1]*6   #Tied note added here: to show how its dealt with start at bars 14 then 15z

For comparison part 1 without the tied note is shown below

a1[6]=[:r]*8+a1[0]
b1[6]=[1]*8+b1[0]

Debugging
It can be quite tricky to sort things out if the system doesn’t work. From my experience the usual culprit is an incorrect path or filename in the places where these are included in the scripts. Check very carefully the saved names of the two programs FrereJaquesControlled-RF.rb and FrereJaquesControlled-RFauto.rb and the places where these are referenced in the main program and in the auto program.
You can check the behaviour of the auto program by running it from the  command line with
/usr/bin/ruby ~/Documents/SPfromXML/FrereJaquesControlled-RFauto.rb
You can check the StartBarSelector GUI with the test program detailed in the article. You can also run the GUI from the processing ide, and uncomment the debugging print statements in the program which will give you some output in the terminal window beneath the program.

You can down load all the programs in the project including the source file for the GUI (in text format) which you can paste into a new blank sketch window, and then save it as StartBarSelector. The link is here

Finally, there is also a second example playing a slightly longer piece by Monteverdi (Beatus Vir) which can also be controlled by the GUI. It incorporates a time signature change at bar 62 which is also handled by the software. Try starting two bars earlier to hear the time signature change take place from 4/4 to 6/4 time.
The files for this second example BeatusVirControlled-RF.rb and BEatusVirControlled-RFauto.rb are included with the link above.

I have recorded a series of 6 videos which you can watch which go through the operation of these  programs. You can access them here.