Back in April 2016 I wrote a couple of scripts that enabled a computer keyboard to provide note input to Sonic Pi. This was before Sonic Pi had the facility to accept incoming OSC messages, and I used a tcp/ip link to achieve the result. Although it worked, it was rather sluggish in response and only of limited use. Recently on the in-thread.sonic-pi.net site a user referred to it in a script, and I decided that it was time to look at it again. In the event, I modified the scripts to use OSC messaging and ended up with a simpler faster installation which works much better. It has a very good low latency, and the only drawback is that it is strictly monophonic, and can only respond to one key at a time, although these can follow each other in rapid succession.
A script called terminalcontroller2.rb is run in a terminal window, which will send the ascii code of any key pressed (while it has focus) via an OSC message to sonic-pi port 4559 on the localhost. It is run with the command ruby terminalcontroller2.rb assuming that the file terminalcontroller2.rb is in the currently selected directory.
#terminascontroller2.rb #terminal ruby program to get and transmit key info #originally written for tcp by Robin Newman, April 2016 #this new version uses OSC messages updated December 2018 # use in conjunction with SP-KeyboardController2.rb running in Sonic Pi require 'io/wait' require 'socket' require 'rubygems' require 'osc-ruby' @client = OSC::Client.new( 'localhost', 4559 ) def char_if_pressed #routine to scan for keyboard press (non-blocking) begin system("stty raw -echo") # turn raw input on c = nil if $stdin.ready? c = $stdin.getc end c.chr if c ensure system "stty -raw echo" # turn raw input off end end while true #main loop, runs until stopped by ctrl-c k=0 #0 will remain 0 if no key pressed c = char_if_pressed k= "#{c}".ord if c #work out ascii value of key # client = server.accept # Wait for a client to connect # client.puts k #transmit the keycode # client.close #close the client terminating input if k!=0 #only send message if k !=0 @client.send( OSC::Message.new( "/key" , k )) end sleep 0.005 #short gap end
In order for the script to work, you must also install the gem osc-ruby
In my case I have rvm (ruby management installed). I used
rvm use 2.5.1 gem install osc-ruby
where ruby 2.5.1 was the version of ruby I had installed under rvm.
At the Sonic Pi end a script called SP-KeyboardController2.rb is run in a buffer window. This contains a live_loop :getkey which polls for a key press and then carries out actions depending upon the key pressed. In this example program , and some keys are interpreted as a piano keyboard (keys “a”, “w”, “s”, “e”, “d”, “f”, “t”, “g”, “y”, “h”, “u”, “j”, “k”, “o”, “l”, “p”, “;”, “‘”, “]”, “#”) and others (number keys 1.2.3……0) trigger a series of live_loops each of plays a given sequence or sample.
#SP-KeyboardController2.rb #experimental program to control Sonic Pi from the keyboard #developed from an orbianl program written by Robin Newman, April 2016 #you can play notes direct, or initiate any of 10 liveloops #now updated to use OSC communiation from the key polling program #set up hash to contain notes notes=Hash.new #list of notes l=[:c4,:cs4, :d4, :ds4, :e4, :f4, :fs4, :g4, :gs4, :a4, :as4, :b4, :c5, :cs5, :d5, :ds5, :e5, :f5, :fs5, :g5] #list of associated keys (may have to alter depending on your keyboard layout) n=["a", "w", "s", "e", "d", "f", "t", "g", "y", "h", "u", "j", "k", "o", "l", "p", ";", "'", "]", "#"] set :n,n 20.times do |i| #loop to create the hash notes[n[i].ord]=l[i] end live_loop :getkey do use_real_time b= sync "/osc/key" #kye pressed sent in an OSC message from k=b[0] puts k ks=k.to_i.chr #check if key to play note or not if !["a", "w", "s", "e", "d", "f", "t", "g", "y", "h", "u", "j", "k", "o", "l", "p", ";", "'", "]", "#"].include? ks #check for cued live_loop cue :one if ks == "1" cue :two if k.to_i.chr == "2" cue :three if ks == "3" cue :four if ks == "4" cue :five if ks == "5" cue :six if ks == "6" cue :seven if ks == "7" cue :eight if ks == "8" cue :nine if ks == "9" cue :zero if ks == "0" else #note to play play notes[k.to_i],sustain: 0.5,release: 0.05,amp: 0.3 sleep 0.05 end end define :pl do |tune,dur| #loop to play a tune held in a notes and a durations list tune.zip(dur).each do |n,d| play n,sustain: d*0.9,release: d*0.1 sleep d end end live_loop :dr do #plays loop_amen_full sync :one l= (sample_duration :loop_amen_full) with_fx :level,amp: 1 do |v| control v,amp_slide: 2*l,amp: 0 2.times do sample :loop_amen_full sleep l end end end live_loop :dr2 do #plays loop_tabla sync :two with_fx :level,amp: 3 do |v| control v,amp_slide: (sample_duration :loop_tabla),amp: 0 sample :loop_tabla end end live_loop :melody do #plays notes using :pluck synth use_synth :pluck sync :three with_fx :level, amp: 1 do |v| control v, amp_slide: 9.6, amp: 0 n=scale(:a1,:minor_pentatonic,num_octaves: 2) 96.times do play n.choose if spread(5,8).tick sleep 0.1 end end end live_loop :woosh do #plays misc_cineboom sync :four 4.times do sample :misc_cineboom,start: 0,finish: 0.4,beat_stretch: 4 sleep 1.6 sample :misc_cineboom,start: 0.5,finish: 0.8, beatstretch: 4 sleep 1.2 end end live_loop :rhythm do #plays notes with :tb303 use_synth :tb303 sync :five with_fx :level, amp: 1 do |v| control v, amp_slide: 9.6, amp: 0 f= (note_range :c2, :c5, pitches: (scale :c, :minor)) 96.times do play f.choose,release: 0.1 if spread(5,8).tick play (f.choose - 12),release: 0.1 if !spread(5,8).look sleep 0.1 end end end live_loop :frere do #plays frere jaques round sync :six sq=0.1 q=2*sq c=2*q tune=[:c4,:d4,:e4,:c4]*2+[:e4,:f4,:g4]*2+[:g4,:a4,:g4,:f4,:e4,:c4]*2+[:c4,:g3,:c4]*2 dur=[q,q,q,q,q,q,q,q,q,q,c,q,q,c,sq,sq,sq,sq,q,q,sq,sq,sq,sq,q,q,q,q,c,q,q,c] in_thread do use_synth :tri pl(tune,dur) end sleep 4*c in_thread do use_synth :saw pl(tune,dur) end sleep 4*c in_thread do use_synth :prophet pl(tune,dur) end end live_loop :fanfare do #plays a three chord fanfare sync :seven use_synth :tri ch1=[:c4,:e4,:g4,:c5] ch2=[:c4,:d4,:f4,:a4,:c5] ch3=[:e4,:g4,:c5,:g5] q=0.2 c=2*q cd=3*q m=4*q dur=[c,q,q,q,q,cd,q,m] tune=[ch1,ch2,ch2,ch2,ch2,ch1,ch1,ch3] pl(tune,dur) #play using pl function end live_loop :boom do #drum roll and bass drum thump sync :eight puts sample_duration :drum_roll sample :drum_roll,finish: 0.32 sleep 2 sample :drum_bass_hard sleep sample_duration :drum_bass_hard end live_loop :slider do #slides two notes around sync :nine with_fx :level,amp: 0,amp_slide: 1 do|v| use_synth :tb303 s=play :a2,sustain: 8,note_slide: 1 #start the two notes with 8 sec sustains r=play :a3,sustain: 8,note_slide: 1 4.times do control v,amp: 1 #slide the volume up control s, note: :a3 #slide the ptiches control r, note: :a2 sleep 1 control v,amp: 0 #slide the volume down control s, note: :a1 #slide the ptiches control r, note: :a3 sleep 1 end end end live_loop :sampfrenzy do #plays all samples (except loops) fast sync :zero sample_groups.each do |g| if g !=:loop then sample_names(g).each do |n| if sample_duration(n) > 0.2 sample n,beat_stretch: 0.2 else sample n end sleep 0.2 end end end end
You can down load the files from this gist
a video of the programs in action is here