Skip to content

Instantly share code, notes, and snippets.

@tehn
Last active February 2, 2019 22:49
Show Gist options
  • Save tehn/2a1b0535df95cadde79ab79f4e373d4b to your computer and use it in GitHub Desktop.
Save tehn/2a1b0535df95cadde79ab79f4e373d4b to your computer and use it in GitHub Desktop.
loom.lua for 2.0
-- Loom
--
-- Pattern weaver for grids.
--
-- Hold a grid key and press
-- another on the same row/col
-- to add a trigger or note.
-- Three keys clear a row/col.
--
-- ENC1/KEY2 : Change page
--
-- PAGE 1:
-- ENC2 : BPM
-- ENC3 : Add/Remove
-- KEY3 : Play/Pause
-- PAGE 2:
-- ENC2 : Root note/Select
-- ENC3 : Scale type/Note edit
-- KEY3 : Scale/Custom
-- PAGE 3:
-- Load/Save/Delete
--
-- v1.0.3
-- Concept Jay Gilligan
-- Code Mark Eats
--
local MusicUtil = require "musicutil"
local UI = require "ui"
local BeatClock = require "beatclock"
local MollyThePoly = require "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_FOLDER_PATH = data_dir .. "mark_eats/"
local DATA_FILE_PATH = DATA_FOLDER_PATH .. "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 root_note = 36
local scale_type = 1
local custom_scale = false
local scale_edit_id = 1
local scale_notes = {}
local scale_note_names = {}
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 DOWN_BRIGHTNESS = 3
local TRAIL_BRIGHTNESS = 3
local OUTSIDE_BRIGHTNESS = 3
local INACTIVE_BRIGHTNESS = 10
local ACTIVE_BRIGHTNESS = 15
local pages
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 save_data = {version = 1, patterns = {}}
local save_menu_items = {"Load", "Save", "Delete"}
local save_slot_list
local save_menu_list
local last_edited_slot = 0
local confirm_message
local confirm_function
local function copy_object(object)
if type(object) ~= 'table' then return object end
local result = {}
for k, v in pairs(object) do result[copy_object(k)] = copy_object(v) end
return result
end
local function update_save_slot_list()
local entries = {}
for i = 1, math.min(#save_data.patterns + 1, 999) do
local entry
if i <= #save_data.patterns then
entry = save_data.patterns[i].name
else
entry = "-"
end
if i == last_edited_slot then entry = entry .. "*" end
entries[i] = i .. ". " .. entry
end
save_slot_list.entries = entries
end
local function read_data()
local disk_data = tab.load(DATA_FILE_PATH)
if disk_data then
if disk_data.version then
if disk_data.version == 1 then
save_data = disk_data
else
print("Unrecognized data, version " .. disk_data.version)
end
end
else
os.execute("mkdir " .. DATA_FOLDER_PATH)
end
update_save_slot_list()
end
local function write_data()
tab.save(save_data, DATA_FILE_PATH)
end
local function load_pattern(index)
if index > #save_data.patterns then return end
local pattern = copy_object(save_data.patterns[index])
params:set("bpm", pattern.bpm)
params:set("step_length", pattern.step_length)
params:set("pattern_width", pattern.pattern_width)
params:set("pattern_height", pattern.pattern_height)
params:set("min_velocity", pattern.min_velocity)
params:set("max_velocity", pattern.max_velocity)
notes = pattern.notes
triggers = pattern.triggers
root_note = pattern.root_note
scale_type = pattern.scale_type
custom_scale = pattern.custom_scale
scale_notes = pattern.scale_notes
scale_note_names = MusicUtil.note_nums_to_names(scale_notes, true)
last_edited_slot = index
update_save_slot_list()
grid_dirty = true
end
local function save_pattern(index)
local pattern = {
name = os.date("%b %d %H:%M"),
bpm = params:get("bpm"),
step_length = params:get("step_length"),
pattern_width = params:get("pattern_width"),
pattern_height = params:get("pattern_height"),
min_velocity = params:get("min_velocity"),
max_velocity = params:get("max_velocity"),
notes = notes,
triggers = triggers,
root_note = root_note,
scale_type = scale_type,
custom_scale = custom_scale,
scale_notes = scale_notes
}
save_data.patterns[index] = copy_object(pattern)
last_edited_slot = index
update_save_slot_list()
write_data()
end
local function delete_pattern(index)
if index > 0 and index <= #save_data.patterns then
table.remove(save_data.patterns, index)
if index == last_edited_slot then
last_edited_slot = 0
elseif index < last_edited_slot then
last_edited_slot = last_edited_slot - 1
end
end
update_save_slot_list()
write_data()
end
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_kill()
-- 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 start_remove_animation(orientation, position)
local ani = {orientation = orientation, position = position, time_remaining = REMOVE_ANI_LENGTH }
table.insert(remove_animations, ani)
end
local function start_add_remove_animation(icon)
if #add_remove_animations < 16 then
local ani = {icon = icon, x = math.random(118) + 5, y = math.random(54) + 5, time_remaining = ADD_REMOVE_ANI_LENGTH}
table.insert(add_remove_animations, ani)
screen_dirty = true
end
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)
if show_screen_animation then
start_add_remove_animation("note")
else
notes_changed_timeout = NOTES_TRIGGERS_TIMEOUT_LENGTH
end
if pages.index == 1 or show_screen_animation then screen_dirty = true end
grid_dirty = true
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)
if show_screen_animation then
start_add_remove_animation("trigger")
else
triggers_changed_timeout = NOTES_TRIGGERS_TIMEOUT_LENGTH
end
if pages.index == 1 or show_screen_animation then screen_dirty = true end
grid_dirty = true
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
start_add_remove_animation("remove")
else
notes_changed_timeout = NOTES_TRIGGERS_TIMEOUT_LENGTH
end
start_remove_animation("col", note.position)
if pages.index == 1 or show_screen_animation then screen_dirty = true 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
start_add_remove_animation("remove")
else
triggers_changed_timeout = NOTES_TRIGGERS_TIMEOUT_LENGTH
end
start_remove_animation("row", trigger.position)
if pages.index == 1 or show_screen_animation then screen_dirty = true end
grid_dirty = true
end
end
local 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
local 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
local active_notes_this_step = {}
for _, t in pairs(triggers) do
-- Move triggers
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
-- Generate trigger 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
end
grid_dirty = true
end
for _, n in pairs(notes) do
-- Move notes
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
-- Generate note trails
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
-- Check for intersections
local n_top, n_bottom
if n.direction > 0 then
n_top = n.head - n.length + 1
n_bottom = n.head
else
n_top = n.head
n_bottom = n.head + n.length - 1
end
n_top = (n_top - 1) % params:get("pattern_height") + 1
n_bottom = (n_bottom - 1) % params:get("pattern_height") + 1
for _, t in pairs(triggers) do
-- Is the note on a trigger row?
if (n_top <= n_bottom and (t.position >= n_top and t.position <= n_bottom))
or (n_top > n_bottom and (t.position >= n_top or t.position <= n_bottom)) then
local t_left, t_right
if t.direction > 0 then
t_left = t.head - t.length + 1
t_right = t.head
else
t_left = t.head
t_right = t.head + t.length - 1
end
t_left = (t_left - 1) % params:get("pattern_width") + 1
t_right = (t_right - 1) % params:get("pattern_width") + 1
-- Is the trigger on the note column?
if (t_left <= t_right and (n.position >= t_left and n.position <= t_right))
or (t_left > t_right and (n.position >= t_left or n.position <= t_right)) then
table.insert(active_notes_this_step, scale_notes[n.position])
n.active = true
t.active = true
break
end
end
end
grid_dirty = true
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
note_on(sa)
table.insert(active_notes, sa)
end
screen_dirty = true
end
local function stop()
all_notes_kill()
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
local function screen_update()
if pages.index == 1 and #add_remove_animations > 0 then screen_dirty = true end
local time_increment = 1 / SCREEN_FRAMERATE
-- Add/remove animations
for i = #add_remove_animations, 1, -1 do
add_remove_animations[i].time_remaining = add_remove_animations[i].time_remaining - time_increment
if add_remove_animations[i].time_remaining <= 0 then
table.remove(add_remove_animations, i)
end
end
notes_changed_timeout = util.clamp(notes_changed_timeout - time_increment, 0, NOTES_TRIGGERS_TIMEOUT_LENGTH)
triggers_changed_timeout = util.clamp(triggers_changed_timeout - time_increment, 0, NOTES_TRIGGERS_TIMEOUT_LENGTH)
end
local function init_scale()
scale_notes = MusicUtil.generate_scale_of_length(root_note, scale_type, 16)
while #scale_notes < 16 do
table.insert(scale_notes, scale_notes[#scale_notes])
end
scale_note_names = MusicUtil.note_nums_to_names(scale_notes, true)
end
-- Encoder input
function enc(n, delta)
-- Global
if n == 1 then
pages:set_index_delta(util.clamp(delta, -1, 1), false)
save_menu_list:set_index(1)
else
-- Time
if pages.index == 1 then
if n == 2 and beat_clock.external == false then
params:delta("bpm", delta)
elseif n == 3 then
if delta > 0 then
add_random()
else
remove_last()
end
end
-- Pitch
elseif pages.index == 2 then
if custom_scale then
if n == 2 then
scale_edit_id = util.clamp(scale_edit_id + util.clamp(delta, -1, 1), 1, grid_w)
elseif n == 3 then
scale_notes[scale_edit_id] = util.clamp(scale_notes[scale_edit_id] + delta, 0, 127)
scale_note_names = MusicUtil.note_nums_to_names(scale_notes, true)
end
else
if n == 2 then
root_note = util.clamp(root_note + util.clamp(delta, -1, 1), 0, 127)
init_scale()
elseif n == 3 then
scale_type = util.clamp(scale_type + util.clamp(delta, -1, 1), 1, SCALES_LEN)
init_scale()
end
end
-- Load/Save
elseif pages.index == 3 then
if n == 2 then
save_slot_list:set_index_delta(util.clamp(delta, -1, 1))
elseif n == 3 then
save_menu_list:set_index_delta(util.clamp(delta, -1, 1))
end
end
end
screen_dirty = true
end
-- Key input
function key(n, z)
if z == 1 then
-- Key 2
if n == 2 then
if confirm_message then
confirm_message = nil
confirm_function = nil
else
pages:set_index_delta(1, true)
save_menu_list:set_index(1)
end
-- Key 3
elseif n == 3 then
if confirm_message then
confirm_function()
confirm_message = nil
confirm_function = nil
else
-- Time
if pages.index == 1 then
if not beat_clock.external then
if beat_clock.playing then
beat_clock:stop()
else
beat_clock:start()
end
end
-- Pitch
elseif pages.index == 2 then
custom_scale = not custom_scale
if not custom_scale then
init_scale()
else
scale_edit_id = 1
end
-- Load/Save
elseif pages.index == 3 then
-- Load
if save_menu_list.index == 1 then
load_pattern(save_slot_list.index)
-- Save
elseif save_menu_list.index == 2 then
if save_slot_list.index < #save_slot_list.entries then
confirm_message = UI.Message.new({"Replace saved pattern?"})
confirm_function = function() save_pattern(save_slot_list.index) end
else
save_pattern(save_slot_list.index)
end
-- Delete
elseif save_menu_list.index == 3 then
if save_slot_list.index < #save_slot_list.entries then
confirm_message = UI.Message.new({"Delete saved pattern?"})
confirm_function = function() delete_pattern(save_slot_list.index) end
end
end
end
end
end
screen_dirty = true
end
end
-- Grid event
local 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()
for x = 1, 16 do
grid_leds[x] = {}
trails[x] = {}
for y = 1, 16 do
grid_leds[x][y] = 0
trails[x][y] = 0
end
end
for k, v in pairs(MusicUtil.SCALES) do
if v.name == "Major Pentatonic" then
scale_type = k
break
end
end
init_scale()
grid_device = grid.connect(1)
grid_device.key = grid_event
beat_clock = BeatClock.new()
beat_clock.on_step = advance_step
beat_clock.on_stop = stop
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.init()
screen_refresh_metro.event = function()
screen_update()
if screen_dirty then
screen_dirty = false
redraw()
end
end
local grid_redraw_metro = metro.init()
grid_redraw_metro.event = function()
grid_update()
if grid_dirty 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 = grid.connect(value)
grid_device:all(0)
grid_device:refresh()
end}
params:add{type = "option", id = "output", name = "Output", options = options.OUTPUT, action = all_notes_kill}
params:add{type = "number", id = "midi_out_device", name = "MIDI Out Device", min = 1, max = 4, default = 1,
action = function(value)
midi_out_device = midi.connect(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_kill()
midi_out_channel = value
end}
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 = midi.connect(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 = 240, default = beat_clock.bpm,
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 = 10,
action = function(value)
beat_clock.ticks_per_step = 96 / options.STEP_LENGTH_DIVIDERS[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 = 4, max = 64, default = 16,
action = function()
grid_dirty = true
end}
params:add{type = "number", id = "pattern_height", name = "Pattern Height", min = 4, max = 64, default = 16,
action = function()
grid_dirty = true
end}
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
pages = UI.Pages.new(1, 3)
save_slot_list = UI.ScrollingList.new(5, 9, 1, {})
save_menu_list = UI.List.new(92, 20, 1, save_menu_items)
playback_icon = UI.PlaybackIcon.new(121, 1)
screen.aa(1)
screen_refresh_metro:start(1 / SCREEN_FRAMERATE)
grid_redraw_metro:start(1 / GRID_FRAMERATE)
beat_clock:start()
-- Data
read_data()
end
function grid_redraw()
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
local function draw_add_remove_icons()
screen.level(15)
screen.line_width(1)
local RADIUS = 4
for k, v in pairs(add_remove_animations) do
if v.icon == "remove" then
screen.move(v.x - RADIUS, v.y - RADIUS)
screen.line(v.x + RADIUS, v.y + RADIUS)
screen.stroke()
screen.move(v.x + RADIUS, v.y - RADIUS)
screen.line(v.x - RADIUS, v.y + RADIUS)
screen.stroke()
elseif v.icon == "trigger" then
screen.move(v.x - RADIUS - 2, v.y)
screen.line(v.x + RADIUS, v.y)
screen.stroke()
screen.move(v.x, v.y - RADIUS)
screen.line(v.x + RADIUS, v.y)
screen.line(v.x, v.y + RADIUS)
screen.stroke()
else
screen.circle(v.x, v.y + RADIUS * 0.5, RADIUS * 0.5)
screen.stroke()
screen.move(v.x + RADIUS * 0.5, v.y + RADIUS * 0.5)
screen.line(v.x + RADIUS * 0.5, v.y - RADIUS - 2)
screen.line(v.x + RADIUS * 0.5 + 3, v.y - RADIUS + 1)
screen.stroke()
end
end
end
function redraw()
screen.clear()
if confirm_message then
confirm_message:redraw()
else
pages:redraw()
if beat_clock.playing then
playback_icon.status = 1
else
playback_icon.status = 3
end
playback_icon:redraw()
-- Time
if pages.index == 1 then
-- BPM
screen.move(5, 29)
if beat_clock.external then
screen.level(3)
screen.text("External")
else
screen.level(15)
screen.text(params:get("bpm") .. " BPM")
end
-- Status
if notes_changed_timeout > 0 then screen.level(15)
else screen.level(3) end
screen.move(69, 29)
screen.text("Notes " .. #notes)
if triggers_changed_timeout > 0 then screen.level(15)
else screen.level(3) end
screen.move(69, 40)
screen.text("Triggers " .. #triggers)
-- Pitch
elseif pages.index == 2 then
-- Scale name
screen.move(5, 10)
if custom_scale then
screen.level(3)
screen.text("Custom")
else
screen.level(15)
screen.text(MusicUtil.note_num_to_name(root_note, true) .. " " .. MusicUtil.SCALES[scale_type].name)
end
-- Scale notes
local x, y = 5, 14
local COLS = 4
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 or (custom_scale and i == scale_edit_id) then screen.level(15)
else screen.level(3) end
screen.move(x, y)
screen.text(scale_note_names[i])
x = x + 25
end
-- Load/Save
elseif pages.index == 3 then
save_slot_list:redraw()
save_menu_list:redraw()
end
-- Icons
screen.fill()
draw_add_remove_icons()
end
screen.update()
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment