Skip to content

Instantly share code, notes, and snippets.

@rbnpi
Last active August 28, 2021 20:17
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rbnpi/ebedd26d61180230db891ccbd9d74971 to your computer and use it in GitHub Desktop.
Save rbnpi/ebedd26d61180230db891ccbd9d74971 to your computer and use it in GitHub Desktop.
A polyphonic gated synth for Sonic Pi3 with midi keyboard input. Runs on Raspberry Pi3 or more powerful computer with Sonic Pi 3. Accompanying article and video SECOND VERSION ADDED: see comments
#polyphonic midi input program with sustained notes
#experimental program by Robin Newman, November 2017
#pitchbend can be applied to notes at any time while they are sounding
use_debug false
set :synth,:tb303 #initial value
set :pb,0 #pitchbend initial value
kill_list=[] #list to contain notes to be killed
on_notes=[] #list of notes currently playing
ns=[] #array to store note playing references
nv=[0]*128 #array to store state of note for a particlar pitch 1=on, 0=off
128.times do |i|
ns[i]=("n"+i.to_s).to_sym #set up array of symbols :n0 ...:n127
end
#puts ns #for testing
define :sv do |sym| #extract numeric value associated with symbol eg :n64 => 64
return sym.to_s[1..-1].to_i
end
#puts sv(ns[64]) #for testing
live_loop :choose_synth do
b= sync "/midi/*/*/*/control_change" #use wild cards to works with any controller
if b[0]==10 #adjust control number to suit your controller
sc=(b[1].to_f/127*3 ).to_i
set :synth,[:tri,:saw,:tb303,:fm][sc] #can change synth list if you wish
puts "Synth #{get(:synth)} selected"
end
end
live_loop :pb do #get current pitchbend value adjusted in range -12 to +12 (octave)
b = sync "/midi/*/*/*/pitch_bend" #change to match your controller
set :pb,(b[0]-8192).to_f/8192*12
end
with_fx :reverb,room: 0.8,mix: 0.6 do #add some reverb
live_loop :midi_note_on do #this loop starts 100 second notes for specified pitches and stores reference
use_real_time
note, on = sync "/midi/*/*/*/note_on"
if on >0
if nv[note]==0 #check if new start for the note
puts "setting note #{note} on"
vn=on.to_f/127
nv[note]=1 #mark note as started for this pitch
use_synth get(:synth)
x = play note+get(:pb),attack: 0.01, sustain: 100,amp: vn #start playing note
set ns[note],x #store reference to note in ns array
on_notes.push [note,vn] #add note to list of notes playing
end
else
if nv[note]==1 #check if this pitch is on
nv[note]=0 #set this pitch off
kill_list.push note #add note to list of notes to kill
end
end
end
live_loop :processnote,auto_cue: false,delay: 0.4 do # this applies pitchbend if any to note as it plays
#delayed start helps reduce timing errors
use_real_time
if on_notes.length > 0 #check if any notes on
k=on_notes.pop #get next note from "on" list
puts "processing note #{k[0]}"
in_thread do #start a thread to apply pitchbend to the note every 0.05 seconds
v=get(ns[k[0]]) #retrieve control value for the note
while nv[k[0]]==1 #while the note is still merked as on
control v,note: k[0]+get(:pb),note_slide: 0.05,amp: k[1]
sleep 0.05
end
#belt and braces kill here as well as in notekill liveloop: catches any that miss
control v,amp: 0,amp_slide: 0.02 #fade note out in 0.02 seconds
sleep 0.02
puts "backup kill note #{k[0]}"
kill v #kill the note referred to in ns array
end
end
sleep 0.08 #so that the loop sleeps if no notes on
end
live_loop :notekill,auto_cue: false,delay: 0.3 do # this loop kills released notes
#delayed start helps reduce timing errors
use_real_time
while kill_list.length > 0 #check if there are notes to be killed
k=kill_list.pop #get next note to kill
puts "killing note #{k}"
v=get(ns[k]) #retrieve reference to the note
control v,amp: 0,amp_slide: 0.02 #fade note out in 0.02 seconds
sleep 0.02
kill v #kill the note referred to in ns array
end
sleep 0.08 #so that the loop sleeps if no notes to be killed
end
end #reverb
#polyphonic midi input program with sustained notes
#experimental program by Robin Newman, November 2017
#pitchbend can be applied to note AS IT STARTS
#This version for controllers with separate note_on and note_off midi signals
#rather than using note_on with velocity 0 for midi_off signal
set :pb,0 #pitchbend value
plist=[] #list to contains references to notes to be killed
ns=[] #array to store note playing references
nv=[0]*128 #array to store state of note for a particlar pitch 1=on, 0 = 0ff
128.times do |i|
ns[i]=("n"+i.to_s).to_sym #set up array of symbols :n0 ...:n127
end
#puts ns #for testing
define :sv do |sym| #extract numeric value associated with symbol eg :n64 => 64
return sym.to_s[1..-1].to_i
end
#puts sv(ns[64]) #for testing
live_loop :pb do #get current pitchbend value adjusted in range -12 to +12 (octave)
b = sync "/midi/*/*/*/pitch_bend"
set :pb,(b[0]-8192).to_f/8192*12
puts get(:pb)
end
define :geton do |address|
v= get_event(address).to_s.split(",")[6]#[address.length+1..-2].to_i
return v.include?"note_on"
end
define :parse_sync_address do |address|
v= get_event(address).to_s.split(",")[6]#[address.length+1..-2].to_i
if v != nil
return v[3..-2].split("/")
else
return ["error"]
end
end
live_loop :midi_piano_on do #this loop starts 5 second notes for spcified pitches and stores reference
use_real_time
note, vol = sync "/midi/*/*/*/note_*"
res= parse_sync_address "/midi/*/*/*/*"
puts res[4]
if res[4]=="note_on"
puts note,nv[note]
if nv[note]==0 #check if new start for the note
nv[note]=1 #mark note as started for this pitch
use_synth :tri
#max duration of note set to 5 on next line. Can increase if you wish.
x = play note+get(:pb), amp: vol/127.0,sustain: 50 #play note
set ns[note],x #store reference in ns array
end
else
if nv[note]==1 #check if this pitch is on
nv[note]=0 #set this pitch off
plist << get(ns[note])
end
end
end
live_loop :notekill,auto_cue: false,delay: 0.25 do
use_real_time
if plist.length > 0 #check if notes to be killed
k=plist.pop
control k,amp: 0,amp_slide: 0.02 #fade note out in 0.02 seconds
sleep 0.02
kill k #kill the note referred to in ns array
end
sleep 0.01
end
@rbnpi
Copy link
Author

rbnpi commented Apr 29, 2018

I Have added a second version simplepolysynth2.rb which works with a controller which uses separate note_on and note_off midi signals, rather than using midi_on with velocity 0 which many controllers use for the midi_off signal.
It may be that this is what you need for your keyboard controller.

@TheBeachLab
Copy link

Yet another version with the new midi cue format introduced in SP3.2 as read here: https://in-thread.sonic-pi.net/t/midi-changes-for-upcoming-sonic-pi-3-2/3319

#polyphonic midi input program with sustained notes
#experimental program by Robin Newman, November 2017
#pitchbend can be applied to note AS IT STARTS

#This version for controllers with separate note_on and note_off midi signals
#rather than using note_on with velocity 0 for midi_off signal

set :pb,0 #pitchbend value
plist=[] #list to contains references to notes to be killed
ns=[] #array to store note playing references
nv=[0]*128 #array to store state of note for a particlar pitch 1=on, 0 = 0ff

128.times do |i|
  ns[i]=("n"+i.to_s).to_sym #set up array of symbols :n0 ...:n127
end
#puts ns #for testing

define :sv do |sym| #extract numeric value associated with symbol eg :n64 => 64
  return sym.to_s[1..-1].to_i
end
#puts sv(ns[64]) #for testing

live_loop :pb do #get current pitchbend value adjusted in range -12 to +12 (octave)
  b = sync "/midi*/pitch_bend"
  set :pb,(b[0]-8192).to_f/8192*12
  puts get(:pb)
end

define :geton do |address|
  v= get_event(address).to_s.split(",")[6]#[address.length+1..-2].to_i
  return v.include?"note_on"
end

define :parse_midi_sync_address do |address|
  v= get_event(address).to_s.split(",")[6]#[address.length+1..-2].to_i
  if v != nil
    p1=v[3..-2].split("/")
    return p1[0].split(":")+p1[1..-1]
  else
    return ["error"]
  end
end

live_loop :midi_piano_on do #this loop starts 5 second notes for spcified pitches and stores reference
  use_real_time
  note, vol = sync "/midi*/note_*"
  res= parse_midi_sync_address "/midi*/*"
  #puts res #for testing
  puts res[4]
  if res[4]=="note_on"
    puts note,nv[note]
    if nv[note]==0 #check if new start for the note
      nv[note]=1 #mark note as started for this pitch
      use_synth :tri
      #max duration of note set to 5 on next line. Can increase if you wish.
      x = play note+get(:pb), amp: vol/127.0,sustain: 50 #play note
      set ns[note],x #store reference in ns array
    end
  else
    if nv[note]==1 #check if this pitch is on
      nv[note]=0 #set this pitch off
      plist << get(ns[note])
    end
  end
end

live_loop :notekill,auto_cue: false,delay: 0.25 do
  use_real_time
  if plist.length > 0 #check if notes to be killed
    k=plist.pop
    control k,amp: 0,amp_slide: 0.02 #fade note out in 0.02 seconds
    sleep 0.02
    kill k #kill the note referred to in ns array
  end
  sleep 0.01
end

@TheBeachLab
Copy link

And here the version with the live pitch bend and reverb

set :synth,:tri #initial synth value
set :pb,0 #initial pitchbend value
on_notes=[] #list of notes currently playing
plist=[] #list to contains references to notes to be killed
ns=[] #array to store note playing references
nv=[0]*128 #array to store state of note for a particlar pitch 1=on, 0 = 0ff

128.times do |i|
  ns[i]=("n"+i.to_s).to_sym #set up array of symbols :n0 ...:n127
end
#puts ns #for testing

define :sv do |sym| #extract numeric value associated with symbol eg :n64 => 64
  return sym.to_s[1..-1].to_i
end
#puts sv(ns[64]) #for testing

#choose the synth with a rotary encoder
live_loop :choose_synth do
  b= sync "/midi*/control_change" #use wild cards to works with any controller
  if b[0]==77 #adjust control number to suit your controller
    sc=(b[1].to_f/127*3 ).to_i
    set :synth,[:tri,:saw,:tb303,:fm][sc] #can change synth list if you wish
    puts "Synth #{get(:synth)} selected"
  end
end

#check pitch bend value
live_loop :pb do #get current pitchbend value adjusted in range -12 to +12 (+/-1octave)
  b = sync "/midi*/pitch_bend"
  set :pb,(b[0]-8192).to_f/8192*12
  #puts get(:pb)
end

define :parse_midi_sync_address do |address|
  v= get_event(address).to_s.split(",")[6]#[address.length+1..-2].to_i
  if v != nil
    p1=v[3..-2].split("/")
    return p1[0].split(":")+p1[1..-1]
  else
    return ["error"]
  end
end

with_fx :reverb,room: 0.8,mix: 0.6 do #add some reverb
  live_loop :midi_piano_on do #this loop starts 100 second notes for spcified pitches and stores reference
    use_real_time
    note, vol = sync "/midi*/note_*"
    res= parse_midi_sync_address "/midi*/*"
    #puts res #for testing
    #puts res[4]
    if res[4]=="note_on"
      puts note,nv[note]
      if nv[note]==0 #check if new start for the note
        nv[note]=1 #mark note as started for this pitch
        use_synth get(:synth)
        #max duration of note set to 100 on next line. Can increase if you wish.
        x = play note+get(:pb), attack: 0.01, amp: vol/127.0,sustain: 100 #play note
        set ns[note],x #store reference in ns array
        on_notes.push [note,vol/127.0] #add note to list of notes playing
      end
    else
      if nv[note]==1 #check if this pitch is on
        nv[note]=0 #set this pitch off
        plist << get(ns[note]) #add note to list of notes to kill
      end
    end
  end
  
  live_loop :processnote,auto_cue: false,delay: 0.4 do # this applies pitchbend if any to note as it plays
    #delayed start helps reduce timing errors
    use_real_time
    if on_notes.length > 0 #check if any notes on
      k=on_notes.pop #get next note from "on" list
      puts "processing note #{k[0]}"
      in_thread do #start a thread to apply pitchbend to the note every 0.05 seconds
        j=get(ns[k[0]]) #retrieve control value for the note
        while nv[k[0]]==1 #while the note is still merked as on
          control j,note: k[0]+get(:pb),note_slide: 0.05,amp: k[1]
          sleep 0.05
        end
        #belt and braces kill here as well as in notekill liveloop: catches any that miss
        control j,amp: 0,amp_slide: 0.02 #fade note out in 0.02 seconds
        sleep 0.02
        puts "backup kill note #{k[0]}"
        kill j #kill the note referred to in ns array
      end
    end
    sleep 0.08 #so that the loop sleeps if no notes on
  end
  
  live_loop :notekill,auto_cue: false,delay: 0.25 do
    use_real_time
    if plist.length > 0 #check if notes to be killed
      k=plist.pop
      control k,amp: 0,amp_slide: 0.02 #fade note out in 0.02 seconds
      sleep 0.02
      kill k #kill the note referred to in ns array
    end
    sleep 0.01
  end
end #reverb

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment