This project is one that I really enjoyed doing, as it combines such a variety fo processes together, to give an integrated control system for Matt Tytel’s superb software Synth Helm, controlled by midi signals from Sonic Pi, which in turn is controlled via OSC signals going to and from a table running TouchOSC. The system also involved delving into Ruby file handling commands which enabled me to retrieve all the patch names directly from the folders where they are stored by Helm. You can click the image below to see a larger copy.
Sonic Pi is extremely versatile. Not only can it produce sounds itslef using built in synths, but it can also send and receive midi singals, and send and receive OSC messages. It is based on teh Ruby language, and you can also use (with care) many Ruby commands in addition to those in the official Sonic Pi language detained in the help files.
Probably a couple of years ago I first came across Help and have used it with Sonic Pi, but I wanted to see if I could strealine the setup, particularly by allowing Sonic Pi to choose the current patch selected on Helm. It took a bit of playing around, and looking at the helm code to work out how to send midi signals to achieve this. In fact you have to send three signals in succession. The first is to midi control 0 with data giving the bank number. This specifies whether you are using Factory Presets, User Presets, or another collection altogether. Each of these sections is held in a separate folder on the computer. In this case I am only concerned with the supplied Factory Presets, which has a bank number of 0. The program could be used with a different bank instead, or with a little more modification to several alternative banks. The second signal is to midi control 32 and specifes the sections within the bank: in this case arp patches, bass patches, SFX patches etc. Each if these are contained in a separate folder within the main bank folder. The final midi command is a midi program command which selects the patch number. This corresponds to the index position (from 0) of the alphabetical listing of patches within the section folder.
These commands are sent via the patch function I have defined.
define :patch do |bank=0,folder=0,patch=0| #midi code to adjust patch. midi_cc 0,bank #always 0 in this version. Could expand to cover user bank midi_cc 32,folder #patch folder type index midi_pc patch #patch name index in folder end
After setting up some initial stuff such as logging defaults, the midi port and channel, the IP address of the TouchOS tablet. I then developed code to populate some lists with the names of the patches.
#select ONE of the next two lines as appropriate dirbase=(ENV['HOME']+"/Library/Audio/Presets/Helm/Factory Presets/") #path to helm presets (Mac) #dirbase=("/home/pi/.helm/patches/Factory Presets/") #path to helm presets (Raspberry Pi) define :getList do |type| #get patch names for a given type from their stored location files = Dir.glob(File.join(dirbase+type, '**', '*')).select{|file| File.file?(file)}.sort l=[] files.each do |f| #select one of the next two lines depending on OS used l<< f.split("/")[9][0..-6] #strip out just filename without .helm suffix (for Mac) #l<< f.split("/")[7][0..-6] #strip out just filename without .helm suffix (for RPi) end return l end define :getTypeList do #get list of patch types (these are folder names) tl=[] Dir.chdir(dirbase) do tl= Dir.glob('*').select { |f| File.directory? f }.sort #get sorted list of folders end return tl end typeList=getTypeList #store list of patch type folders l=[] #initialise list for patch names limits=[] #initialise list for index max limit for each patch type typeList.each_with_index do |type,i| #fill type lists and limits list l[i]=getList type limits[i]=l[i].length - 1 end plist=[l[0],l[1],l[2],l[3],l[4],l[5],l[6],l[7],l[8]] #plist is a list of the 9 patch type lists
This first defines the path to where the patch files are located, and then defines fa function :getList to read in the absolute file names from a specified folder, and to strip out the actual file name minus the .helm type suffix, and return these as comma separated string list. A second functions :getTypeList lists the folder names within the main bank folder (Factory Presets) and returns these as a list. (These are the types arp, bass,chip etc.)
With these two functions defined, code is run to populate individual lists l[0] to [8] for each of the types of patches, and to populate a separate list limits which holds the maximum index (from 0) to select the last patch in each list. Finally a list of the 9 separate patch lists is assembled in the list plist.
As you can see from the screen shot above, there are a lot of items on the TouchOSC interface. The main section has 57 yellow labels, which can be populated with the names of a patch. This is controlled by 9 similar (green) labels, each containing the name of a type of patch. Behind all of these yellow and green labels there resides a grey push button area. When the table screen is touched on one of these labels, the button behind sends an OSC message from the tablet to Sonic Pi. This message has the form /helm/pxx for the yellow labels grey buttons, where xx is a 2 digit number from 00 to 56, and /helm/tPx for the green labels grey buttons where x is a number from 0 to 8. The Green labels are permanently displaying the patch categories for the Factory Presets, however the Yellow labels will have their contents changed depending upon the patch names for the current type selected. This means that they each have to have a separate name and osc address. These range from /helm/L00 to /helm/L56 In order to accommodate this large selection of address, I make use of that fact that Sonic PI can respond to incoming OSC messages with wild card matches, so that ONE live loop can deal with all the incomping /help/pxx messages, rather than 57 different ones! To enable this to be useful, I developed a function called parse_sync_address which lets you find out the exact address that matched the wild card(s) used.
To look at one example:
live_loop :getType do #get osc message to update current type use_real_time b = sync "/osc*/helm/Tp*" if b[0]==1.0 n= parse_sync_address("/osc*/helm/Tp*")[2][-1].to_i puts n setType n end end
the live_loop :getType waits for an incoming OSC message to match the wild card string “/osc*/helm/Tp*” This can be matched by any of the messages /helmTp0 to /helm/Tp9. The actual address returned is returned by the parse_sync_address function as a list with each element corresponding to one section of the incoming message. e.g.[“osc”, “helm”, “Tp3”], from which the last section can be retrieved and the last character used to get the required “3”, which is then converted to an integer.
The line n= parse_sync_address(“/osc*/helm/Tp*”)[2][-1].to_i is responsible. One point to note if you haven’t used OSC with Sonic Pi before, is that it always prepends the incoming address with /osc to differentiate it from other event triggering mesages such as those from incoming midi messages which always start with /midi. In fact in the next version of sonic pi the event string will start with something like /osc:192.168.1.240:9000 including the source ip address and port. Luckily this will still match /osc* so usiing /osc* future proofs the software to work with version 3.2 when that arrives.
I don’t intend to go through all the code exhaustively, and I have included quite a few comments to indicate what is going on. two other sections however are perhaps worthy of some attention. First the method of adjusting the volume of the incoming live_audo from Helm. The relevant code section is below.
live_loop :getVol,delay: 2 do #live loop to adjust volume. Delayed start to allow live_audio setup use_real_time b = sync "/osc*/helm/volume" set :vol,b[0] end live_loop :getVolScale,delay: 2 do #live loop to get volScale setting use_real_time b = sync "/osc*/helm/volScale/*/1" if b[0]==1.0 n=parse_sync_address("/osc*/helm/volScale/*/1")[3].to_i set :volScale,[1,3,5,7][n-1] #select vol multiplier end end
First the two live loops above enable volume information to be sent from TouchOSC. The first live_loop :getVolretrieve the setting of the volume slider on teh TouchOSC screen, returing a value between 0 and 1 which is stored in the time state using set :vol, b[0]. The second live_loop :getVolScale retrieve the number of the Volume Scale switch on the TouchOSC screen which has been pushed, and uses that to set a multiplier 1,3,5 or 7 which is stored in :volScale. Note that both of these live loops are delayed from starting for 2 seconds to make sure the live_audio is set up first. The code to get the audio in is wrapped by a with_fx :level effect, which is then controlled by the volume data just discussed.
with_fx :level, amp: get(:vol)*get(:volScale) do |vl| #set volume level of incoming audio from Helm set :vl,vl live_loop :adjustV, delay: 2 do #loop checks and adjust vol every 0.5 seconds control get(:vl),amp: get(:vol)*get(:volScale),amp_slide: 0.2 sleep 0.5 end live_audio :helm,input: 1, stereo: true #audio in from Helm end
A reference vl is stored as :vl for the with_fx wrapper, and the amp: option referenced by this reference is then controlled in teh loive_+loop :adjustV. This is set to refresh every 0.5 seconds, so that it doesn’t consume TouchOSC screen which are used to control one of 5 live_loops which are set up to produce sequences of midi notes to be sent to helm. As a new live_loop is selected (and created) any previously running loop is stopped. This is done by creating the live loops within a function called doLoop. The relevant sections of code are shown below.
live_loop :chooseScale do #choose midi notes to use depending on selector switch use_real_time b = sync "/osc*/helm/selectScale/*/1" if b[0]==1.0 n= parse_sync_address("/osc*/helm/selectScale/*/1")[3].to_i #puts "scale on #{n}" case n when 1 set :sv,0.2 set :snote,:c4 doLoop 1,1 #doLoop sets up live_loop name1 to send midi notes when 2 set :sv,0.2 set :snote,:c5 doLoop 2,2 #sets up live_loop name2 when 3 set :sv,1 set :snote,:c3 doLoop 3,1 #sets up live_loop name3 when 4 set :sv,0.5 set :snote,:c3 doLoop 4,2 #sets up live_loop name4 when 5 set :sv,2 set :snote,:c4 doLoop 5,1 #sets up live_loop name5 end else #b[0]==0.0 so deal with stopping any deselected loop n= parse_sync_address("/osc*/helm/selectScale/*/1")[3].to_i #puts "scale off #{n}" set ("kill"+n.to_s).to_sym,true #set kill switch for the selected loop #puts "stopping loop"+n.to_s end end #sets up and starts live_loop named name define :doLoop do |n,ch,vol=get(:vol)| #parameters n (for name),ch for tick or choose,vol setting set ("kill"+n.to_s).to_sym,false #set kill flag to false ln=("name"+n.to_s).to_sym #create loop name live_loop ln do #start the loop use_bpm get(:tempo) #set current tempo nv=scale(get(:snote),:minor_pentatonic).tick if ch==1 #choose note select method depending on ch nv=scale(get(:snote),:minor_pentatonic).choose if ch==2 midi nv,sustain: get(:sv),vel_f: vol #send midi to helm sleep get(:sv) #puts "loop"+n.to_s,get( ("kill"+n.to_s).to_sym) stop if get( ("kill"+n.to_s).to_sym)==true #stop the loop if flag has changed to true end end define :stopAllScales do #set all midi loop kill flags to true 5.times do |n| osc "/helm/selectScale/"+(n+1).to_s+"/1",0 #turn all button indicators off set ("kill"+(n+1).to_s).to_sym,true end end live_loop :oscKillScales do #detect Stop Patter button pushed and stop all midi loops use_real_time b = sync "/osc*/helm/stopAll" if b[0]==1.0 stopAllScales midi_all_notes_off end end
The live_loop :chooseScale, responds to an incoming OSC addressed to /helm/selectScale/*/1 The * will correspond to which PatternSelect button is pushed. This is then used to control a Ruby case statement, and according to the value of the star 1-5 it sets values for the note sustain and base note of the scale to be used and then calls the doLoop function mentioned above. This creates a name for a live loop selected by the first incoming parameter n in the call with value :name1 .. :name5. It also creates an associated flag :kill1 .. :kill5 (initially set to false) which will later be used to stop the live loop. It starts the live_loop running, using the current tempo value (which can be changed whilst the loop is running). The loop stops when it detects the associated kill flag has been set to true. The chooseScale live_loop generates the change in kill flag value because when a new pattern is selected, because of the toggle action of these switches a switch off value 0 will be generated by the previously selected switch which is then used to set the ill flag for the loop to be stopped.
Two further live_loops stopAllScales and oscKillScales cooperate to switch off all 5 live loops (assuming any are running), when the Stop Pattern or Clear All buttons are pushed. The latter also clears the contents of all the patch labels and the currently selected type.
#helmOSCsynthSelector.rb #program written by Robin Newman, November 2019 #uses TouchOSC and Sonic Pi to allow easy remote selection of any factory preset for Helm Synth #also has some test midi sequences that can be sent to Helm #audio output from Helm fed back through Sonic Pi, and volume is controllable there. #turn off most logging. Can enable for testing use_osc_logging false use_midi_logging false use_debug false #setup user specified values use_osc "192.168.1.240",9000 #address and port of TouchOSC device #choose midi port to suit. I used "iac_driver_sonicpi" (mac) or "midi_through_port-0" (RaspberryPi) use_midi_defaults port: "iac_driver_sonicpi",channel: 1 #midi port and channel #select ONE of the next two lines as appropriate dirbase=(ENV['HOME']+"/Library/Audio/Presets/Helm/Factory Presets/") #path to helm presets (Mac) #dirbase=("/home/pi/.helm/patches/Factory Presets/") #path to helm presets (Raspberry Pi) define :getList do |type| #get patch names for a given type from their stored location files = Dir.glob(File.join(dirbase+type, '**', '*')).select{|file| File.file?(file)}.sort l=[] files.each do |f| #select one of the next two lines depending on OS used l<< f.split("/")[9][0..-6] #strip out just filename without .helm suffix (for Mac) #l<< f.split("/")[7][0..-6] #strip out just filename without .helm suffix (for RPi) end return l end define :getTypeList do #get list of patch types (these are folder names) tl=[] Dir.chdir(dirbase) do tl= Dir.glob('*').select { |f| File.directory? f }.sort #get sorted list of folders end return tl end typeList=getTypeList #store list of patch type folders l=[] #initialise list for patch names limits=[] #initialise list for index max limit for each patch type typeList.each_with_index do |type,i| #fill type lists and limits list l[i]=getList type limits[i]=l[i].length - 1 end plist=[l[0],l[1],l[2],l[3],l[4],l[5],l[6],l[7],l[8]] #plist is a list of the 9 patch type lists sleep 1 #let things settle. (maily for RPi) define :parse_sync_address do |address| # used to retrieve data which matched wild card in synced event v= get_event(address).to_s.split(",")[6] if v != nil return v[3..-2].split("/") else return ["error"] end end #initialise volume slider position osc "/helm/volume",0.5 osc "/helm/volScale/2/1",1 #set second switch osc "/helm/tempo/2/1",1 #set 2nd tempo set :type,0 #set starting type, choice and volume set :choice,0 set :vol, 0.5 set :volScale, 3 set :tempo,60 live_loop :getVol,delay: 2 do #live loop to adjust volume. Delayed start to allow live_audio setup use_real_time b = sync "/osc*/helm/volume" set :vol,b[0] end live_loop :getVolScale,delay: 2 do #live loop to get volScale setting use_real_time b = sync "/osc*/helm/volScale/*/1" if b[0]==1.0 n=parse_sync_address("/osc*/helm/volScale/*/1")[3].to_i set :volScale,[1,3,5,7][n-1] #select vol multiplier end end live_loop :getTempo,delay: 2 do #live loop to adjust volume. Delayed start to allow live_audio setup use_real_time b = sync "/osc*/helm/tempo/*/1" if b[0]==1.0 n=parse_sync_address("/osc*/helm/tempo/*/1")[3].to_i set :tempo,[30,60,90,120][n-1] #select tempo end end with_fx :level, amp: get(:vol)*get(:volScale) do |vl| #set volume level of incoming audio from Helm set :vl,vl live_loop :adjustV, delay: 2 do #loop checks and adjust vol every 0.5 seconds control get(:vl),amp: get(:vol)*get(:volScale),amp_slide: 0.2 sleep 0.5 end live_audio :helm,input: 1, stereo: true #audio in from Helm end define :patch do |bank=0,folder=0,patch=0| #midi code to adjust patch. midi_cc 0,bank #always 0 in this version. Could expand to cover user bank midi_cc 32,folder #patch folder type index midi_pc patch #patch name index in folder end define :emptyLabels do #clear all patch label names 58.times do |x| v=x.to_s v="0"+v if x<10 osc "/helm/L"+v,"" sleep 0.01 end end define :populateLabels do |lab| #populate patch label names for given selected type emptyLabels lab.length.times do |x| v=x.to_s v="0"+v if x<10 osc "/helm/L"+v,lab[x] sleep 0.01 end end define :setPled do |n| #light led for current choice, or none if n < 0 57.times do |x| osc "/helm/led"+x.to_s,0 end if n >=0 osc "/helm/led"+n.to_s,1 set :choice,n end end define :setType do |n| #light type led for current selection n, then populate labels 9.times do |x| osc "/helm/T"+x.to_s,0 end sleep 0.1 osc "/helm/T"+n.to_s,1 set :type,n populateLabels plist[n] if n >= 0 #ignore for negative n setPled -1 #turn off choice led as no longer valid for current list end setType 0 #init starting position (arp choices) live_loop :getType do #get osc message to update current type use_real_time b = sync "/osc*/helm/Tp*" if b[0]==1.0 puts parse_sync_address("/osc*/helm/Tp*") n= parse_sync_address("/osc*/helm/Tp*")[2][-1].to_i puts n setType n end end live_loop :getChoice do #get osc message to update current choice use_real_time b = sync "/osc*/helm/p*" if b[0]==1.0 n = parse_sync_address("/osc*/helm/p*")[2][-2..-1].to_i #puts n setPled n if n<= limits[get(:type)] #update led patch 0,get(:type),get(:choice) #send new patch selection to helm end end live_loop :chooseScale do #choose midi notes to use depending on selector switch use_real_time b = sync "/osc*/helm/selectScale/*/1" if b[0]==1.0 n= parse_sync_address("/osc*/helm/selectScale/*/1")[3].to_i #puts "scale on #{n}" case n when 1 set :sv,0.2 set :snote,:c4 doLoop 1,1 #doLoop sets up live_loop name1 to send midi notes when 2 set :sv,0.2 set :snote,:c5 doLoop 2,2 #sets up live_loop name2 when 3 set :sv,1 set :snote,:c3 doLoop 3,1 #sets up live_loop name3 when 4 set :sv,0.5 set :snote,:c3 doLoop 4,2 #sets up live_loop name4 when 5 set :sv,2 set :snote,:c4 doLoop 5,1 #sets up live_loop name5 end else #b[0]==0.0 so deal with stopping any deselected loop n= parse_sync_address("/osc*/helm/selectScale/*/1")[3].to_i #puts "scale off #{n}" set ("kill"+n.to_s).to_sym,true #set kill switch for the selected loop #puts "stopping loop"+n.to_s end end #sets up and starts live_loop named name define :doLoop do |n,ch,vol=get(:vol)| #parameters n (for name),ch for tick or choose,vol setting set ("kill"+n.to_s).to_sym,false #set kill flag to false ln=("name"+n.to_s).to_sym #create loop name live_loop ln do #start the loop use_bpm get(:tempo) #set current tempo nv=scale(get(:snote),:minor_pentatonic).tick if ch==1 #choose note select method depending on ch nv=scale(get(:snote),:minor_pentatonic).choose if ch==2 midi nv,sustain: get(:sv),vel_f: vol #send midi to helm sleep get(:sv) #puts "loop"+n.to_s,get( ("kill"+n.to_s).to_sym) stop if get( ("kill"+n.to_s).to_sym)==true #stop the loop if flag has changed to true end end define :stopAllScales do #set all midi loop kill flags to true 5.times do |n| osc "/helm/selectScale/"+(n+1).to_s+"/1",0 #turn all button indicators off set ("kill"+(n+1).to_s).to_sym,true end end live_loop :oscKillScales do #detect Stop Pattern button pushed and stop all midi loops use_real_time b = sync "/osc*/helm/stopAll" if b[0]==1.0 stopAllScales midi_all_notes_off end end live_loop :clearAll do #detect Clear All pushed use_real_time b= sync "/osc*/helm/clearPanel" if b[0]==1.0 emptyLabels #clear all labels setType -1 #clear all type leds stopAllScales #stop any playing midi loops midi_all_notes_off #stop any other midi notes currently playing end end
So that gives an overview of the Sonic Pi program which is shown in entirety below There is a download link here which also contains a download link for the TouchOSC template, together with instructions as to how to install it.
The TouchSCreen template should be downloaded as index.xml You should then compress (zip) it and rename the resulting file thelmSelector.touchosc This file should be opened in your TouchScreen editor and then synced to your phone or tablet in the usual TouchScreen way. You should enable OSC and adjust the host address to match that of the computer running your Sonic Pi copy. Set the outgoing part to 4559 for Sonic Pi 3.1 (or 4560 for Sonic Pi 3.2dev or later). Set the incoming port to 9000, and note the ip address of your tablet or phone shown below. You should adjust the use_osc line in the program to match this information. In my case with my iPhone it was ip 192,1681.240 and port 9000.
I hope to write another article in the near future detailing how to loopback the audio from Helm (and similar software synths such as Qsynth) to the audio inputs of Sonic Pi, both on a Mac, and on a Raspberry Pi (or linux) box. (There already is an article about setting up Qsynth on a Raspberry Pi here). The former can either use the (rather expensive) Amoeba LoopBack utility, or the (free) SoundFlower add (although this does not seem to give the same performance). The latter can be achieved in Qjackctl.
EDIT just come across a new free program called BlackHole which makes it easy to loopback audio from Helm or qaynth on a Mac. See this link for details
I have made a video of the program in action which you can see here.