Skip to content

Instantly share code, notes, and snippets.

@elsewhat
Last active January 9, 2021 20:52
Show Gist options
  • Save elsewhat/f4e0756c06c3ebed4ca04531d3658ecd to your computer and use it in GitHub Desktop.
Save elsewhat/f4e0756c06c3ebed4ca04531d3658ecd to your computer and use it in GitHub Desktop.
Midi controller script for Sonic Pi
# 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