Last active
October 20, 2024 17:50
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
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
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
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.