Skip to content

Instantly share code, notes, and snippets.

@amiika
Last active April 22, 2021 17:53
Show Gist options
  • Save amiika/4e2d9d326d419d7a286dd4a2235d3061 to your computer and use it in GitHub Desktop.
Save amiika/4e2d9d326d419d7a286dd4a2235d3061 to your computer and use it in GitHub Desktop.
Markov chain launchpad for novation mini and Sonic Pi
# 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