Last active
October 13, 2018 07:27
-
-
Save j-flee/845b3fbf7301020158f02c830b7889b3 to your computer and use it in GitHub Desktop.
Earthsea grid pattern + KarplusRings engine
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
-- earth + rings | |
-- | |
-- karplus-strong synth | |
-- controlled by midi or grid | |
-- | |
-- grid pattern player: | |
-- 1 1 record toggle | |
-- 1 2 play toggle | |
-- 1 4-7 octave shift (-1 to +2) | |
-- 1 8 transpose mode | |
-- | |
-- created by @burn | |
-- | |
-- Earthsea grid pattern added: | |
-- Johnathan F. Lee | |
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 screen_framerate = 15 | |
local screen_refresh_metro | |
local ripple_repeat_rate = 1 / 0.3 / screen_framerate | |
local ripple_decay_rate = 1 / 0.5 / screen_framerate | |
local ripple_growth_rate = 1 / 0.02 / screen_framerate | |
local screen_notes = {} | |
local MAX_NUM_VOICES = 16 | |
engine.name = 'KarplusRings' | |
local cs = require 'controlspec' | |
-- pythagorean minor/major, kinda | |
local ratios = { 1, 9/8, 6/5, 5/4, 4/3, 3/2, 27/16, 16/9 } | |
local base = 27.5 -- low A | |
local function getHz(deg,oct) | |
return base * ratios[deg] * (2^oct) | |
end | |
local function getHzET(note) | |
return 55*2^(note/12) | |
end | |
-- current count of active voices | |
local nvoices = 0 | |
function init() | |
pat = pattern_time.new() | |
pat.process = grid_note_trans | |
cs.AMP = cs.new(0,1,'lin',0,0.75,'') | |
params:add_control("amp", "amp", cs.AMP) | |
params:set_action("amp", | |
function(x) engine.amp(x) end) | |
engine.amp(0.75) | |
cs.DECAY = cs.new(0.1,5,'lin',0,3.6,'s') | |
params:add_control("damping", "damping", cs.DECAY) | |
params:set_action("damping", | |
function(x) engine.decay(x) end) | |
cs.COEF = cs.new(0,1,'lin',0,0.11,'') | |
params:add_control("brightness", "brightness", cs.COEF) | |
params:set_action("brightness", | |
function(x) engine.coef(x) end) | |
cs.LPF_FREQ = cs.new(100,10000,'lin',0,3600,'') | |
params:add_control("lpf_freq", "lpf_freq", cs.LPF_FREQ) | |
params:set_action("lpf_freq", | |
function(x) engine.lpf_freq(x) end) | |
engine.lpf_freq(3600.0) | |
cs.LPF_GAIN = cs.new(0,3.2,'lin',0,0.5,'') | |
params:add_control("lpf_gain", "lpf_gain", cs.LPF_GAIN) | |
params:set_action("lpf_gain", | |
function(x) engine.lpf_gain(x) end) | |
cs.BPF_FREQ = cs.new(100,9000,'lin',0,1200,'') | |
params:add_control("bpf_freq", "bpf_freq", cs.BPF_FREQ) | |
params:set_action("bpf_freq", | |
function(x) engine.bpf_freq(x) end) | |
engine.bpf_freq(1200.0) | |
cs.BPF_RES = cs.new(0.05,2.5,'lin',0,0.5,'') | |
params:add_control("bpf_res", "bpf_res", cs.BPF_RES) | |
params:set_action("bpf_res", | |
function(x) engine.bpf_res(x) end) | |
-- engine.level(0.05) | |
-- engine.stopAll() | |
stop_all_screen_notes() | |
-- params:read("tehn/earthsea.pset") | |
params:bang() | |
if g then gridredraw() end | |
screen_refresh_metro = metro.alloc() | |
screen_refresh_metro.callback = function(stage) | |
update() | |
redraw() | |
end | |
screen_refresh_metro:start(1 / screen_framerate) | |
local startup_ani_count = 1 | |
local startup_ani_metro = metro.alloc() | |
startup_ani_metro.callback = function(stage) | |
start_screen_note(-startup_ani_count) | |
stop_screen_note(-startup_ani_count) | |
startup_ani_count = startup_ani_count + 1 | |
end | |
startup_ani_metro:start( 0.1, 3 ) | |
end | |
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 = (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.hz(getHzET(note)) | |
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 = (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.hz(getHzET(note)) | |
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 | |
function enc(n,delta) | |
if n == 1 then | |
mix:delta("output", delta) | |
end | |
end | |
function key(n,z) | |
end | |
function start_screen_note(note) | |
local screen_note = nil | |
-- Get an existing screen_note if it exists | |
local count = 0 | |
for key, val in pairs(screen_notes) do | |
if val.note == note then | |
screen_note = val | |
break | |
end | |
count = count + 1 | |
if count > 8 then return end | |
end | |
if screen_note then | |
screen_note.active = true | |
else | |
screen_note = {note = note, active = true, repeat_timer = 0, x = math.random(128), y = math.random(64), init_radius = math.random(6,18), ripples = {} } | |
table.insert(screen_notes, screen_note) | |
end | |
add_ripple(screen_note) | |
end | |
function stop_screen_note(note) | |
for key, val in pairs(screen_notes) do | |
if val.note == note then | |
val.active = false | |
break | |
end | |
end | |
end | |
function stop_all_screen_notes() | |
for key, val in pairs(screen_notes) do | |
val.active = false | |
end | |
end | |
function add_ripple(screen_note) | |
if tab.count(screen_note.ripples) < 6 then | |
local ripple = {radius = screen_note.init_radius, life = 1} | |
table.insert(screen_note.ripples, ripple) | |
end | |
end | |
function update() | |
for n_key, n_val in pairs(screen_notes) do | |
if n_val.active then | |
n_val.repeat_timer = n_val.repeat_timer + ripple_repeat_rate | |
if n_val.repeat_timer >= 1 then | |
add_ripple(n_val) | |
n_val.repeat_timer = 0 | |
end | |
end | |
local r_count = 0 | |
for r_key, r_val in pairs(n_val.ripples) do | |
r_val.radius = r_val.radius + ripple_growth_rate | |
r_val.life = r_val.life - ripple_decay_rate | |
if r_val.life <= 0 then | |
n_val.ripples[r_key] = nil | |
else | |
r_count = r_count + 1 | |
end | |
end | |
if r_count == 0 and not n_val.active then | |
screen_notes[n_key] = nil | |
end | |
end | |
end | |
function redraw() | |
screen.clear() | |
screen.aa(0) | |
screen.line_width(1) | |
local first_ripple = true | |
for n_key, n_val in pairs(screen_notes) do | |
for r_key, r_val in pairs(n_val.ripples) do | |
if first_ripple then -- Avoid extra line when returning from menu | |
screen.move(n_val.x + r_val.radius, n_val.y) | |
first_ripple = false | |
end | |
screen.level(math.max(1,math.floor(r_val.life * 15 + 0.5))) | |
screen.circle(n_val.x, n_val.y, r_val.radius) | |
screen.stroke() | |
end | |
end | |
screen.update() | |
end | |
local function note_on(note, vel) | |
if nvoices < MAX_NUM_VOICES then | |
--engine.start(id, getHz(x, y-1)) | |
--engine.start(e.id, getHzET(note)) | |
engine.hz(getHzET(note)) | |
start_screen_note(note) | |
nvoices = nvoices + 1 | |
end | |
end | |
local function note_off(note, vel) | |
engine.stop(note) | |
stop_screen_note(note) | |
nvoices = nvoices - 1 | |
end | |
local function midi_event(data) | |
if data[1] == 144 then | |
if data[3] == 0 then | |
note_off(data[2]) | |
else | |
note_on(data[2], data[3]) | |
end | |
elseif data[1] == 128 then | |
note_off(data[2]) | |
elseif data[1] == 176 then | |
--cc(data1, data2) | |
elseif data[1] == 224 then | |
--bend(data1, data2) | |
end | |
end | |
midi.add = function(dev) | |
print('earthsea: midi device added', dev.id, dev.name) | |
dev.event = midi_event | |
end | |
function cleanup() | |
stop_all_screen_notes() | |
pat:stop() | |
pat = nil | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment