Sonic Pi 3 Synth Driver

Recently I published an article detailing the use of Sonic Pi 3 being controlled by a wireless PS3 games controller. Wile developing this project I tried out the idea of creating a “long note” synth which could then be controlled in pitch and cutoff. I liked using it, and decided to develop some of the ideas in that project for use with TouchOSC as an input device. This allowed more refined and easier to use input than the PS3 controller, and in particular enabled me to add a virtual keyboard input. I ended up with a project I called Sonic Pi 3 Synth Driver.

The project gives the following controls.
1. a synth controlled by a 2 octave + 1 note virtual keyboard. You can control the synth, the volume and the release time of the notes as you play, together with an octave shifter for the keyboard and a transpose setting up and down an octave in semitone steps.
2. an XY “pad” which controls a selectable synth in pitch (Y axis) and Release time (X axis). IT has a separate volume control.
3. A continuous note from a selectable synth, which can be muted and unmuted, or stopped and restarted with a different synth. It has its own volume control and is controlled with an XY “pad” with pitch on the Y axis and cutoff value on the X axis.

All three sources are affected by the transpose setting (up to +/- 12 semitones) and there are two independent synth selectors. The left hand one controls the left XY pad and the right hand one controls the right XY pad (continuous note). The keyboard can be switched to either selector. Once the continuous note is started, the right selector can be used for the keyboard without affecting the continuous note. There is also an adjustable reverb setting applied to all outputs.

The program, which requires Sonic PI 3, has been tested on a Pi3, Ubuntu 17.04 and on a Mac. The computers must be on a network, and TouchOSC runs on an iPad connected wirelessly to the network.

The program for the project (ver1.1) is shown below. It contains extensive comments.

#nc (Note Continuous) program for Sonic Pi 3 controlled by TouchOSC
#written by Robin Newman, August 2017  (ver 1.1)
#Three separate note inputs are controlled.
#For each one, synth,volume,pitch,sustain nd transpose are controlled
#One uses a virtual 2 octave+ 1 note keyboard, which can be octave shifted
#One uses an xy input to control Pitch and Release
#One generates a continous note with xy control of pitch and cutoff.
#This note can be muted/unmuted, or restarted with a differnt synth setting
#TouchOSC controls everything, by sending a variety of OSC messages to Sonic Pi 3
#It also receives feed back (via OSC messages) which set up the initial controller positions
#and controls two leds which indicate settings for the "long note"
#It features the use of set and get commands to send and receive information between the multiple
#live loops in the program.
#It uses two undocumented features of Sonic Pi 3 (which therefore may change in the future)
#1: use of n.kill to kill a synth node n
#2: use of the get_event function to retrieve address information for OSC messages
#this enables one live loop to control ALL the keyboard button inputs, and enables
#the address decoding of the multi-toggle buttons used for synth/transpose and kbd octave

TouchOSC_IP="192.168.1.50" #adjust for IP of TouchOSC
use_osc TouchOSC_IP,9000 #TouchOSC set to receive on port 9000

use_debug false
use_cue_logging false
#initialise set values. These are used to communicate with the various live_loops
set :volLN2,0 #used to hold "long Note" volume level
set :volLeft,0.5 #vol for left hand XY pad
set :volKbd,0.5 #vol for keyboard
set :kbdRelease,0.5 #keyboard release time
set :kbdOctave,1 #keyboard octave shift
set :ncontrol,36 #used to hold controlled note value for "long note"
set :kill,1 #used to kill "long note"
set :mutevol,1 #used to mute/unmute "long note"
set :cutoff_val,80 #cutoff value for "long note"
set :leftpitch,60 #left note starting pitch
set :synleft,:tri #current synth name for buttons and "left note"
set :synright,:tri #current synth name for "long note"
set :kbdsyn,:tri #current synth name for keyboard
set :kbdsynptr,1 #points to left (1) or right(2) synth list
set :trsetting,0 #transpose setting in semitomes


