Last active
April 22, 2021 17:53
-
-
Save amiika/4e2d9d326d419d7a286dd4a2235d3061 to your computer and use it in GitHub Desktop.
Markov chain launchpad for novation mini and 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
# Markov chain player for novation mini launchpad | |
# Tested only on novation mini mk3 model but probably works on any novaion launchpad | |
use_debug false | |
use_midi_logging false | |
# Midi ports for the launchpad | |
launchpad_in = "/midi:midiin2_(lpminimk3_midi)_1:1/*" | |
launchpad_out = "midiout2_(lpminimk3_midi)_2" | |
# Coloring scheme for the launchpad | |
colors = [ | |
[0,0,0], | |
[50,0,0], | |
[200,0,100], | |
[200,0,50], | |
[0,255,255] | |
] | |
# Creates rgb from probability based on the current color scheme | |
define :prob_to_color do |prob| | |
index = colors.index.with_index do |col,i| | |
prob <= i.to_f/(colors.length-1) | |
end | |
lower = colors[index-1] | |
upper = colors[index] | |
upperProb = index.to_f/(colors.length-1) | |
lowerProb = (index-1).to_f/(colors.length-1) | |
u = (prob - lowerProb) / (upperProb - lowerProb) | |
l = 1 - u | |
[(lower[0]*l + upper[0]*u).to_i, (lower[1]*l + upper[1]*u).to_i, (lower[2]*l + upper[2]*u).to_i].map {|color| ((127*color)/255) } | |
end | |
# Shorthand function to create the markov chain | |
define :to_mm do |i, mm=8, init=false| | |
# Length of the markov matrix | |
length = mm.is_a?(Integer) ? mm : mm.length | |
# Init with random matrix if mm=integer | |
mm = Array.new(length) { Array.new(length, init ? 1.0/length : 0.0) } if mm.is_a?(Integer) | |
# Treat integer as a markov chain: 121 = 1->2, 2->1 | |
degrees = i.to_s.split("") | |
degrees.each_with_index do |d,i| | |
d = d.to_i(36) | |
# degrees A=10, B=11 ... 0=length | |
d = (d==0 ? length : d-1) | |
# Overflow depending on mm length: 8->0, 9->1, 0->2 | |
#print d | |
row = d % length | |
# Treat int as 'ring': 12 = 1->2->1 | |
next_d = degrees[(i+1)%degrees.length].to_i(36) | |
next_d = (next_d==0 ? length : next_d-1) | |
column = next_d % length | |
mm[row][column] += 1.0 | |
end | |
# Normalize the resulted matrix | |
normalize mm | |
end | |
# Normalizes markov chain | |
define :normalize do |mm| | |
mm.length.times do |row| | |
pp = 0.0 | |
mm[row].each do |p| | |
pp += p | |
end | |
mm[row].length.times do |i| | |
if pp == 0.0 then | |
#puts "warning: no transition defined for row #{row+1}!" if i == 0 | |
mm[row][i] = 1.0/mm[row].length | |
else | |
mm[row][i] /= pp | |
end | |
end | |
end | |
mm | |
end | |
# Get next id | |
define :next_idx do |chain| | |
r = rand | |
pp = 0 | |
mm = chain[:matrix] | |
i = mm[chain[:current]].index do |p| | |
pp += p | |
pp > r | |
end | |
i | |
end | |
# Set novation mini to programmer mode | |
define :programmer_sysex do | |
midi_sysex 0xf0, 0x00, 0x20, 0x29, 0x02, 0x0D, 0x0E, 0x01, 0xf7 | |
end | |
# Light up multiple leds from novation launchpad | |
define :led_sysex do |values| | |
midi_sysex 0xf0, 0x00, 0x20, 0x29, 0x02, 0x0d, 0x03, *values, 0xf7, port: launchpad_out | |
end | |
# Flash some cell in blue | |
define :flash_color do |mm, x, y| | |
r_x = mm.length-x | |
cell = (r_x.to_s+(y+1).to_s).to_i | |
values = [0x02, cell, 50, 0] | |
led_sysex values | |
end | |
# Set single cell color from propability | |
define :set_cell_color do |mm, x, y| | |
r_x = mm.length-x | |
cell = (r_x.to_s+(y+1).to_s).to_i | |
cell_prob = mm[x][y] | |
values = [0x03, cell, *prob_to_color(cell_prob)] | |
led_sysex values | |
end | |
# Set single cell color | |
define :set_cell_rgb do |x, y, rgb| | |
cell = (x.to_s+y.to_s).to_i | |
values = [0x03, cell, *rgb] | |
led_sysex | |
end | |
# Set colors for the whole matrix based on the probability | |
define :set_colors do |mm| | |
cells = [] | |
mm.length.times do |i| | |
row = mm[i] | |
row.length.times do |y| | |
cell = mm[i][y] | |
cell_color = [0x03, (mm.length-i).to_s+(y+1).to_s, *prob_to_color(cell)] | |
cells = cells+cell_color | |
end | |
end | |
led_sysex cells | |
end | |
# Get sync type from the midi call | |
define :sync_type do |address| | |
v = get_event(address).to_s.split(",")[6] | |
if v != nil | |
return v[3..-2].split("/")[1] | |
else | |
return "error" | |
end | |
end | |
# Set novation launchpad to programmer mode | |
programmer_sysex | |
# List of chains that can be browsed using the launchpad navigation | |
chains = [] | |
set :current_chain, 0 | |
alt_opts = 0 | |
seed = 600999700 | |
# Used scales, alternatively define smaller list | |
# scales = (ring :major, :minor, :minor_pentatonic, :major_pentatonic) | |
# or by index do print scales.index {|k| k==:minor } | |
scales = scale_names | |
# Used synths | |
synths = synth_names | |
define :debug_info do |chain| | |
chain[:id].to_s+": "+synths[chain[:synth]].to_s+" "+chain[:sleep].to_s | |
end | |
alt_list = ["AMP | SYNTH", "PITCH | SCALE"] | |
# Alternative operations when pressing down righthand arrows | |
define :do_alt_up_down do |chain, alt_op, up=true| | |
case alt_op | |
when 1 | |
up ? chain[:amp]+=0.1 : chain[:amp]>=0.1 ? chain[:amp]-=0.1 : chain[:amp]=0.0 | |
print "Amp: "+chain[:amp].round(2).to_s | |
when 2 | |
up ? chain[:octave]+=1 : chain[:octave]-=1 | |
print "Pitch: "+(chain[:octave]*12).to_s | |
end | |
end | |
# Alternative operations for right and left when pressing down righthand arrows | |
define :do_alt_right_left do |chain, alt_op, right=true| | |
case alt_op | |
when 1 | |
right ? chain[:synth]+=1 : chain[:synth]-=1 | |
print "Synth: "+synths[chain[:synth]].to_s | |
when 2 | |
right ? chain[:scale]+=1 : chain[:scale]-=1 | |
print "Scale: "+scales[chain[:scale]].to_s | |
end | |
end | |
# Default options for new chains | |
default_opts = {octave: -1, scale: 95, synth: 0, sleep: 0.25, amp: 1.0} | |
# Creates new random 8*8 markov matrix | |
define :new_chain do |pause=true| | |
seed = (seed.to_i+seed.to_s.reverse.to_i).to_s(8).to_i | |
r = seed | |
#r = rrand_i 10000000000000, 9999999999999999999 | |
n = r.to_s.chars.choose.to_i % 8 | |
n = n-1 if n>0 | |
mm = to_mm r | |
chains.push({id: chains.length, name: "chain_"+chains.length.to_s, **default_opts, current: n, pause: pause, matrix: mm, times: Array.new(8) {Array.new(8)} }) | |
end | |
# Create new live loop for each chain | |
define :run_chains do | |
chains.each do |chain| | |
live_loop chain[:name] do | |
use_real_time | |
stop if chain[:pause] | |
sync chains[0][:name].to_sym if chain[:id]>0 | |
old_n = chain[:current] | |
n = next_idx chain | |
chain[:current] = n | |
flash_color chain[:matrix], old_n, n if get(:current_chain)==chain[:id] | |
synth synths[chain[:synth]], amp: chain[:amp], note: scale(:d, scales[chain[:scale]])[n], pitch: chain[:octave]*12 | |
sleep chain[:sleep] | |
set_cell_color chain[:matrix], old_n, n if get(:current_chain)==chain[:id] | |
end | |
end | |
end | |
# Creating new random chain. Alternatively you could just add matrix to the chains array | |
new_chain false | |
set_colors chains[get(:current_chain)][:matrix] | |
run_chains | |
live_loop :launchpad_mini do | |
use_real_time | |
chain = chains[get(:current_chain)] | |
times = chain[:times] | |
mm = chain[:matrix] | |
# midi note is touch position 11, 12, 13 ... | |
# midi velocity is touch 127=on 0=off | |
note, velocity = sync launchpad_in | |
# note_on = pads, control_change = options | |
type = sync_type launchpad_in | |
if type=="note_on" then | |
xy = note.to_s.split("") | |
x = 8-xy[0].to_i | |
y = xy[1].to_i-1 | |
if velocity>0 then # On | |
mm[x][y] += 0.1 | |
normalize mm | |
set_colors mm | |
times[x][y] = current_time | |
elsif velocity==0 # Off | |
last_off = current_time | |
diff = last_off - times[x][y] if times[x][y] | |
if diff and diff>1 then | |
mm[x][y] = mm[x][y] > 0.5 ? 0.0 : 1.0 | |
normalize mm | |
set_colors mm | |
end | |
end | |
elsif type=="control_change" then | |
if velocity==0 then # Off | |
case note | |
when 19 | |
# Start / Stop | |
chain[:pause] = !chain[:pause] | |
print chain[:pause] ? "Stopping "+(debug_info chain) : "Starting "+(debug_info chain) | |
run_chains | |
when -> (n) { n.to_s=~/[2-8]9/ } # > off | |
alt_opts = 0 | |
when 91 | |
# Up | |
if alt_opts>0 | |
do_alt_up_down chain, alt_opts, true | |
else | |
chain[:sleep]+=0.05 | |
print "Sleep: "+chain[:sleep].round(2).to_s | |
end | |
when 92 | |
# Down | |
if alt_opts>0 | |
do_alt_up_down chain, alt_opts, false | |
else | |
chain[:sleep]-=0.05 if chain[:sleep]>=0.05 | |
print "Sleep: "+chain[:sleep].round(2).to_s | |
end | |
when 93 | |
# Left | |
if alt_opts>0 | |
do_alt_right_left chain, alt_opts, true | |
else | |
current_chain = (get(:current_chain)-1) % chains.length | |
set :current_chain, current_chain | |
chain = chains[current_chain] | |
print "Chain "+(debug_info chain) | |
set_colors chain[:matrix] | |
end | |
when 94 | |
# Right | |
if alt_opts>0 | |
do_alt_right_left chain, alt_opts, false | |
else | |
current_chain = (get(:current_chain)+1) % chains.length | |
set :current_chain, current_chain | |
chain = chains[current_chain] | |
print "Chain "+(debug_info chain) | |
set_colors chain[:matrix] | |
end | |
when 95 | |
# Session | |
new_chain | |
set :current_chain, chains.length-1 | |
chain = chains[chains.length-1] | |
chain[:sleep]*=2 | |
print "New chain: "+(debug_info chain) | |
run_chains | |
set_colors chain[:matrix] | |
end | |
elsif velocity==127 then ## cc on | |
case note | |
when -> (n) { n.to_s=~/[2-8]9/ } # > on | |
alt_opts = (8-note.digits[1])+1 | |
print alt_list[alt_opts-1] if alt_list[alt_opts-1] | |
end | |
end | |
end | |
sleep 0.1 | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment