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