define :setLed do |name,col,intensity| #sets LED colour and intensity
  osc "/nc/"+name+"/color",col
  osc "/nc/"+name,intensity
end

define :init do #OSC messages to set up TouchOSC controls
  osc "/nc/vfade1",0.5
  osc "/nc/vfade2",0
  osc "/nc/synthleft/1/1",1
  osc "/nc/synthright/1/1",1
  osc "/nc/fadersrelease",0.5
  osc "/nc/kbdoctave/1/2",1
  osc "/nc/volKbd",0.5
  osc "/nc/kbdsynth/1/1",1
  osc "/nc/transpose/1/8",1 # equiv 0 transpose
  osc "/nc/faderReverb",0.5
  setLed("ledsk","red",1)
  setLed("ledmu","green",1)
end

init #intialise TouchOSC

define :pdec2 do |n| #for nice printing to 2 decimal places
  return (n.to_f*100).round.to_f/100
end

########## CONTROL SECTION ADJUSTS VOL SYNTHS, MUTE ETC ####
define :getsyn do |address| #decode synth multi-toggle address
  return get_event(address).to_s.split(",")[6][address.length-1].to_i
end
live_loop :get_synthR do #Right synth selector
  use_real_time
  b= sync "/osc/nc/synthright/?/1"
  if b[0]>0
    slist=[:tri,:saw,:prophet,:tb303,:fm,:mod_saw]
    p= getsyn("/osc/nc/synthright/?/1") - 1 #to get offset from 0
    set(:synright,slist[p])
    set(:kbdsyn,get(:synright)) if get(:kbdsynptr) == 2
    puts "synRight is",get(:synright)
  end
end


live_loop :get_synthL do #left synth selector
  use_real_time
  b= sync "/osc/nc/synthleft/?/1"
  if b[0]>0
    slist=[:tri,:saw,:prophet,:tb303,:fm,:mod_saw]
    p= getsyn("/osc/nc/synthleft/?/1") - 1 #to get offset from 0
    set(:synleft,slist[p])
    set(:kbdsyn,get(:synleft)) if get(:kbdsynptr)==1
    puts "synLeft is",get(:synleft)
  end
end

live_loop :volLN2 do #long note vol select
  use_real_time
  b = sync "/osc/nc/vfade2"
  set :volLN2,b[0]*2
  puts "Vol long note is",pdec2(get(:volLN2))
end

live_loop :volLeft do #left xy pad Vol
  use_real_time
  b = sync "/osc/nc/vfade1"
  set :volLeft,b[0]*2
  puts "Vol left note is",pdec2(get(:volLeft))
end

define :getTranspose do |address| #decode Transpose button
  return get_event(address).to_s.split(",")[6][address.length+1..-2].to_i
end
live_loop :setTranspose do #set Transpose
  use_real_time
  b= sync "/osc/nc/transpose/1/*"
  if b[0]==1
    tr= getTranspose("/osc/nc/transpose/1/*")-1
    puts "tr is",tr
    set(:trsetting,[-12,-10,-8,-7,-5,-3,-1,0,2,4,5,7,9,11,12][tr])
    puts "Transpose (semitones)",get(:trsetting)
  end
end

live_loop :volKbd do #set kbd volume
  use_real_time
  b = sync "/osc/nc/vfadekeys"
  set :volKbd,b[0]*2
  puts "Vol keyboard note is",pdec2(get(:volKbd))
end

live_loop :releaseKbd do #set kbd release time
  use_real_time
  b = sync "/osc/nc/fadersrelease"
  set :kbdRelease,b[0]
end

define :getkbdOctave do |address| #decode Octave multi-toggle selected
  return get_event(address).to_s.split(",")[6][address.length+1].to_i
end
live_loop :selectKbdOctave do #select kbd offset
  use_real_time
  b = sync "/osc/nc/kbdoctave/1/*"
  if b[0]==1
    octave=getkbdOctave("/osc/nc/kbdoctave/1/*")-1 #num from 0
    puts("Kbd octave offset ",octave)
    set :kbdOctave,octave
  end
