Skip to content

Instantly share code, notes, and snippets.

@j-flee
Last active October 13, 2018 07:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save j-flee/586af2dde6307c0a3c90e2815fdd20d6 to your computer and use it in GitHub Desktop.
Save j-flee/586af2dde6307c0a3c90e2815fdd20d6 to your computer and use it in GitHub Desktop.
Earthsea + Molly the Poly
-- Molly the Poly
--
-- MIDI / grid controlled classic
-- polysynth with patch creator.
--
-- ENC1 : Choose a patch planet
-- ENC2 : Create
--
-- grid pattern player:
-- 1 1 record toggle
-- 1 2 play toggle
-- 1 4-7 octave shift (-1 to +2)
-- 1 8 transpose mode
--
-- v1.0.0 Mark Eats
-- Earthsea grid pattern added:
-- Johnathan F. Lee
local MusicUtil = require "mark_eats/musicutil"
local MollyThePoly = require "mark_eats/mollythepoly"
-- 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 = 8
-- current count of active voices
local nvoices = 0
-- from earthsea end
engine.name = "MollyThePoly"
local SCREEN_FRAMERATE = 15
local screen_dirty = true
local midi_in_device
local SUN_BASE_RADIUS = 5
local sun_mod_radius = 0
local sun_cool_down = false
local explosions = {}
local PLANET_RADIUS = 2.8
local planets = {{name = "Lead"}, {name = "Pad"}, {name = "Perc"}}
local selected_planet = 1
local selected_planet_id = 1
local stars = {}
local function add_star(id)
local size = math.random(1, 3)
local star = {x = math.random(2, 126), y = math.random(2, 62), width = math.max(3, size * 2 - 1), height = 1 + size * 2}
local distance_from_sun = math.sqrt(math.pow(math.abs(star.x - 64), 2) + math.pow(math.abs(star.y - 32), 2))
if distance_from_sun < 35 then
if star.x > 64 then
star.x = star.x + 35
else
star.x = star.x - 35
end
star.x = util.clamp(star.x, 5, 123)
end
stars[id] = star
end
local function remove_star(id)
stars[id] = nil
end
local function remove_all_stars()
stars = {}
end
local function add_explosion(planet_id, radius)
table.insert(explosions, {planet_id = planet_id, x = 64, y = 32, radius = radius, velocity = 2, life = 0.66})
end
local function randomize()
MollyThePoly.randomize_params(planets[selected_planet_id].name:lower())
add_explosion(nil, SUN_BASE_RADIUS + sun_mod_radius)
add_explosion(selected_planet_id, PLANET_RADIUS)
end
local function note_on(note_num, vel)
engine.noteOn(note_num, MusicUtil.note_num_to_freq(note_num), vel)
add_star(note_num)
end
local function note_off(note_num)
engine.noteOff(note_num)
remove_star(note_num)
end
local function note_off_all()
engine.noteOffAll()
remove_all_stars()
end
local function note_kill_all()
engine.noteKillAll()
remove_all_stars()
end
local function set_key_pressure(note_num, pressure)
engine.pressure(note_num, pressure)
end
local function set_channel_pressure(pressure)
engine.pressureAll(pressure)
end
local function set_pitch_bend(bend_st)
engine.pitchBendAll(MusicUtil.interval_to_ratio(bend_st))
end
-- Encoder input
function enc(n, delta)
if n == 2 then
selected_planet = util.clamp(selected_planet + util.clamp(-1, 1, delta) * 0.1, 1, #planets)
selected_planet_id = util.round(selected_planet)
elseif n == 3 then
if not sun_cool_down then
sun_mod_radius = util.clamp(sun_mod_radius + delta * util.linlin(0, 32, 0.3, 0.8, sun_mod_radius), SUN_BASE_RADIUS * - 0.5, planets[selected_planet_id].orbit + 2)
end
end
end
-- Key input
function key(n, z)
if z == 1 then
if n == 2 then
elseif n == 3 then
end
end
end
-- 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()
engine.noteKillAll()
-- stop_all_screen_notes()
remove_all_stars()
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()
engine.noteKillAll()
-- stop_all_screen_notes()
remove_all_stars()
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)
add_star(e.id)
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.noteOff(e.id)
-- engine.stop(e.id)
-- stop_screen_note(note)
remove_star(e.id)
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)
add_star(e.id)
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)
engine.noteOff(e.id)
remove_star(e.id)
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)
g.led(1,6,3)
g.led(1,6-range,7)
end
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
-- MIDI input
local function midi_event(data)
if #data == 0 then return end
local msg = midi.to_msg(data)
local channel_param = params:get("midi_channel")
if channel_param == 1 or (channel_param > 1 and msg.ch == channel_param - 1) then
-- Note off
if msg.type == "note_off" then
note_off(msg.note)
-- Note on
elseif msg.type == "note_on" then
note_on(msg.note, msg.vel / 127)
-- Key pressure
elseif msg.type == "key_pressure" then
set_key_pressure(msg.note, msg.val / 127)
-- Channel pressure
elseif msg.type == "channel_pressure" then
set_channel_pressure(msg.val / 127)
-- Pitch bend
elseif msg.type == "pitchbend" then
local bend_st = (util.round(msg.val / 2)) / 8192 * 2 -1 -- Convert to -1 to 1
set_pitch_bend(bend_st * 2) -- 2 Semitones of bend
end
end
end
local function solar_system_update()
if not sun_cool_down and SUN_BASE_RADIUS + sun_mod_radius > planets[selected_planet_id].orbit + 1 then
randomize()
sun_cool_down = true
else
if sun_cool_down then
sun_mod_radius = sun_mod_radius * 0.5
elseif sun_mod_radius > 0 then
sun_mod_radius = sun_mod_radius * 0.85
elseif sun_mod_radius < 0 then
sun_mod_radius = sun_mod_radius + 0.15
end
if sun_mod_radius < 1 then sun_cool_down = false end
end
for i = 1, #planets do
planets[i].position = (planets[i].position + planets[i].velocity) % (math.pi * 2)
planets[i].x = 64 + planets[i].orbit * math.cos(planets[i].position)
planets[i].y = 32 + planets[i].orbit * math.sin(planets[i].position)
end
for i = #explosions, 1, -1 do
if explosions[i].planet_id then
explosions[i].x = planets[explosions[i].planet_id].x
explosions[i].y = planets[explosions[i].planet_id].y
end
explosions[i].radius = explosions[i].radius + explosions[i].velocity
explosions[i].velocity = explosions[i].velocity * 0.93
explosions[i].life = explosions[i].life - 1 / SCREEN_FRAMERATE
if explosions[i].life <= 0 then
table.remove(explosions, i)
end
end
screen_dirty = true
end
function init()
-- from earthsea
pat = pattern_time.new()
pat.process = grid_note_trans
if g then gridredraw() end
midi_in_device = midi.connect(1)
midi_in_device.event = midi_event
-- Add params
params:add{type = "number", id = "midi_device", name = "MIDI Device", min = 1, max = 4, default = 1, action = function(value)
midi_in_device:reconnect(value)
end}
local channels = {"All"}
for i = 1, 16 do table.insert(channels, i) end
params:add{type = "option", id = "midi_channel", name = "MIDI Channel", options = channels}
params:add_separator()
MollyThePoly.add_params()
local orbit = 13.5
for i = 1, #planets do
planets[i].orbit = orbit
planets[i].position = math.random() * math.pi * 2
planets[i].velocity = util.linlin(0, 1, 0.01, 0.03, math.random())
orbit = orbit + 8
end
local screen_refresh_metro = metro.alloc()
screen_refresh_metro.callback = function()
solar_system_update()
if screen_dirty then
screen_dirty = false
redraw()
end
end
solar_system_update()
screen_refresh_metro:start(1 / SCREEN_FRAMERATE)
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
local function dashed_circle(x, y, radius, dash_length, gap_length)
local circum = 2 * math.pi * radius
local segments = util.round(circum / (dash_length + gap_length))
local segment_angle = math.pi * 2 / segments
local dash_angle = segment_angle * (dash_length / (dash_length + gap_length))
local start_angle = 0
while start_angle < math.pi * 2 do
screen.arc(64, 32, radius, start_angle, start_angle + dash_angle)
screen.stroke()
start_angle = start_angle + segment_angle
end
end
function redraw()
screen.clear()
screen.aa(1)
-- Explosions
for i = 1, #explosions do
screen.level(util.round(util.linexp(0, 1, 2, 15, explosions[i].life)))
screen.circle(explosions[i].x, explosions[i].y, explosions[i].radius)
screen.stroke()
end
-- Stars
screen.level(3)
for _, star in pairs(stars) do
screen.rect(star.x - math.floor(star.width * 0.5), star.y, star.width, 1)
screen.rect(star.x, star.y - math.floor(star.height * 0.5), 1, star.height)
end
screen.fill()
-- Planets
for i = 1, #planets do
if i == selected_planet_id then
-- Path
screen.line_width(0.7)
screen.level(5)
screen.circle(64, 32, planets[i].orbit)
screen.stroke()
-- Planet outline
screen.level(15)
screen.circle(planets[i].x, planets[i].y, 5.5)
screen.line_width(0.7)
screen.stroke()
else
-- Path
screen.line_width(1)
screen.level(3)
dashed_circle(64, 32, planets[i].orbit, 3, 3)
screen.level(4)
end
-- Planet
screen.circle(planets[i].x, planets[i].y, PLANET_RADIUS)
screen.fill()
end
screen.line_width(1)
-- Sun
screen.circle(64, 32, SUN_BASE_RADIUS + sun_mod_radius)
screen.level(15)
screen.fill()
-- Label
screen.move(3, 58)
screen.level(15)
screen.text(planets[selected_planet_id].name)
screen.fill()
screen.update()
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment