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: