Revised computer keyboard note input for Sonic Pi

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

Advertisements