Skip to content

Instantly share code, notes, and snippets.

@j-flee
Last active October 13, 2018 07:27
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/845b3fbf7301020158f02c830b7889b3 to your computer and use it in GitHub Desktop.
Save j-flee/845b3fbf7301020158f02c830b7889b3 to your computer and use it in GitHub Desktop.
Earthsea grid pattern + KarplusRings engine
-- 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