end

define :getkbdsynth do |address| #decode kbd synth select multi-toggle address
  return get_event(address).to_s.split(",")[6][address.length+1].to_i
end
live_loop :selectkbdsynth do #select kbd synth from appropriate list
  use_real_time
  b = sync "/osc/nc/kbdsynth/1/*"
  if b[0]==1
    s=getkbdsynth("/osc/nc/kbdsynth/1/*")
    puts"s is",s
    set :kbdsynptr,s
    set :kbdsyn,get(:synleft) if s==1
    set :kbdsyn,get(:synright) if s==2
    puts "kbd synth is",get(:kbdsyn)
  end
end

live_loop :durationLeftNote do #left XY duration setting
  use_real_time
  b = sync "/osc/nc/xy1/1"
  set :dur,b[1]*0.98+0.02
  puts "Left note pitch",pdec2(get(:leftpitch)),"duration",pdec2(get(:dur))
end

live_loop :kill2 do #set kill flag for "long" note
  b=sync "/osc/nc/kill2"
  set :kill,1 if b[0]>0 #only on push, not release
end

live_loop :long_pitch do #change pitch of "long" note
  use_real_time
  b= sync "/osc/nc/xy2/1"
  set :ncontrol,b[0]*12+36
  set :cutoff_val,b[1]*40+80
  #print current data for Long Note
  puts "Long Note pitch:",pdec2(get(:ncontrol)+get(:trsetting)),"Cutoff:",pdec2(get(:cutoff_val))
end

live_loop :muteln2 do #temp mute "long note"
  use_real_time
  b=sync "/osc/nc/mute2"
  setLed("ledmu","red",1)
  set :mutevol,0
end

live_loop :unmuteln2 do #used to unmute "long note"
  use_real_time
  b=sync "/osc/nc/unmute2"
  setLed("ledmu","green",1)
  set :mutevol,1
end


###### PLAYING SECTION BELOW #####

with_fx :reverb,room: 0.8,mix: 0.5 do |rv| #playing section inside fx reverb
  in_thread do #thread to alter reverb value
    loop do #loop to react to reverb slider
      b=sync  "/osc/nc/faderReverb"
      control rv,mix: b[0],mix_slide: 0.1 #adjust reverb according to slider
      puts"Reverb mix is",pdec2(b[0])
    end
  end
  
  live_loop :continuous_note do #setup up "long note" and control it
    use_real_time
    b= sync "/osc/nc/enable2" #start the "long" note
    if b[0]>0 and get(:kill)==1 #don't restart running note
      set :kill,0
      setLed("ledsk","green",1)
      use_synth get(:synright)
      puts"long note started with synth",get(:synright)
      n= play get(:ncontrol)+get(:trsetting),sustain: 1000,cutoff: get(:cutoff_val),amp: 0  #start "long note" with zero volume for 1000 seconds
      10000.times do
        control n,note: get(:ncontrol)+get(:trsetting),note_slide: 0.1,amp: get(:mutevol)*get(:volLN2),amp_slide: 0.1,cutoff: get(:cutoff_val),cutoff_slide: 0.1
        sleep 0.1
        break if get(:kill)==1 #if kill==1 abort loop
      end
      setLed("ledsk","red",1)
      control n, amp: 0,amp_slide: 0.1 #fade out and stop
      sleep 0.1
      n.kill
    end
  end
  
  live_loop :leftNote do #adjust pitch from left xy pad
    use_real_time
    f = sync "/osc/nc/xy1/1"
    set :leftpitch,f[0]*24+60+get(:trsetting)
    synth get(:synleft),note: get(:leftpitch),attack: 0.15,release: get(:dur),amp: get(:volLeft) #or use synth get(:syn)
    sleep get(:dur)*0.05+0.01
  end
  
  define :getkey do |address| #decode key num from OSC address
    return get_event(address).to_s.split(",")[6][address.length..-2].to_i
  end
  
  live_loop :kybd do #play note pushed
    use_real_time
    b= sync "/osc/nc/k[!i]**" #match k0...k24 (exclude kill1 match)
    n= getkey("/osc/nc/k**")+48
    kn=n+get(:kbdOctave)*12+get(:trsetting) #calc pitch
    puts "keybd note",kn
    synth get(:kbdsyn),note: kn,attack: 0.1,release: get(:kbdRelease),amp: get(:volKbd)   if b[0]==1
  end
  
