Skip to content

Instantly share code, notes, and snippets.

@j-flee
Last active October 13, 2018 07:20
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 j-flee/658f1900444179a47c3f8ef17a931915 to your computer and use it in GitHub Desktop.
Save j-flee/658f1900444179a47c3f8ef17a931915 to your computer and use it in GitHub Desktop.
earthsea grid and pattern recording + passersby engine demo
-- Earth + Passersby
--
-- MIDI controlled West Coast
-- style mono synth.
--
-- ENC1/KEY2 : Change page
-- KEY3 : Change tab
-- ENC2/3 : Adjust parameters
--
-- grid pattern player:
-- 1 1 record toggle
-- 1 2 play toggle
-- 1 4-7 octave shift (-1 to +2)
-- 1 8 transpose mode
--
-- Responds to MIDI and grid
--
-- v1.0.1 Mark Eats
-- Earthsea grid pattern added:
-- Johnathan F. Lee
--
local MusicUtil = require "mark_eats/musicutil"
local Graph = require "mark_eats/graph"
local EnvGraph = require "mark_eats/envgraph"
local Passersby = require "mark_eats/passersby"
-- from earthsea
local tab = require 'tabutil'
local pattern_time = require 'pattern_time'
local g = grid.connect()
local mode_transpose = 0
local root = { x=5, y=5 }
local trans = { x=5, y=5 }
local lit = {}
local range = 0
local MAX_NUM_VOICES = 4
-- current count of active voices
local nvoices = 0
-- from earthsea end
local SCREEN_FRAMERATE = 15
local screen_refresh_metro
local screen_dirty = true
local PAGES = 4
local page_id = 1
local tab_id = 1
local input_indicator_active = false
local input_indicator_metro
local wave_table = {}
local wave = {}
local wave_graph
local SUB_SAMPLING = 4
local fm_graph
local lpg_graph
local lpg_status = {}
local lpg_status_metro
local spring_path = {}
local lfo_graph
local dice_throw_vel = 0
local dice_throw_progress = 0
local dice_thrown = false
local dice_need_update = false
local dice = {}
local mod_wheel = 0
local wave_shape = {actual = 0, modu = 0, dirty = true}
local wave_folds = {actual = 0, modu = 0, dirty = true}
local fm1_amount = {actual = 0, modu = 0, dirty = true}
local fm2_amount = {actual = 0, modu = 0, dirty = true}
local lpg_peak = {actual = 0, modu = 1, dirty = true}
local lpg_decay = {actual = 0, modu = 0, dirty = true}
local reverb_mix = {actual = 0, modu = 0, dirty = true}
local lfo_freq = {dirty = true}
local lfo_amount = {dirty = true}
local lfo_destinations = {dirty = true}
local drift = {actual = 0, dirty = true}
engine.name = "Passersby"
-- earthsea grid
function g.event(x, y, z)
if x == 1 then
if z == 1 then
if y == 1 and pat.rec == 0 then
mode_transpose = 0
trans.x = 5
trans.y = 5
pat:stop()
-- engine.stopAll()
-- stop_all_screen_notes()
pat:clear()
pat:rec_start()
elseif y == 1 and pat.rec == 1 then
pat:rec_stop()
if pat.count > 0 then
root.x = pat.event[1].x
root.y = pat.event[1].y
trans.x = root.x
trans.y = root.y
pat:start()
end
elseif y == 2 and pat.play == 0 and pat.count > 0 then
if pat.rec == 1 then
pat:rec_stop()
end
pat:start()
elseif y == 2 and pat.play == 1 then
pat:stop()
-- engine.stopAll()
-- stop_all_screen_notes()
nvoices = 0
lit = {}
elseif y>3 and y<8 then
range = 6-y
elseif y == 8 then
mode_transpose = 1 - mode_transpose
end
end
else
if mode_transpose == 0 then
local e = {}
e.id = x*8 + y
e.x = x
e.y = y
e.state = z
pat:watch(e)
grid_note(e)
else
trans.x = x
trans.y = y
end
end
gridredraw()
end
function grid_note(e)
local note = (24+(12*range)) + ((7-e.y)*5) + e.x
if e.state > 0 then
if nvoices < MAX_NUM_VOICES then
--engine.start(id, getHz(x, y-1))
--print("grid > "..id.." "..note)
--engine.start(e.id, getHzET(note))
engine.noteOn(e.id, MusicUtil.note_num_to_freq(note), 100)
-- start_screen_note(note)
lit[e.id] = {}
lit[e.id].x = e.x
lit[e.id].y = e.y
nvoices = nvoices + 1
end
else
if lit[e.id] ~= nil then
-- engine.stop(e.id)
-- stop_screen_note(note)
lit[e.id] = nil
nvoices = nvoices - 1
end
end
gridredraw()
end
function grid_note_trans(e)
local note = (24+(12*range)) + ((7-e.y+(root.y-trans.y))*5) + e.x + (trans.x-root.x)
if e.state > 0 then
if nvoices < MAX_NUM_VOICES then
--engine.start(id, getHz(x, y-1))
--print("grid > "..id.." "..note)
--engine.start(e.id, getHzET(note))
engine.noteOn(e.id, MusicUtil.note_num_to_freq(note), 127)
-- start_screen_note(note)
lit[e.id] = {}
lit[e.id].x = e.x + trans.x - root.x
lit[e.id].y = e.y + trans.y - root.y
nvoices = nvoices + 1
end
else
-- engine.stop(e.id)
-- stop_screen_note(note)
lit[e.id] = nil
nvoices = nvoices - 1
end
gridredraw()
end
function gridredraw()
g.all(0)
g.led(1,1,3 + pat.rec * 10)
g.led(1,2,3 + pat.play * 10)
g.led(1,8,3 + mode_transpose * 10)
for i=4,7 do
g.led(1,i,2)
end
g.led(1,6,3)
g.led(1,6-range,7)
if mode_transpose == 1 then g.led(trans.x, trans.y, 4) end
for i,e in pairs(lit) do
g.led(e.x, e.y,15)
end
g:refresh()
end
-- earthsea grid end
-- Utilities
local function generate_wave_table(cycles, length)
local wave_table = {{},{},{},{}}
for sx = 1, length do
local x = util.linlin(1, length, 0, cycles, sx)
local square = math.abs(x * 2 % 2 - 1) - 0.5
square = square > 0 and 0.5 or math.floor(square) * 0.5
table.insert(wave_table[1], math.sin(x * 2 * math.pi)) -- Sine
table.insert(wave_table[2], math.abs((x * 2 - 0.5) % 2 - 1) * 2 - 1) -- Tri
table.insert(wave_table[3], square) -- Square
table.insert(wave_table[4], (1 - (x + 0.25) % 1) * 2 - 1) -- Saw
end
return wave_table
end
local function generate_wave(x)
x = util.round(x)
local index_f = wave_shape.actual * (#wave_table - 1) + 1
local index = util.round(index_f)
local delta = index_f - index
local index_offset = delta < 0 and -1 or 1
local y
-- Wave table lookup
if delta == 0 then
y = wave_table[index][x]
else
y = wave_table[index + index_offset][x] * math.abs(delta) + wave_table[index][x] * (1 - math.abs(delta))
end
-- Wave folding
y = y * (1 + wave_folds.actual)
local abs_y = math.abs(y)
if abs_y > 1 then
local folded = abs_y % 1
if math.floor(abs_y - 1) % 2 == 0 then
folded = 1 - folded
else
folded = folded
end
folded = folded * 2 - 1
if y < 0 then folded = folded * -1 end
y = folded
end
return y
end
local function generate_spring_path(width, height, turns)
local spring_path = {}
for y = 0, height - 1 do
local progress = util.linlin(0, height - 1, 0, math.pi * turns * 2, y)
table.insert(spring_path, {x = width * 0.5 + math.sin(progress) * width * 0.5, y = y})
end
return spring_path
end
local function generate_lfo_wave(x)
x = x * util.linlin(Passersby.specs.LFO_FREQ.minval, Passersby.specs.LFO_FREQ.maxval, 0.5, 10, params:get("lfo_frequency"))
return (math.abs((x * 2 - 0.5) % 2 - 1) * 2 - 1) * params:get("lfo_amount")
end
local function randomize_dice()
for i = 1, 2 do
local direction = 0
if dice[i] then
direction = (dice[i].top_angle - dice[i].table_angle > 0) and 1 or -1
end
dice[i] = {}
dice[i].face = math.random(6)
dice[i].top_angle = 1 + math.random() * 2
if math.random() > 0.5 then dice[i].top_angle = dice[i].top_angle * -1 end
dice[i].table_angle = math.random() * 0.5 - 0.25 + (2 * math.pi * direction)
end
end
local function set_dice_throw_vel(vel_delta)
dice_throw_vel = util.clamp(dice_throw_vel + vel_delta, -0.35, 0.3)
dice_need_update = true
end
local function start_input_indicator_timeout()
input_indicator_active = true
screen_dirty = true
if input_indicator_metro.is_running then
input_indicator_metro:stop()
end
input_indicator_metro:start(0.25, 1)
end
local function start_lpg_status_timeout(status_text, x, y)
lpg_status.text = status_text
lpg_status.x, lpg_status.y = x, y
if lpg_status_metro.is_running then
lpg_status_metro:stop()
end
lpg_status_metro:start(1, 1)
end
local function update_tabs()
if tab_id == 1 then
wave_graph:set_active(true)
lpg_graph:set_active(true)
lfo_graph:set_active(true)
else
wave_graph:set_active(false)
lpg_graph:set_active(false)
lpg_status.text = ""
lfo_graph:set_active(false)
end
end
local function set_page(id)
page_id = util.clamp(id, 1, PAGES)
tab_id = 1
lpg_status.text = ""
update_tabs()
screen_dirty = true
end
local function set_page_delta(delta, loop)
local id
if loop then
id = page_id % PAGES + delta
else
id = page_id + delta
end
set_page(id)
end
local function set_tab(id)
tab_id = util.clamp(id, 1, 2)
update_tabs()
screen_dirty = true
end
local function set_tab_delta(delta, loop)
local id
if loop then
id = tab_id % 2 + delta
else
id = tab_id + delta
end
set_tab(id)
end
-- Engine functions
local function note_on(note_num, vel)
engine.noteOn(note_num, MusicUtil.note_num_to_freq(note_num), util.linlin(0, 127, 0, 1, vel))
end
local function note_off(note, vel)
engine.stop(note)
end
local function set_pitch_bend(bend_st)
engine.pitchBend(MusicUtil.interval_to_ratio(bend_st))
end
local function set_mod_wheel(value)
mod_wheel = value * 0.5
engine.waveFolds(params:get("wave_folds") + mod_wheel)
wave_folds.dirty = true
end
-- Updaters
local function update_wave_shape()
wave_shape.actual = util.clamp(params:get("wave_shape") + wave_shape.modu, Passersby.specs.WAVE_SHAPE.minval, Passersby.specs.WAVE_SHAPE.maxval)
wave_graph:update_functions()
wave_shape.dirty = false
screen_dirty = true
end
local function update_wave_folds()
wave_folds.actual = util.clamp(params:get("wave_folds") + mod_wheel + wave_folds.modu, Passersby.specs.WAVE_FOLDS.minval, Passersby.specs.WAVE_FOLDS.maxval)
wave_graph:update_functions()
wave_folds.dirty = false
screen_dirty = true
end
local function update_fm1_amount()
fm1_amount.actual = util.clamp(params:get("fm_low_amount") + fm1_amount.modu, Passersby.specs.FM_LOW_AMOUNT.minval, Passersby.specs.FM_LOW_AMOUNT.maxval)
fm1_amount.dirty = false
screen_dirty = true
end
local function update_fm2_amount()
fm2_amount.actual = util.clamp(params:get("fm_high_amount") + fm2_amount.modu, Passersby.specs.FM_HIGH_AMOUNT.minval, Passersby.specs.FM_HIGH_AMOUNT.maxval)
fm2_amount.dirty = false
screen_dirty = true
end
local function update_lpg_peak()
lpg_peak.actual = util.clamp(params:get("lpg_peak") * lpg_peak.modu, Passersby.specs.LPG_PEAK.minval, Passersby.specs.LPG_PEAK.maxval)
local norm_peak = util.explin(Passersby.specs.LPG_PEAK.minval * 0.5, Passersby.specs.LPG_PEAK.maxval, 0, 1, lpg_peak.actual)
lpg_graph:edit_ar(nil, nil, norm_peak)
lpg_peak.dirty = false
screen_dirty = true
end
local function show_lpg_peak_status()
local norm_peak = util.explin(Passersby.specs.LPG_PEAK.minval * 0.5, Passersby.specs.LPG_PEAK.maxval, 0, 1, params:get("lpg_peak") * lpg_peak.modu)
start_lpg_status_timeout(params:string("lpg_peak"), 57, util.linlin(0, 1, 56, 26, norm_peak))
screen_dirty = true
end
local function update_lpg_decay()
lpg_decay.actual = util.clamp(params:get("lpg_decay") + lpg_decay.modu, Passersby.specs.LPG_DECAY.minval, Passersby.specs.LPG_DECAY.maxval)
local norm_decay = util.explin(Passersby.specs.LPG_DECAY.minval * 0.5, Passersby.specs.LPG_DECAY.maxval, 0, 1, lpg_decay.actual)
lpg_graph:edit_ar(nil, norm_decay)
lpg_decay.dirty = false
screen_dirty = true
end
local function show_lpg_decay_status()
local norm_decay = util.explin(Passersby.specs.LPG_DECAY.minval * 0.5, Passersby.specs.LPG_DECAY.maxval, 0, 1, params:get("lpg_decay") + lpg_decay.modu)
start_lpg_status_timeout(params:string("lpg_decay"), util.linlin(0, 1, 36, 58, norm_decay), 48)
screen_dirty = true
end
local function update_reverb_mix()
reverb_mix.actual = util.clamp(params:get("reverb_mix") + reverb_mix.modu, Passersby.specs.REVERB_MIX.minval, Passersby.specs.REVERB_MIX.maxval)
reverb_mix.dirty = false
screen_dirty = true
end
local function update_lfo_freq()
lfo_graph:update_functions()
lfo_freq.dirty = false
screen_dirty = true
end
local function update_lfo_amount()
lfo_graph:update_functions()
lfo_amount.dirty = false
screen_dirty = true
end
local function update_lfo_destinations()
lfo_destinations.dirty = false
screen_dirty = true
end
local function update_drift()
drift.dirty = false
screen_dirty = true
end
-- Init
function init()
-- from earthsea
pat = pattern_time.new()
pat.process = grid_note_trans
if g then gridredraw() end
-- Add params
local channels = {"All"}
for i = 1, 16 do table.insert(channels, i) end
params:add_option("midi_channel", "MIDI Channel", channels)
params:add_separator()
Passersby.add_params()
-- Override param actions
params:set_action("wave_shape", function(value)
engine.waveShape(value)
wave_shape.dirty = true
end)
params:set_action("wave_folds", function(value)
engine.waveFolds(value + mod_wheel)
wave_folds.dirty = true
end)
params:set_action("fm_low_amount", function(value)
engine.fm1Amount(value)
fm1_amount.dirty = true
end)
params:set_action("fm_high_amount", function(value)
engine.fm2Amount(value)
fm2_amount.dirty = true
end)
params:set_action("lpg_peak", function(value)
engine.lpgPeak(value)
if page_id == 2 then show_lpg_peak_status() end
lpg_peak.dirty = true
end)
params:set_action("lpg_decay", function(value)
engine.lpgDecay(value)
if page_id == 2 then show_lpg_decay_status() end
lpg_decay.dirty = true
end)
params:set_action("reverb_mix", function(value)
engine.reverbMix(value)
reverb_mix.dirty = true
end)
params:set_action("lfo_frequency", function(value)
engine.lfoFreq(value)
lfo_freq.dirty = true
end)
params:set_action("lfo_amount", function(value)
engine.lfoAmount(value)
lfo_amount.dirty = true
end)
for i = 1, 2 do
params:set_action("lfo_destination_" .. i, function(value)
engine.lfoDest(i - 1, value - 1)
lfo_destinations.dirty = true
end)
end
params:set_action("drift", function(value)
engine.drift(value)
drift.dirty = true
end)
wave_shape.actual = params:get("wave_shape")
wave_folds.actual = params:get("wave_folds")
fm1_amount.actual = params:get("fm_low_amount")
fm2_amount.actual = params:get("fm_high_amount")
lpg_peak.actual = params:get("lpg_peak")
lpg_decay.actual = params:get("lpg_decay")
reverb_mix.actual = params:get("reverb_mix")
drift.actual = params:get("drift")
input_indicator_metro = metro.alloc()
input_indicator_metro.callback = function(stage)
input_indicator_active = false
screen_dirty = true
end
-- Init graphs
wave_graph = Graph.new(0, 2, "lin", -1, 1, "lin", nil, true, false)
wave_graph:set_position_and_size(8, 22, 49, 36)
wave_table = generate_wave_table(2, wave_graph:get_width() * SUB_SAMPLING)
local wave_func = function(x)
return generate_wave(util.linlin(0, 2, 1, wave_graph:get_width() * SUB_SAMPLING, x))
end
wave_graph:add_function(wave_func, SUB_SAMPLING)
lpg_graph = EnvGraph.new_ar(0, 1, 0, 1, 0.003, util.explin(Passersby.specs.LPG_DECAY.minval * 0.5, Passersby.specs.LPG_DECAY.maxval, 0, 1, lpg_decay.actual), util.explin(Passersby.specs.LPG_PEAK.minval * 0.5, Passersby.specs.LPG_PEAK.maxval, 0, 1, lpg_peak.actual), -4)
lpg_graph:set_position_and_size(8, 22, 49, 36)
lpg_graph:set_show_x_axis(true)
lfo_graph = Graph.new(0, 1, "lin", -1, 1, "lin", nil, true, false)
lfo_graph:set_position_and_size(8, 18, 49, 36)
lfo_graph:add_function(generate_lfo_wave, SUB_SAMPLING)
lpg_status.text = ""
lpg_status.x, lpg_status.y = 0, 0
lpg_status_metro = metro.alloc()
lpg_status_metro.callback = function(stage)
lpg_status.text = ""
screen_dirty = true
end
spring_path = generate_spring_path(10, 36, 5)
randomize_dice()
-- Init polls
local wave_shape_poll = poll.set("waveShapeMod", function(value)
if wave_shape.modu ~= value then
wave_shape.modu = value
wave_shape.dirty = true
end
end)
wave_shape_poll:start()
local wave_folds_poll = poll.set("waveFoldsMod", function(value)
if wave_folds.modu ~= value then
wave_folds.modu = value
wave_folds.dirty = true
end
end)
wave_folds_poll:start()
local fm1_amount_poll = poll.set("fm1AmountMod", function(value)
if fm1_amount.modu ~= value then
fm1_amount.modu = value
fm1_amount.dirty = true
end
end)
fm1_amount_poll:start()
local fm2_amount_poll = poll.set("fm2AmountMod", function(value)
if fm2_amount.modu ~= value then
fm2_amount.modu = value
fm2_amount.dirty = true
end
end)
fm2_amount_poll:start()
local lpg_peak_poll = poll.set("lpgPeakMulMod", function(value)
if lpg_peak.modu ~= value then
lpg_peak.modu = value
lpg_peak.dirty = true
end
end)
lpg_peak_poll:start()
local lpg_decay_poll = poll.set("lpgDecayMod", function(value)
if lpg_decay.modu ~= value then
lpg_decay.modu = value
lpg_decay.dirty = true
end
end)
lpg_decay_poll:start()
local reverb_mix_poll = poll.set("reverbMixMod", function(value)
if reverb_mix.modu ~= value then
reverb_mix.modu = value
reverb_mix.dirty = true
end
end)
reverb_mix_poll:start()
-- Start drawing to screen
screen_refresh_metro = metro.alloc()
screen_refresh_metro.callback = function(stage)
update()
if screen_dirty then
screen_dirty = false
redraw()
end
end
screen_refresh_metro:start(1 / SCREEN_FRAMERATE)
end
-- Input functions
-- Encoder input
function enc(n, delta)
if n == 1 then
-- Page scroll
set_page_delta(util.clamp(delta, -1, 1), false)
end
if page_id == 1 then
if tab_id == 1 then
-- Wave
if n == 2 then
params:delta("wave_shape", delta)
elseif n == 3 then
params:delta("wave_folds", delta)
end
else
-- FM
if n == 2 then
params:delta("fm_low_amount", delta)
elseif n == 3 then
params:delta("fm_high_amount", delta)
end
end
elseif page_id == 2 then
if tab_id == 1 then
-- LPG
if n == 2 then
params:delta("lpg_peak", delta)
elseif n == 3 then
params:delta("lpg_decay", delta)
end
else
-- Reverb
if n == 2 then
params:delta("reverb_mix", delta)
end
end
elseif page_id == 3 then
if tab_id == 1 then
-- LFO
if n == 2 then
params:delta("lfo_frequency", delta)
elseif n == 3 then
params:delta("lfo_amount", delta)
end
else
-- Destinations
if n == 2 then
params:delta("lfo_destination_" .. 1, util.clamp(delta, -1, 1))
elseif n == 3 then
params:delta("lfo_destination_" .. 2, util.clamp(delta, -1, 1))
end
end
elseif page_id == 4 then
-- Randomize
if n == 2 then
if not dice_thrown then set_dice_throw_vel(delta * 0.05) end
-- Drift
elseif n == 3 then
params:delta("drift", delta)
end
end
end
-- Key input
function key(n, z)
if z == 1 then
if n == 2 then
set_page_delta(1, true)
elseif n == 3 then
set_tab_delta(1, true)
end
end
end
-- MIDI input
local function midi_event(data)
if #data == 0 then return end
local midi_status = data[1]
local data1 = data[2]
local data2 = data[3]
-- Note on
if (params:get("midi_channel") == 1 and midi_status >= 144 and midi_status <= 159 ) or (params:get("midi_channel") > 1 and midi_status == 144 + params:get("midi_channel") - 2) then
note_on(data1, data2)
start_input_indicator_timeout()
-- Note off
-- elseif (params:get("MIDI Channel") == 1 and midi_status >= 128 and midi_status <= 143 ) or (params:get("MIDI Channel") > 1 and midi_status == 128 + params:get("MIDI Channel") - 2) then
-- CC
elseif (params:get("midi_channel") == 1 and midi_status >= 176 and midi_status <= 191 ) or (params:get("midi_channel") > 1 and midi_status == 176 + params:get("midi_channel") - 2) then
-- Mod wheel
if data1 == 1 then
set_mod_wheel(util.linlin(0, 127, 0, 1, data2))
end
-- Pitch bend
elseif (params:get("midi_channel") == 1 and midi_status >= 224 and midi_status <= 239 ) or (params:get("midi_channel") > 1 and midi_status == 224 + params:get("midi_channel") - 2) then
local bend_14bit = bit32.bor(bit32.lshift(data2, 7), data1)
local bend_st = (util.round(bend_14bit / 2)) / 8192 * 2 -1 -- Convert to -1 to 1
set_pitch_bend(bend_st * 2) -- 2 Semitones of bend
end
end
midi.add = function(dev)
dev.event = midi_event
end
function cleanup()
-- from earthsea
pat:stop()
pat = nil
-- from earthsea end
for id, dev in pairs(midi.devices) do
dev.event = nil
end
end
-- Draw functions
local function rotate(x, y, center_x, center_y, angle_rads)
local sin_a = math.sin(angle_rads)
local cos_a = math.cos(angle_rads)
x = x - center_x
y = y - center_y
return (x * cos_a - y * sin_a) + center_x, (x * sin_a + y * cos_a) + center_y
end
local function draw_page_dots(index, total_pages)
local dots_y = util.round((64 - total_pages * 4 - (total_pages - 1) * 2) * 0.5)
for i = 1, total_pages do
if i == index then screen.level(5)
else screen.level(1) end
screen.rect(127, dots_y, 1, 4)
screen.fill()
dots_y = dots_y + 6
end
end
local function draw_input_indicator()
screen.level(4)
screen.move(0, 1)
screen.line(5, 1)
screen.line(2.5, 4)
screen.close()
screen.fill()
end
local function draw_background_rects()
screen.level(1)
screen.rect(8, 18, 49, 44)
screen.fill()
screen.rect(71, 18, 49, 44)
screen.fill()
end
local function draw_tabs(titles_array, active)
local margin = 8
local gutter = 14
local col_width = (128 - (margin * 2) - gutter * (#titles_array - 1)) / #titles_array
for i = 1, #titles_array do
if i == active then screen.level(15)
else screen.level(3) end
screen.move(margin + col_width * 0.5 + ((col_width + gutter) * (i - 1)), 6)
screen.text_center(titles_array[i])
end
end
local function draw_dial(value, x, y, size, active)
local radius = size * 0.5
local start_angle = math.pi * 0.7
local end_angle = math.pi * 2.3
screen.level(5)
screen.arc(x + radius, y + radius, radius - 0.5, util.linlin(0, 1, start_angle, end_angle, value), end_angle)
screen.stroke()
screen.level(15)
screen.line_width(2.5)
screen.arc(x + radius, y + radius, radius - 0.5, start_angle, util.linlin(0, 1, start_angle, end_angle, value))
screen.stroke()
screen.line_width(1)
if active then screen.level(15) else screen.level(3) end
screen.move(x + radius, y + size + 6)
screen.text_center(util.round(value * 100, 1))
screen.fill()
end
local function draw_slider(value, x, y, width, height, active)
screen.level(3)
screen.rect(x + 0.5, y + 0.5, width - 1, height - 1)
screen.stroke()
local filled_height = util.round(util.linlin(0, 1, 0, height, value))
screen.rect(x, y + height - filled_height, width, filled_height)
if active then screen.level(15) else screen.level(5) end
screen.fill()
end
local function draw_spring(x, y, active)
screen.move(x + spring_path[1].x + 0.5, y + spring_path[1].y + 0.5)
for i = 2, #spring_path do
screen.line(x + spring_path[i].x + 0.5, y + spring_path[i].y + 0.5)
end
if active then
screen.level(15)
screen.line_width(0.7)
else
screen.level(5)
end
screen.stroke()
screen.line_width(1)
end
local function draw_die(x, y, rotation_rads, face)
screen.level(15)
local size = 9
screen.move(rotate(x - size + 0.5, y - size + 0.5, x, y, rotation_rads))
screen.line(rotate(x + size - 0.5, y - size + 0.5, x, y, rotation_rads))
screen.line(rotate(x + size - 0.5, y + size - 0.5, x, y, rotation_rads))
screen.line(rotate(x - size + 0.5, y + size - 0.5, x, y, rotation_rads))
screen.close()
screen.stroke()
local dot_size = 1
local dx, dy
if face == 1 then
dot_size = 1.5
dx, dy = rotate(x, y, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
elseif face == 2 then
dx, dy = rotate(x + 3, y - 3, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
dx, dy = rotate(x - 3, y + 3, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
elseif face == 3 then
dx, dy = rotate(x + 4, y - 4, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
dx, dy = rotate(x, y, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
dx, dy = rotate(x - 4, y + 4, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
elseif face == 4 then
dx, dy = rotate(x - 3, y - 3, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
dx, dy = rotate(x + 3, y - 3, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
dx, dy = rotate(x + 3, y + 3, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
dx, dy = rotate(x - 3, y + 3, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
elseif face == 5 then
dx, dy = rotate(x, y, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
dx, dy = rotate(x - 4, y - 4, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
dx, dy = rotate(x + 4, y - 4, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
dx, dy = rotate(x + 4, y + 4, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
dx, dy = rotate(x - 4, y + 4, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
elseif face == 6 then
dx, dy = rotate(x - 3, y - 4, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
dx, dy = rotate(x + 3, y - 4, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
dx, dy = rotate(x - 3, y, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
dx, dy = rotate(x + 3, y, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
dx, dy = rotate(x - 3, y + 4, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
dx, dy = rotate(x + 3, y + 4, x, y, rotation_rads)
screen.circle(dx, dy, dot_size)
screen.fill()
end
end
local function draw_dice()
draw_die(20, util.linlin(0, 1, 32, 17, dice_throw_progress), util.linlin(0, 1, dice[1].table_angle, dice[1].top_angle, dice_throw_progress), dice[1].face)
draw_die(45, util.linlin(0, 1, 46, 61, dice_throw_progress), util.linlin(0, 1, dice[2].table_angle, dice[2].top_angle, dice_throw_progress), dice[2].face)
end
local function update_dice()
if not dice_need_update then return end
if dice_thrown then
if dice_throw_progress < 0.05 then
for i = 1, 2 do
local direction = (dice[i].table_angle > 0) and 1 or -1
dice[i].table_angle = dice[i].table_angle - (2 * math.pi * direction)
end
dice_thrown = false
end
set_dice_throw_vel(-0.08)
else
if dice_throw_progress > 0.9 then
dice_thrown = true
randomize_dice()
Passersby.randomize_params()
set_dice_throw_vel(-0.3)
else
set_dice_throw_vel(util.linlin(0, 1, -0.02, -0.03, dice_throw_progress))
end
end
if dice_throw_progress <= 0 then
dice_throw_vel = math.max(dice_throw_vel, 0)
end
dice_throw_progress = util.clamp(dice_throw_progress + dice_throw_vel, 0, 1)
if page_id == 4 then screen_dirty = true end
if dice_throw_progress == 0 and not dice_thrown then dice_need_update = false end
end
function update()
if page_id == 1 then
if wave_shape.dirty then update_wave_shape() end
if wave_folds.dirty then update_wave_folds() end
if fm1_amount.dirty then update_fm1_amount() end
if fm2_amount.dirty then update_fm2_amount() end
elseif page_id == 2 then
if lpg_peak.dirty then update_lpg_peak() end
if lpg_decay.dirty then update_lpg_decay() end
if reverb_mix.dirty then update_reverb_mix() end
elseif page_id == 3 then
if lfo_freq.dirty then update_lfo_freq() end
if lfo_amount.dirty then update_lfo_amount() end
if lfo_destinations.dirty then update_lfo_destinations() end
elseif page_id == 4 then
if drift.dirty then update_drift() end
end
update_dice()
end
function redraw()
screen.clear()
screen.aa(1)
draw_page_dots(page_id, PAGES)
if input_indicator_active then draw_input_indicator() end
-- draw_background_rects()
if page_id == 1 then
draw_tabs({"Wave", "FM"}, tab_id)
-- Wave
wave_graph:redraw()
-- FM
screen.level(3)
screen.move(83, 33)
screen.text_center("L")
screen.move(108, 48)
screen.text_center("H")
screen.fill() -- Prevents extra line
draw_dial(fm1_amount.actual, 72, 19, 22, tab_id == 2)
draw_dial(fm2_amount.actual, 97, 34, 22, tab_id == 2)
elseif page_id == 2 then
draw_tabs({"LPG", "Reverb"}, tab_id)
-- LPG
lpg_graph:redraw()
screen.level(3)
screen.move(lpg_status.x, lpg_status.y)
screen.text_right(lpg_status.text)
-- Reverb
screen.fill()
draw_spring(82, 22, tab_id == 2)
screen.rect(100, 40, 7, 1)
screen.level(3)
screen.fill()
draw_slider(reverb_mix.actual, 102, 22, 3, 36, true)
elseif page_id == 3 then
draw_tabs({"LFO", "Targets"}, tab_id)
-- LFO
lfo_graph:redraw()
screen.level(3)
screen.move(8, 62)
screen.text(params:string("lfo_frequency"))
-- LFO Targets
if tab_id == 2 then screen.level(15) else screen.level(3) end
screen.move(71, 33)
screen.text(Passersby.LFO_DESTINATIONS[params:get("lfo_destination_1")])
screen.move(71, 49)
screen.text(Passersby.LFO_DESTINATIONS[params:get("lfo_destination_2")])
elseif page_id == 4 then
draw_tabs({"Fate"}, 1)
-- Dice
draw_dice()
-- Drift
screen.move(96, 23)
screen.text_center("Drift")
screen.fill()
draw_dial(params:get("drift"), 85, 28, 22, true)
end
screen.update()
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment