Skip to content

Instantly share code, notes, and snippets.

@ypxk
Last active January 5, 2019 21:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ypxk/c7261783a06bf6b56cc064ef53c017c1 to your computer and use it in GitHub Desktop.
Save ypxk/c7261783a06bf6b56cc064ef53c017c1 to your computer and use it in GitHub Desktop.
-- LA Drivers
-- thanks to mark
--enc 1: move key center by 5th
--enc 2: move key center by 4th
--key 2: walk through current scale
--enc 3: add random/remove last
--key 2: parallel modulation
--key 3: random scale
MusicUtil = require "mark_eats/musicutil"
local UI = require "mark_eats/ui"
local BeatClock = require "beatclock"
local MollyThePoly = require "mark_eats/mollythepoly"
engine.name = "MollyThePoly"
local options = {}
options.OUTPUT = {"Audio", "MIDI", "Audio + MIDI"}
options.STEP_LENGTH_NAMES = {"1 bar", "1/2", "1/3", "1/4", "1/6", "1/8", "1/12", "1/16", "1/24", "1/32", "1/48", "1/64"}
options.STEP_LENGTH_DIVIDERS = {1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64}
local DATA_FILE_PATH = data_dir .. "mark_eats/loom.data"
local SCREEN_FRAMERATE = 15
local screen_dirty = true
local GRID_FRAMERATE = 30
local grid_dirty = true
local grid_leds = {}
local grid_w, grid_h = 16, 8
local beat_clock
local grid_device
local midi_in_device
local midi_out_device
local midi_out_channel
local notes = {}
local triggers = {}
local custom_scale = false
local scale_edit_id = 1
local scale_notes = {}
local SCALES_LEN = #MusicUtil.SCALES
local active_notes = {}
local down_marks = {}
local down_keys = {}
local trails = {}
local remove_animations = {}
local DOWN_ANI_LENGTH = 0.2
local REMOVE_ANI_LENGTH = 0.4
local TRAIL_ANI_LENGTH = 6.0
local pages
local tabs
local playback_icon
local add_remove_animations = {}
local ADD_REMOVE_ANI_LENGTH = 0.2
local notes_changed_timeout = 0
local triggers_changed_timeout = 0
local NOTES_TRIGGERS_TIMEOUT_LENGTH = 0.2
local nextScale
rootNote = math.random(12)
gridScale = {}
minorTonality = true
tonality = 'Natural Minor'
octave = 4
timeLast = util.time()
transposeAmount = 0
function tonality_status ()
if minor_tonality then
tonality = 'Natural Minor'
elseif not minor_tonality then
tonality = 'Major'
end
return tonality
end
function get_incoming_scale(s, t) --semitones to transpose, tonality,
local transposeBy = s
local scaleType = t
local incomingScale = {}
rootNote = (rootNote + transposeBy) % 12
incomingScale = MusicUtil.generate_scale_of_length(rootNote, scaleType, 128)
return incomingScale
end
function update_grid_scale(s) --incoming scale
local incomingScale = s
local notesToKeep = {}
local incomingGridScale = {}
--get tone map of new pitches
for k,v in pairs(masterScale) do
local randomDir = math.random(2)
for n_k, n_v in pairs(incomingScale) do
if n_v == v then
notesToKeep[k] = v
--print('same', notesToKeep[k])
break
elseif n_v ~= v then
if randomDir > 1 then
if n_v - 1 == v then
notesToKeep[k] = v + 1
-- print('diff1', notesToKeep[k])
break
end
else
if n_v + 1 == v then
notesToKeep[k] = v - 1
-- print('diff2', notesToKeep[k])
break
end
end
end
end
end
--map scale to grid swap with place scale function
for k,v in pairs(notesToKeep) do
--print(k,v)
if #incomingGridScale < 16 then
if v >= gridScale[1] then
incomingGridScale[#incomingGridScale + 1] = v
end
end
end
incomingGridScaledeDupe = duplicateCheck(incomingGridScale, incomingScale)
--
masterScale = incomingScale
return incomingGridScaledeDupe, incomingScale
end
function duplicateCheck(s,i_s) --scale, incoming scale
local incomingGridScale = s
local incomingScale = i_s
local attemptCounter = 1
for k,v in pairs(incomingGridScale) do
if incomingGridScale[k] ==incomingGridScale[k+1] then
duplicate = true
--print(k+1, incomingGridScale[k+1], incomingGridScale[k+2])
for a,b in pairs(incomingScale) do
if incomingGridScale[k+1] == incomingScale[a] then
incomingGridScale[k+1] = incomingScale[a+1]
break
end
end
end
end
--print('--------------')
return incomingGridScale
end
function place_scale_on_grid(s) --scale
local scaleBuilder = {}
for k,v in pairs(s) do
if #scaleBuilder < 16 then
if v >= (12*octave) then
scaleBuilder[#scaleBuilder + 1] = v
end
end
end
--for k,v in pairs(scaleBuilder) do print(k,v) end
return scaleBuilder
end
function pivot_scale(s, p) --scale, pivot amount
local toPiv = s
local pivedScale = {}
local pivAmount = p
for k,v in pairs(toPiv) do
for n_k, n_v in pairs(masterScale) do
if v == n_v then
if masterScale[n_k+p] then
pivedScale[k] = masterScale[n_k+p]
break
else
return toPiv
end
end
end
end
return pivedScale
end
function enc(n, delta)
local newScale
if n == 1 then
if delta > 0 then
transposeAmount = 7
elseif delta < 0 then
transposeAmount = -7
end
if util.time() - timeLast > .1 then
newScale, MasterScale = update_grid_scale(get_incoming_scale(transposeAmount, tonality))
gridScale = place_scale_on_grid(newScale)
--print(MusicUtil.note_num_to_name(masterScale[1], true))
end
elseif n == 2 then
if not shiftA then
if delta > 0 then
transposeAmount = 5
elseif delta < 0 then
transposeAmount = -5
end
if util.time() - timeLast > .1 then
newScale = update_grid_scale(get_incoming_scale(transposeAmount, tonality))
gridScale = place_scale_on_grid(newScale)
end
elseif shiftA then
if delta > 0 then
gridScale = pivot_scale(gridScale, 1)
elseif delta < 0 then
gridScale = pivot_scale(gridScale, -1)
end
end
elseif n == 3 then
if delta > 1 then
add_random()
elseif delta < 1 then
remove_last()
end
end
timeLast = util.time()
redraw()
end
function key(n, z)
if z == 1 then
if n == 2 then
shiftA = true
aTime = util.time()
elseif n == 3 then
transposeAmount = math.random(12) - 6
newScale = update_grid_scale(get_incoming_scale(transposeAmount, tonality))
gridScale = place_scale_on_grid(newScale)
end
elseif z == 0 then
if n == 2 then
shiftA = false
if util.time() - aTime < .5 then
if tonality == 'Natural Minor' then
tonality = 'Major'
else
tonality = 'Natural Minor'
end
transposeAmount = 0
newScale = update_grid_scale(get_incoming_scale(transposeAmount, tonality))
gridScale = place_scale_on_grid(newScale)
end
end
end
end
--print(circle_position, key_name)
-----------------
--[[ ]]
local function note_on(note_num)
local min_vel, max_vel = params:get("min_velocity"), params:get("max_velocity")
if min_vel > max_vel then
max_vel = min_vel
end
local note_midi_vel = math.random(min_vel, max_vel)
-- print("note_on", note_num, note_midi_vel)
-- Audio engine out
if params:get("output") == 1 or params:get("output") == 3 then
engine.noteOn(note_num, MusicUtil.note_num_to_freq(note_num), note_midi_vel / 127)
end
-- MIDI out
if (params:get("output") == 2 or params:get("output") == 3) then
midi_out_device.note_on(note_num, note_midi_vel, midi_out_channel)
end
end
local function note_off(note_num)
-- print("note_off", note_num)
-- Audio engine out
if params:get("output") == 1 or params:get("output") == 3 then
engine.noteOff(note_num)
end
-- MIDI out
if (params:get("output") == 2 or params:get("output") == 3) then
midi_out_device.note_off(note_num, nil, midi_out_channel)
end
end
local function all_notes_off()
-- Audio engine out
engine.noteKillAll()
-- MIDI out
if (params:get("output") == 2 or params:get("output") == 3) then
for _, a in pairs(active_notes) do
midi_out_device.note_off(a, 96, midi_out_channel)
end
end
active_notes = {}
end
local function add_note(position, head, length, direction, show_screen_animation)
length = length or 1
direction = direction or 1
local note = {position = position, head = head, length = length, advance_countdown = length, direction = direction, active = false}
table.insert(notes, note)
end
local function add_trigger(position, head, length, direction, show_screen_animation)
length = length or 1
direction = direction or 1
local trigger = {position = position, head = head, length = length, advance_countdown = length, direction = direction, active = false}
table.insert(triggers, trigger)
end
local function remove_note(position, show_screen_animation, silent)
local note
if position then
for k, v in pairs(notes) do
if v.position == position then
note = table.remove(notes, k)
break
end
end
else
note = table.remove(notes)
end
if note and not silent then
if show_screen_animation then
else
notes_changed_timeout = NOTES_TRIGGERS_TIMEOUT_LENGTH
end
grid_dirty = true
end
end
local function remove_trigger(position, show_screen_animation, silent)
local trigger
if position then
for k, v in pairs(triggers) do
if v.position == position then
trigger = table.remove(triggers, k)
break
end
end
else
trigger = table.remove(triggers)
end
if trigger and not silent then
if show_screen_animation then
else
triggers_changed_timeout = NOTES_TRIGGERS_TIMEOUT_LENGTH
end
grid_dirty = true
end
end
function add_random()
if #notes >= grid_w and #triggers >= grid_h then return end
-- Note
if math.random() >= 0.5 then
local available_positions = {}
for i = 1, grid_w do
local available = true
for _, vn in pairs(notes) do
if vn.position == i then
available = false
break
end
end
if available then
table.insert(available_positions, i)
end
end
if #available_positions > 0 then
local length = util.round(math.pow(math.random(), 4) * (grid_h - 2) + 1)
add_note(available_positions[math.random(#available_positions)], math.random(grid_h), length, (math.random() >= 0.5 and 1 or -1), true)
end
-- Trigger
else
local available_positions = {}
for i = 1, grid_h do
local available = true
for _, vt in pairs(triggers) do
if vt.position == i then
available = false
break
end
end
if available then
table.insert(available_positions, i)
end
end
if #available_positions > 0 then
local length = util.round(math.pow(math.random(), 4) * (grid_w - 2) + 1)
add_trigger(available_positions[math.random(#available_positions)], math.random(grid_w), length, (math.random() >= 0.5 and 1 or -1), true)
end
end
end
function remove_last()
if #notes == 0 and #triggers == 0 then return end
if math.random() >= 0.5 then
remove_note(nil, true)
else
remove_trigger(nil, true)
end
end
local function advance_step()
if grid_device then
grid_w = grid_device.cols()
grid_h = grid_device.rows()
if grid_w ~= 8 and grid_w ~= 16 then grid_w = 16 end
if grid_h ~= 8 and grid_h ~= 16 then grid_h = 8 end
end
for _, n in pairs(notes) do
n.advance_countdown = n.advance_countdown - 1
if n.advance_countdown == 0 then
n.advance_countdown = n.length
if n.direction > 0 then n.head = n.head % params:get("pattern_height") + 1
else n.head = (n.head + params:get("pattern_height") - 2) % params:get("pattern_height") + 1 end
end
n.active = false
end
local active_notes_this_step = {}
for _, t in pairs(triggers) do
-- Progress
t.advance_countdown = t.advance_countdown - 1
if t.advance_countdown == 0 then
t.advance_countdown = t.length
if t.direction > 0 then t.head = t.head % params:get("pattern_width") + 1
else t.head = (t.head + params:get("pattern_width") - 2) % params:get("pattern_width") + 1 end
end
t.active = false
-- Check for intersections and generate trails
local tx
for ti = 0, t.length - 1 do
tx = t.head + (ti * t.direction * -1)
tx = (tx - 1) % params:get("pattern_width") + 1
if tx <= grid_w then trails[tx][t.position] = TRAIL_ANI_LENGTH end
for _, n in pairs(notes) do
local ny
for ni = 0, n.length - 1 do
ny = n.head + (ni * n.direction * -1)
ny = (ny - 1) % params:get("pattern_height") + 1
if ny <= grid_h then trails[n.position][ny] = TRAIL_ANI_LENGTH end
if tx == n.position and t.position == ny then
if not n.active then
table.insert(active_notes_this_step, gridScale[n.position])
end
n.active = true
t.active = true
break
end
end
end
end
end
-- Generate trails for notes if need be
if #triggers == 0 then
for _, n in pairs(notes) do
local ny
for ni = 0, n.length - 1 do
ny = n.head + (ni * n.direction * -1)
ny = (ny - 1) % params:get("pattern_height") + 1
if ny <= grid_h then trails[n.position][ny] = TRAIL_ANI_LENGTH end
end
end
end
-- Work out which need noteOffs
for i = #active_notes, 1, -1 do
local still_active = false
for sk, sa in pairs(active_notes_this_step) do
if sa == active_notes[i] then
still_active = true
table.remove(active_notes_this_step, sk)
break
end
end
if not still_active then
note_off(active_notes[i])
table.remove(active_notes, i)
end
end
-- Add remaining, the new notes
for _, sa in pairs(active_notes_this_step) do
if #active_notes < params:get("max_active_notes") then
note_on(sa)
table.insert(active_notes, sa)
end
end
screen_dirty = true
grid_dirty = true
end
local function reset_step()
beat_clock:reset()
end
local function grid_update()
if #down_marks > 0 or #remove_animations > 0 then grid_dirty = true end
local time_increment = 1 / GRID_FRAMERATE
-- Trails
for x = 1, grid_w do
for y = 1, grid_h do
trails[x][y] = util.clamp(trails[x][y] - time_increment, 0, TRAIL_ANI_LENGTH)
if trails[x][y] > 0 then grid_dirty = true end
end
end
-- Down marks
for i = #down_marks, 1, -1 do
if not down_marks[i].active then
down_marks[i].time_remaining = down_marks[i].time_remaining - time_increment
if down_marks[i].time_remaining <= 0 then
table.remove(down_marks, i)
end
end
end
-- Remove animations
for i = #remove_animations, 1, -1 do
remove_animations[i].time_remaining = remove_animations[i].time_remaining - time_increment
if remove_animations[i].time_remaining <= 0 then
table.remove(remove_animations, i)
end
end
end
function grid_event(x, y, z)
if z == 1 then
-- Is there a relevant down mark?
local relevant_down_mark = nil
for k, v in pairs(down_marks) do
-- Re-activate fading down mark
if v.x == x and v.y == y then
v.active = true
v.time_remaining = DOWN_ANI_LENGTH
relevant_down_mark = v
break
-- Note
elseif v.x == x and v.active then
local three_keys = false
for _, kv in pairs(down_keys) do
if kv.x == x then
three_keys = true
break
end
end
if three_keys then
remove_note(x, true, false)
else
remove_note(x, false, true)
add_note(x, v.y, math.abs(v.y - y), (y < v.y and 1 or -1), false)
end
relevant_down_mark = v
-- Trigger
elseif v.y == y and v.active then
local three_keys = false
for _, kv in pairs(down_keys) do
if kv.y == y then
three_keys = true
break
end
end
if three_keys then
remove_trigger(y, true, false)
else
remove_trigger(y, false, true)
add_trigger(y, v.x, math.abs(v.x - x), (x < v.x and 1 or -1), false)
end
relevant_down_mark = v
end
end
-- Make it the down mark
if relevant_down_mark then
if relevant_down_mark.x ~= x or relevant_down_mark.y ~= y then
table.insert(down_keys, {x = x, y = y})
end
else
table.insert(down_marks, {active = true, x = x, y = y, time_remaining = DOWN_ANI_LENGTH})
end
else
for _, v in pairs(down_marks) do
if v.x == x and v.y == y then
v.active = false
break
end
end
for k, v in pairs(down_keys) do
if v.x == x and v.y == y then
table.remove(down_keys, k)
break
end
end
end
grid_dirty = true
end
function init()
--me
masterScale = MusicUtil.generate_scale_of_length(rootNote, tonality, 128)
gridScale = place_scale_on_grid(masterScale)
--og
for x = 1, 16 do
grid_leds[x] = {}
trails[x] = {}
for y = 1, 16 do
trails[x][y] = 0
end
end
grid_device = grid.connect(1)
grid_device.event = grid_event
beat_clock = BeatClock.new()
beat_clock.on_step = advance_step
beat_clock.on_select_internal = function()
beat_clock:start()
screen_dirty = true
end
beat_clock.on_select_external = function()
reset_step()
screen_dirty = true
end
midi_in_device = midi.connect(1)
midi_in_device.event = function(data)
beat_clock:process_midi(data)
if not beat_clock.playing and playback_icon.status == 1 then
screen_dirty = true
end
end
midi_out_device = midi.connect(1)
midi_out_device.event = function() end
local screen_refresh_metro = metro.alloc()
screen_refresh_metro.callback = function()
if screen_dirty then
screen_dirty = false
redraw()
end
end
local grid_redraw_metro = metro.alloc()
grid_redraw_metro.callback = function()
grid_update()
if grid_dirty and grid_device.attached() then
grid_dirty = false
grid_redraw()
end
end
-- Add params
params:add{type = "number", id = "grid_device", name = "Grid Device", min = 1, max = 4, default = 1, action = function(value)
grid_device.all(0)
grid_device.refresh()
grid_device:reconnect(value)
end}
params:add{type = "option", id = "output", name = "Output", options = options.OUTPUT, action = all_notes_off}
params:add{type = "number", id = "midi_out_device", name = "MIDI Out Device", min = 1, max = 4, default = 1, action = function(value)
midi_out_device:reconnect(value)
end}
params:add{type = "number", id = "midi_out_channel", name = "MIDI Out Channel", min = 1, max = 16, default = 1, action = function(value)
all_notes_off()
midi_out_channel = value
end}
params:add{type = "number", id = "max_active_notes", name = "Max Active Notes", min = 1, max = 16, default = 16}
params:add{type = "option", id = "clock", name = "Clock", options = {"Internal", "External"}, default = beat_clock.external or 2 and 1, action = function(value)
beat_clock:clock_source_change(value)
end}
params:add{type = "number", id = "clock_midi_in_device", name = "Clock MIDI In Device", min = 1, max = 4, default = 1, action = function(value)
midi_in_device:reconnect(value)
end}
params:add{type = "option", id = "clock_out", name = "Clock Out", options = {"Off", "On"}, default = beat_clock.send or 2 and 1, action = function(value)
if value == 1 then beat_clock.send = false
else beat_clock.send = true end
end}
params:add_separator()
params:add{type = "number", id = "bpm", name = "BPM", min = 1, max = 480, default = 140, action = function(value)
beat_clock:bpm_change(value)
screen_dirty = true
end}
params:add{type = "option", id = "step_length", name = "Step Length", options = options.STEP_LENGTH_NAMES, default = 8, action = function(value)
beat_clock.steps_per_beat = options.STEP_LENGTH_DIVIDERS[value] / 4
beat_clock:bpm_change(beat_clock.bpm)
end}
params:add{type = "number", id = "pattern_width", name = "Pattern Width", min = 8, max = 64, default = 16}
params:add{type = "number", id = "pattern_height", name = "Pattern Height", min = 8, max = 64, default = 8}
params:add{type = "number", id = "min_velocity", name = "Min Velocity", min = 1, max = 127, default = 80}
params:add{type = "number", id = "max_velocity", name = "Max Velocity", min = 1, max = 127, default = 100}
params:add_separator()
midi_out_channel = params:get("midi_out_channel")
-- Engine params
MollyThePoly.add_params()
-- UI
screen.aa(1)
screen_refresh_metro:start(1 / SCREEN_FRAMERATE)
grid_redraw_metro:start(1 / GRID_FRAMERATE)
beat_clock:start()
end
function grid_redraw()
local DOWN_BRIGHTNESS = 1
local TRAIL_BRIGHTNESS = 1
local OUTSIDE_BRIGHTNESS = 1
local INACTIVE_BRIGHTNESS = 3
local ACTIVE_BRIGHTNESS = 12
local brightness
-- Draw trails
for x = 1, 16 do
for y = 1, 16 do
if trails[x][y] then grid_leds[x][y] = util.round(util.linlin(0, TRAIL_ANI_LENGTH, 0, TRAIL_BRIGHTNESS, trails[x][y]))
else grid_leds[x][y] = 0 end
if (x > params:get("pattern_width") or y > params:get("pattern_height")) and grid_leds[x][y] < OUTSIDE_BRIGHTNESS then grid_leds[x][y] = OUTSIDE_BRIGHTNESS end
end
end
-- Draw down marks
for k, v in pairs(down_marks) do
brightness = util.round(util.linlin(0, DOWN_ANI_LENGTH, 0, DOWN_BRIGHTNESS, v.time_remaining))
for i = 1, grid_w do
if grid_leds[i][v.y] < brightness then grid_leds[i][v.y] = brightness end
end
for i = 1, grid_h do
if grid_leds[v.x][i] < brightness then grid_leds[v.x][i] = brightness end
end
if v.active and grid_leds[v.x][v.y] < INACTIVE_BRIGHTNESS then grid_leds[v.x][v.y] = INACTIVE_BRIGHTNESS end
end
-- Draw remove animations
for _, v in pairs(remove_animations) do
brightness = util.round(util.linlin(0, REMOVE_ANI_LENGTH, 0, 15, v.time_remaining))
if v.orientation == "row" then
for i = 1, grid_w do
if grid_leds[i][v.position] < brightness then grid_leds[i][v.position] = brightness end
end
else
for i = 1, grid_h do
if grid_leds[v.position][i] < brightness then grid_leds[v.position][i] = brightness end
end
end
end
-- Draw notes
for _, n in pairs(notes) do
if n.active then brightness = ACTIVE_BRIGHTNESS
else brightness = INACTIVE_BRIGHTNESS end
if n.position <= grid_w then
local ny
for i = 0, n.length - 1 do
ny = n.head + (i * n.direction * -1)
ny = (ny - 1) % params:get("pattern_height") + 1
if ny > 0 and ny <= grid_h then
grid_leds[n.position][ny] = brightness
end
end
end
end
-- Draw triggers
for _, t in pairs(triggers) do
if t.active then brightness = ACTIVE_BRIGHTNESS
else brightness = INACTIVE_BRIGHTNESS end
if t.position <= grid_h then
local tx
for i = 0, t.length - 1 do
tx = t.head + (i * t.direction * -1)
tx = (tx - 1) % params:get("pattern_width") + 1
if tx > 0 and tx <= grid_w then
grid_leds[tx][t.position] = brightness
end
end
end
end
for x = 1, grid_w do
for y = 1, grid_h do
grid_device.led(x, y, grid_leds[x][y])
end
end
grid_device.refresh()
end
function redraw()
screen.clear()
-- Scale name
screen.move(5, 10)
screen.level(15)
screen.text(MusicUtil.note_num_to_name(rootNote) .. " " .. tonality)
-- Scale notes
local x, y = 5, 14
local scale_note_names = MusicUtil.note_nums_to_names(gridScale, true)
local COLS = 4
if #scale_note_names ~= 16 then
--print (#scale_note_names)
else
for i = 1, grid_w do
if (i - 1) % COLS == 0 then x, y = 5, y + 11 end
local is_active = false
for _, n in pairs(notes) do
if n.position == i and n.active then
is_active = true
break
end
end
local underline_length = 10
if string.len(scale_note_names[i]) > 3 then
underline_length = 18
elseif string.len(scale_note_names[i]) > 2 then
underline_length = 16
end
if custom_scale and i == scale_edit_id then
screen.level(15)
screen.move(x - 1, y + 2.5)
screen.line(x + underline_length, y + 2.5)
screen.stroke()
end
if is_active then screen.level(15)
else screen.level(3) end
screen.move(x, y)
screen.text(scale_note_names[i])
x = x + 25
end
end
screen.update()
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment