Last active
January 9, 2021 20:52
-
-
Save elsewhat/f4e0756c06c3ebed4ca04531d3658ecd to your computer and use it in GitHub Desktop.
Midi controller script for Sonic Pi
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
# Midi controller script for Sonic Pi | |
# by elsewhat | |
####### CONFIG ######### | |
#Configuration parameters based on actual midi controller | |
#Find by viewing cues event-stream | |
minKeyIndex= 52 | |
maxKeyIndex = 72 | |
minKnobIndex =1 | |
maxKnobIndex =8 | |
minDrumpadIndex =44 | |
maxDrumpadIndex =51 | |
#Root path of the controller events | |
midiRootPath="/midi/mpkmini2/0/1" | |
####### EVENT HANDLERS ######### | |
#Do your song specific updates here | |
#midiKeyDown - Called when a key is pressed down | |
#Return values - 1. Synth or sample, 2. Array of FX effects | |
define :midiKeyDown do |note, velocity| | |
print "midiKeyDown" | |
use_synth :tb303 | |
#Get the current controller knob values | |
controllerValue = getMidiControllerValues | |
playPan = controllerValue[1] | |
playAmp = controllerValue[2] | |
playCutoff = controllerValue[5] | |
playRes = controllerValue[6] | |
fxSlicerValue = controllerValue[8] | |
fxSlicerMix = controllerValue[8]>=0.4 ? 0 : 1 | |
fxCutoff = controllerValue[7] | |
fxCutoffMix = controllerValue[7]<=40 ? 0 : 1 | |
sound = nil | |
fxArray = [] | |
with_fx :slicer, phase:fxSlicerValue, mix:fxSlicerMix do |fxDistortion| | |
with_fx :bitcrusher, cutoff:fxCutoff,mix:fxCutoffMix do |fxBitcrusher| | |
#Play sound. Add non-standard attribute org_note for pitch bend later | |
sound = play note, release: 5, cutoff:playCutoff, amp:playAmp, pan:playPan, sustain:20,res:playRes, org_note: note | |
fxArray.push (fxDistortion) | |
fxArray.push (fxBitcrusher) | |
end | |
end | |
#Return synth played and fx array for control later based on controller knob events | |
return sound, fxArray | |
end | |
#midiKeyUp - Called when a key is released | |
#Return values - 1. Synth or sample, 2. Array of FX effects | |
define :midiKeyUp do |synth,fxArray| | |
print "midiKeyUp" | |
if synth then | |
#control sound, freq: 110 | |
#control sound, note: :e5 | |
#control synth, amp: 0,amp_slide: 0.2 | |
#sleep 0.02 | |
kill synth | |
end | |
#Implicit return value | |
return synth,fxArray | |
end | |
#midiDrumpadDown - Called when a drumpad is pressed | |
#Return values - 1. Synth or sample, 2. Array of FX effects | |
define :midiDrumpadDown do |note, velocity| | |
print "midiDrumpadDown" | |
fxArray = [] | |
controllerValue = getMidiControllerValues | |
playAmp = controllerValue[2] | |
playPan = controllerValue[3] | |
case note | |
when 44 | |
sound = sample :drum_cowbell, rate: 1, attack:0.0, amp:playAmp, pan:playPan | |
when 45 | |
sound = sample :drum_cymbal_open, rate: 1, attack:0.0, amp:playAmp, pan:playPan | |
when 46 | |
sound = sample :drum_heavy_kick, rate: 1, attack:0.0,amp:playAmp, pan:playPan | |
when 47 | |
sound = sample "c:/music/trance/do you love me hard h1.wav" | |
when 48 | |
sound = sample :drum_tom_hi_hard, rate: 1, attack:0.0, amp:playAmp, pan:playPan | |
when 49 | |
sound = sample :ambi_choir, rate: 4, attack:0.0, amp:playAmp, pan:playPan | |
when 50 | |
sound = sample :ambi_swoosh, rate: 1, attack:0.0, amp:playAmp, pan:playPan | |
when 51 | |
sound = sample "c:/music/trance/do you love me hard h2.wav", amp:playAmp, pan:playPan | |
else | |
print "Unhandled drumpad event #{note} in midiDrumpadDown" | |
end | |
return sound, fxArray | |
end | |
#midiDrumpadUp - Called when a drumpad is released | |
#Return values - 1. Synth or sample, 2. Array of FX effects | |
define :midiDrumpadUp do |synth,fxArray| | |
print "midiDrumpadUp" | |
if synth then | |
print synth | |
kill synth | |
end | |
#Implicit return value | |
return synth,fxArray | |
end | |
#midiControlChange - Called when a controller knob is changed | |
#Value is rawValue normalized through normalizeControllerValue | |
#Return value - The last value to use on next event | |
define :midiControlChange do |control, value, rawValue, lastValue| | |
print ":midiControlChange #{control},#{value}#{rawValue},#{lastValue}" | |
playingKeys = getMidiPlayingKeys | |
playingDrumpads = getMidiPlayingDrumpads | |
#Case statement allowing different handling based on controller used | |
case control | |
when 1 | |
playingKeys.select do |sound| | |
synth = sound[:synth] | |
control synth, pan:value | |
end | |
playingDrumpads.select do |sound| | |
synth = sound[:synth] | |
control synth, pan:value | |
end | |
when 2 | |
playingKeys.select do |sound| | |
synth = sound[:synth] | |
control synth, amp:value | |
end | |
playingDrumpads.select do |sound| | |
synth = sound[:synth] | |
control synth, amp:value | |
end | |
when 3 | |
set_mixer_control! lpf: value | |
when 4 | |
set_mixer_control! hpf: value | |
when 5 | |
playingKeys.select do |sound| | |
synth = sound[:synth] | |
control synth, cutoff:value | |
end | |
when 6 | |
playingKeys.select do |sound| | |
synth = sound[:synth] | |
control synth, res:value | |
end | |
when 7 | |
playingKeys.select do |sound| | |
fxArray = sound[:fxArray] | |
#Only handle bitcrusher fx effect | |
unless fxArray.nil? || fxArray == 0 || fxArray.length== 0 | |
fxArray.select do |fxEffect| | |
if fxEffect.name == "sonic-pi-fx_bitcrusher" then | |
if value >40 then | |
control fxEffect, phase:value, mix:1 | |
else #Mix out the slicer effect if value less than 40 | |
control fxEffect, phase:value, mix:0 | |
end | |
control fxEffect, cutoff:value | |
end | |
end | |
end | |
end | |
when 8 | |
playingKeys.select do |sound| | |
fxArray = sound[:fxArray] | |
#Only handle slicer fx effect | |
unless fxArray.nil? || fxArray == 0 || fxArray.length== 0 | |
fxArray.select do |fxEffect| | |
if fxEffect.name == "sonic-pi-fx_slicer" then | |
if value <0.4 then | |
control fxEffect, phase:value, mix:1 | |
else #Mix out the slicer effect if value [0.4,->] | |
control fxEffect, phase:value, mix:0 | |
end | |
end | |
end | |
end | |
end | |
else | |
print "Unhandled controller #{control} in midiControlChange" | |
end | |
#implicit return value - | |
value | |
end | |
#midiPitchChange - Called when a controller knob is changed | |
#Return value - The last value to use on next event | |
define :midiPitchChange do |value, lastValue| | |
print ":midiPitchChange #{value},#{lastValue}" | |
playingKeys = getMidiPlayingKeys | |
playingDrumpads = getMidiPlayingDrumpads | |
#Normalize from [0,16383] to [-12,12]= +-1 octave | |
pitchValue = normalizeMidiValue value, 0, 16383, -12,12 | |
print "Pitch value normalized to #{pitchValue}" | |
playingKeys.select do |sound| | |
synth = sound[:synth] | |
orgNote = synth.args["org_note"] | |
control synth, note: (orgNote+pitchValue) | |
end | |
#implicit return value | |
value | |
end | |
#normalizeControllerValue - Normalize the value of the controller. | |
#Normalization is dependent on the usage of the controller value | |
#Raw value is in [0,127] | |
#Return value - Normalized value | |
define :normalizeControllerValue do |control, rawValue| | |
case control | |
when 1 #For panning | |
return normalizeMidiValue rawValue, 0, 127, -1,1 | |
when 2 #For amp | |
return normalizeMidiValue rawValue, 0, 127, 0,2 | |
when 3 #For global lowpass filter | |
return normalizeMidiValue rawValue, 0, 127, 0,135 | |
when 4 #For global highpass filter | |
return normalizeMidiValue rawValue, 0, 127, 0,135 | |
when 5 #For play cutoff | |
return normalizeMidiValue rawValue, 0, 127, 30,130 | |
when 6 #For res | |
return normalizeMidiValue rawValue, 0, 127, 0.5,0.9999 | |
when 7 #For fx bitcrush cutoff | |
return normalizeMidiValue rawValue, 0, 127, 30,130 | |
when 8 #For fx slicer | |
return normalizeMidiValue rawValue, 0, 127, 0.01,0.5 | |
else | |
print "No normalized value defined for #{control}" | |
return rawValue | |
end | |
end | |
####### Utility functions ######### | |
#getMidiPlayingKeys - Get currently playing keys | |
#Return value - Array of key sounds. Sound is a hash with :synth and :fxArray | |
define :getMidiPlayingKeys do | |
print "getPlayingKeys" | |
keysArray = get[:keysRing].to_a | |
midiPlayingKeys =keysArray.select do |sound| | |
unless sound.nil? || sound == 0 | |
unless sound[:synth].nil? || sound[:synth] == 0 | |
true | |
end | |
end | |
end | |
print midiPlayingKeys | |
#Implicit return | |
midiPlayingKeys | |
end | |
#getMidiPlayingDrumpads - Get currently playing keys | |
#Return value - Array of drumpad sounds. Sound is a hash with :synth and :fxArray | |
define :getMidiPlayingDrumpads do | |
print "getMidiPlayingDrumpads" | |
drumpadsArray = get[:drumpadsRing].to_a | |
midiPlayingDrumpads =drumpadsArray.select do |sound| | |
unless sound.nil? || sound == 0 | |
unless sound[:synth].nil? || sound[:synth] == 0 | |
true | |
end | |
end | |
end | |
print midiPlayingDrumpads | |
#Implicit return | |
midiPlayingDrumpads | |
end | |
#getMidiControllerValues - Get current values for controller knobs | |
#Return value - Ring of controller values. Index controller id, value in [0,127] | |
define :getMidiControllerValues do | |
knobsRing = get[:knobsRing] | |
end | |
#normalizeMidiValue - Normalize a value into the given range | |
#value >= minValue and value<= maxValue | |
#Return value - Normalize value in [minValue, maxValue] | |
define :normalizeMidiValue do |value, minValue,maxValue, minNormalizedValue, maxNormalizedValue| | |
#Value in [0,1] | |
valueFraction = (value-minValue).to_f/ (maxValue - minValue).to_f | |
normalizeMidiValue = minNormalizedValue + valueFraction*(maxNormalizedValue - minNormalizedValue).to_f | |
#Implicit return | |
normalizeMidiValue | |
end | |
####### DATASTRUCTURES ######### | |
#Typically no need to update here | |
#Ring datastructure to store ongoing sounds | |
set :keysRing,SonicPi::Core::RingVector.new(Array.new(maxKeyIndex-minKeyIndex+1)) | |
set :drumpadsRing, SonicPi::Core::RingVector.new(Array.new(maxDrumpadIndex-minDrumpadIndex+1)) | |
#Ring datastructure to store last value for knobs. Default value = 64 , but normalize them | |
knobsRingRaw = SonicPi::Core::RingVector.new(Array.new(maxKnobIndex-minKnobIndex+1,64)) | |
knobsRing = knobsRingRaw.map.with_index do |raw,index| | |
normalizeControllerValue index,raw | |
end | |
set :knobsRing, knobsRing | |
set :pitchBendLastValue, 0 | |
####### CONTROLLER ENGINE ######### | |
#Typically no need to update here | |
#As event handling is sync (blocking the thread) | |
#we need one live_loop thread pr event type | |
### note_on midi event ### | |
#Can be triggered from both keys and drum-pads | |
live_loop :midi_controller_note_on do | |
use_real_time | |
note, velocity = sync "#{midiRootPath}/note_on" | |
if note >= minKeyIndex and note <= maxKeyIndex then | |
#Call function midiKeyDown for actual processing | |
synth,fxArray = midiKeyDown note, velocity | |
sound = {:synth=>synth, :fxArray=>fxArray } | |
#Update ongoing sounds | |
index = note - minKeyIndex | |
keysRing = get[:keysRing] | |
keysRing = keysRing.put(index, sound) | |
set :keysRing,keysRing | |
end | |
if note >= minDrumpadIndex and note <= maxDrumpadIndex then | |
#Call function midiKeyDown for actual processing | |
synth,fxArray = midiDrumpadDown note, velocity | |
sound = {:synth=>synth, :fxArray=>fxArray } | |
#Update ongoing sounds | |
index = note - minDrumpadIndex | |
drumpadsRing = get[:drumpadsRing] | |
drumpadsRing = drumpadsRing.put(index, sound) | |
set :drumpadsRing,drumpadsRing | |
end | |
end | |
### note_off midi event ### | |
#Can be triggered from both keys and drum-pads | |
live_loop :midi_controller_note_off do | |
use_real_time | |
note, velocity = sync "#{midiRootPath}/note_off" | |
if note >= minKeyIndex and note <= maxKeyIndex then | |
keysRing = get[:keysRing] | |
index = note - minKeyIndex | |
sound = keysRing[index] | |
if sound then | |
#Call function midiKeyDown for actual processing | |
synth,fxArray = midiKeyUp sound[:synth], sound[:fxArray] | |
sound = {:synth=>synth, :fxArray=>fxArray } | |
#Sound may have been update | |
index = note - minKeyIndex | |
keysRing = keysRing.put(index, sound) | |
set :keysRing,keysRing | |
end | |
end | |
#In theory both key and drumpad can have same index | |
if note >= minDrumpadIndex and note <= maxDrumpadIndex then | |
drumpadsRing = get[:drumpadsRing] | |
index = note - minDrumpadIndex | |
sound = drumpadsRing[index] | |
if sound then | |
#Call function midiDrumpadDown for actual processing | |
synth,fxArray = midiDrumpadUp sound[:synth], sound[:fxArray] | |
sound = {:synth=>synth, :fxArray=>fxArray } | |
#Sound may have been update | |
index = note - minDrumpadIndex | |
drumpadsRing = drumpadsRing.put(index, sound) | |
set :drumpadsRing,drumpadsRing | |
end | |
end | |
end | |
### control_change midi event ### | |
#Triggered from controller knobs | |
live_loop :midi_controller_control_change do | |
use_real_time | |
control, rawValue = sync "#{midiRootPath}/control_change" | |
knobsRing = get[:knobsRing] | |
if control>= minKnobIndex and control<= maxKnobIndex then | |
lastValue = knobsRing[control] | |
normalizedValue = normalizeControllerValue control,rawValue | |
print "Normalized #{normalizedValue}" | |
#Call event handler. Allow it to overwrite the last value for next time | |
newLastValue = midiControlChange control,normalizedValue, rawValue, lastValue | |
#store last value | |
knobsRing = knobsRing.put(control,newLastValue) | |
set :knobsRing,knobsRing | |
else | |
print "Control #{control} is outside valid range [#{minKnobIndex},#{maxKnobIndex}]" | |
end | |
end | |
### pitch_bend midi event ### | |
#Triggered from controller knobs | |
live_loop :midi_controller_pitch_bend do | |
use_real_time | |
value = sync "#{midiRootPath}/pitch_bend" | |
value = value[0] #Value is array; take first element | |
lastValue = get[:pitchBendLastValue] | |
#Call event handler. Allow it to overwrite the last value for next time | |
newLastValue = midiPitchChange value, lastValue | |
#store last value | |
set :pitchBendLastValue, newLastValue | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment