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 #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 #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 #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 #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(",")[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 #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: