Using controllers with Sonic Pi 3

Sonic Pi’s versatility is greatly increased by the use of external controllers, be they MIDI controllers or ones that can send OSC messages to Sonic PI. I am a great fan of TouchOSC, available for android or IOS devices, with accompanying free editors for Windows or Mac. TouchOSC has the advantage of being quite cheap, but also very versatile, as you can easily design your own controller layout using a range of predefined controls, such as buttons, sliders and rotary controls. You can also send signals BACK from Sonic PI to TouchOSC to control the settings of these controls, or “leds”, which can be placed on the Touch OSC layout.
For some time I have been interested in controlling “long” continuous notes in Sonic Pi. In a separate project I produced an 8 note polyphonic generator where the notes would last as long as a midi note on a keyboard was pushed. In this simpler example, I control two separate sinewave oscillators on Sonic Pi with sliders to control their pitches, and a selector switch to adjust the range of adjustment of these pitches. Three separate buttons allow you to mute or restart the tones, and to zero the slider positions. Each tone lasts for a “long time” determined in the program, and when its pitch is altered the note is killed and a fresh “long” note is started in its place.
You can explore the phenomenon of ‘beats’ using a small range setting, or use wider ranges to produce a range of separate notes.
The examples uses OSC messaging rather than MIDI, sending a range of messages to control the operation of live-loops controlling the program.
The code is shown below

#two tone player. Two oscillators controlled independently
#by TouchOSC sliders. Notes sound continuosly.
#Stop and Restart buttons
#Slider range selector +- 0.1,1,4,7,12 semitones
#Can illustrate "beats" between the notes on the bottom two ranges 0.1 and 1
#written by Robin Newman, October 2017

use_osc "localhost",4559
remote_ip="192.168.1.240" #adjust to IP address of TouchOSC device 
#reset sliders and note values
osc "/notegen/reset"
use_synth :sine
set :kv1,0;set :kv2,0;set :n1,0;set :n2,0;set :range,12 #initalise set values
#set correct range button
osc_send remote_ip,9000,"/notegen/range/5/1",1


with_fx :reverb,room: 0.8 do
  
  live_loop :getnote1 do #loop plays first (left slider) note
    use_real_time
    b = sync "/osc/notegen/note1"
    set :n1,b[0] #store note value
    k=get(:kv1) #get pointer to previous note (if any)
    if k!=0 #if exists, fade out and ill previous note
      control k,amp: 0,amp_slide: 0.025
      sleep 0.025
      kill k
    end
    r=get(:range)# get range scaler
    puts "r is ",r
    #when n1 is zero centre note is 72
    k=play get(:n1)*r+72,release: 100,attack: 0.01 #start new "long" note
    set :kv1,k
    sleep 0.01 #set minimum loop time to reduce "crackle"
  end
  
  live_loop :getnote2 do #loop plays second (right slider) note
    use_real_time
    b = sync "/osc/notegen/note2"
    set :n2,b[0] #store note value
    k=get(:kv2) #get pointer to previous note
    if k!=0 #if exists, fade out and kill note
      control k,amp: 0,amp_slide: 0.025
      sleep 0.025
      kill k
    end
    r=get(:range) #get current range scaler
    #when n2 is zero centre note is 72
    k=play get(:n2)*r+72,release: 100,attack: 0.01 #start new "long" note
    set :kv2,k
    sleep 0.01 #set minimum loop time to reduce "crackle"
  end
  live_loop :killnote do #=used by red "stop" button
    use_real_time
    b = sync "/osc/notegen/kill"
    if b[0]>0 #if button is pushed (not released)
      k=get(:kv1) # get current note1 pointer
      if k!=0 #if it exists kill the note
        control k,amp: 0,amp_slide: 0.04
        sleep 0.04
        kill k
      end
      k=get(:kv2) #gtet current note23 pointer
      if k!=0# if it exists kill the note
        control k,amp: 0,amp_slide: 0.04
        sleep 0.04
        kill k
      end
    end
  end
  live_loop :startnote do #used by update and astart green button
    use_real_time
    b = sync "/osc/notegen/start"
    if b[0]>0 #check if depressed
      osc "/notegen/note1",get(:n1) #send OSC message to getnote1 liveloop to (re)start the note
      osc "/notegen/note2",get(:n2) #send OSC message to getnote2 liveloop to (re)start the note
    end
    
  end
end

define :getbtn do |address| #"/osc/notegen/range/*/1" returns * value which will be 1->5
  #uses unpublished get_event function which may alter in future versions
  return get_event(address).to_s.split(",")[6][address.length-1..-4].to_i
end

live_loop :getrange do #get range set on TouchOSC multitoggle switch
  use_real_time
  b = sync "/osc/notegen/range/*/1"
  if b[0]>0 #only respond as button is set, not cleared
    r= getbtn("/osc/notegen/range/*/1") #function gets * match which will be 1->5 depending on button selected
    set :range,[0.1,1,4,7,12][r-1]
    puts"range set to ",get(:range)
  end
end

live_loop :resetsliders do #responds to YellowZero Sliders button to set silders to mid position 0 (range -1,1)
  use_real_time
  b = sync "/osc/notegen/reset"
  osc_send remote_ip,9000,"/notegen/note1",0 #send osc messages to zero sliders
  osc_send remote_ip,9000,"/notegen/note2",0
  set :n1,0 #reset n1 and n2 values to 0
  set :n2,0
end

Note the code uses one undocumented function from Sonic PI 3, namely get_event. This is used to extract the address for an OSC message which has been matched with a wildcard * to find out what the actual address was. Sam would want me to point out that this function is highly likely to change in the future, and therefore the code would have to be altered at that point.

Below is a picture of the TouchOSC layout I used.

You can download the layout code for the TouchOSC layout here, together with instructions for adding it to your own TouchOSC device.

A video of the program in action with a full explanation of its operation is shown below:

Two Tone Generator videolink

7 Likes