end #fx

The five functions getsyn, getTranspose,getkbdOctave, getkbdsynth and getkey all perhaps deserve some mention. They are all very similar. They use the undocumented function get_event to examine the full data returned by a particular event, which matches a given event_address. This can produce an output like

#<SonicPi::CueEvent:[[1501696407.315011, 0, #, 0, 0.0, 60.0], "/osc/nc/k12", [1.0]]

This event occurred when key 12 was pushed on the keyboard. Normally you see the end of this data “/osc/nc/k12”,[1.0] produced in the cues log on the Sonic Pi screen., but you can only retrieve the data part [1.0] from a statement like

b = sync "/osc/nc/k12"

In this case I want to use wild cards in the cue matching so a statement like

b = sync "/osc/nc/k**"

would match ANY of the keyboard keys being pressed. This is fine, but we need to be able to determine which one has been pressed. That is where the output of the get_event function is useful. All we have to do is to use some ruby string functions to extract the bit we want. For each of cues we want to match this process is slightly different, but they essentially all work in the same way, letting us know exactly which key or toggle switch triggered the cue. Here is the getkey function reproduced below, together with the live_loop :kybd which uses it:

  define :getkey do |address| #decode key number from OSC address
    return get_event(address).to_s.split(",")[6][address.length..-2].to_i
  end
  live_loop :kybd do #play note pushed
    use_real_time
    b= sync "/osc/nc/k[!i]**" #match k0...k24 but exclude kill1 match
    n= getkey("/osc/nc/k**")+48
    kn=n+get(:kbdOctave)*12+get(:transpose) #calc pitch to play
    puts "keybd note ",kn
    synth get(:kbdsyn),note: kn,attack: 0.1,release: get(:kbdRelease),amp: get(:volKbd)   if b[0]==1
  end

The TouchOSC layout is produced with the editor which runs on a Mac, and produces the file named nc1.1.touchosc This is in fact a compressed file containing an xml format file named index.xml. You can download this file, together with the Sonic Pi program file from here Follow the instructions to convert the index.xml file to a TouchOSC file. It can then be loaded into the TouchOSC editor, and from there transmitted to an iPad running TouchOSC, in the normal manner for the program. See here for file downloads
In order to use the programs, you need to make sure that the IP addresses are configured correctly in each program, (TouchOSC on the iPad and nc1.1.rb on Sonic Pi 3).
Set the OSC ip address in TouchOSC to be the IP address of the machine running Sonic Pi. You can get this address from the IO Preferences Panel in Sonic Pi
Also make sure that the OSC Port (outgoing) is set to 4559 and Port (incoming) is set to 9000
In the nc1.1.rb program make sure that the data at the start of the program after the initial comment lines matches the setup for your iPad

TouchOSC_IP="192.168.1.50" #adjust for IP of TouchOSC
use_osc TouchOSC_IP,9000 #TouchOSC set to receive on port 9000

As you can see above, TouchOSC shows the IP address of the iPad  (Local IP address) on the OSC screen (in my case 192.168.1.50)
Finally make sure that the Enable OSC server and Receive remote OSC messages are both ticked on the Sonic Pi 3 IO preferences tab

Now run the nc1.1.rb program in Sonic PI 3 and you should be able to effect control via TouchOSC on the iPad.
I have made a video of the program in action which you may find instructive.