Sonic Pi, Helm Synth, TouchOSC combined

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.