-- interval practice -- -- hear two notes; -- play two notes -- -- key2: repeat -- key3: skip -- -- to do: -- 1) select base tone and a range -- 2) key signatures -- 3) listen to input (tuner-style) -- 4) keep a score -- 5) spaced repetition? local ControlSpec = require "controlspec" local MusicUtil = require "musicutil" -- graphics and audio utilities function interval_name(base, interval) t = { "unison", "minor second", "major second", "minor third", "major third", "perfect fourth", "diminished fifth", "perfect fifth", "minor sixth", "major sixth", "minor seventh", "major seventh", "octave" } return t[interval - base + 1] end function blit(x, y, bitmap, p) for i=1,#bitmap do for j=1,#bitmap[i] do local level = bitmap[i][j] if p and p.brightness then level = level * p.brightness else level = level * 10 end if level > 0 then screen.level(math.floor(level)) screen.pixel(x + j, y + i) screen.fill() end end end end function blit_center(x, y, sprite, p) local offset_before = sprite.offset_before or #sprite.data[1] / 2 local offset_above = sprite.offset_above or #sprite.data / 2 blit(x - offset_before, y - offset_above, sprite.data, p) end local quarter_sprite = { data={ { 0, 0, 0, 0, 1, 0, 0}, { 0, 0, 0, 0, 1, 0, 0}, { 0, 0, 0, 0, 1, 0, 0}, { 0, 0, 0, 0, 1, 0, 0}, { 0, 0, 0, 0, 1, 0, 0}, { 0, 0, 0, 0, 1, 0, 0}, { 0, 1, 1, 1, 1, 0, 0}, { 1, 1, 1, 1, 1, 0, 0}, { 0, 1, 1, 1, 0, 0, 0}, }, -- metadata to center properly offset_before=2, offset_above=8 } local sharp_sprite = { data = { { 0, 1, 0, 1, 0 }, { 1, 1, 1, 1, 1 }, { 0, 1, 0, 1, 0 }, { 1, 1, 1, 1, 1 }, { 0, 1, 0, 1, 0 } } } local flat_sprite = { data = { { 1, 0, 0, 0}, { 1, 0, 0, 0}, { 1, 1, 1, 0}, { 1, 0, 1, 0}, { 1, 1, 0, 0} } } -- parameters: -- p.y: offset from the top -- p.margin: distance from the side (since it's all centered) -- p.notes: all quarter notes for now function staff (p) local x_start = p.margin; local x_end = 128 - p.margin local width = 128 - p.margin * 2 screen.level(1) for i=0,4 do for j=x_start,x_end do screen.pixel(j, p.y + 5 * i) end end screen.fill() for i=1,#p.notes do -- adjust for lines drawn local bottom_y = p.y + 20 -- offset for this particular note local y = math.ceil(bottom_y - 2.5 * p.notes[i].place) -- evenly distribute along the x-axis local total_x = 128 - p.margin * 2 local each_x = total_x / #p.notes local x = x_start + each_x * (i - 1) + each_x / 2 -- draw the broken staff line, if it's out of range if y > bottom_y or y < p.y then screen.level(1) -- todo p.brightness screen.move(x - quarter_sprite.offset_before - 1, y + 1) screen.line(x + #quarter_sprite.data[1] - 1, y + 1) screen.stroke() end if p.notes[i].accidental > 0 then blit_center(x - 6, y, sharp_sprite, p.notes[i]) elseif p.notes[i].accidental < 0 then blit_center(x - 6, y, flat_sprite, p.notes[i]) end -- draw the note blit_center(x, y, quarter_sprite, p.notes[i]) end end -- todo could probably do this more nicely function c_major_treble(midi_note) local scale = { -- c / c# [60]={place=-2, accidental=0}, [61]={place=-2, accidental=1}, -- d / d# [62]={place=-1, accidental=0}, [63]={place=-1, accidental=1}, -- e [64]={place=0, accidental=0}, -- f / f# [65]={place=1, accidental=0}, [66]={place=1, accidental=1}, -- g / g# [67]={place=2, accidental=0}, [68]={place=2, accidental=1}, -- a / a# [69]={place=3, accidental=0}, [70]={place=3, accidental=1}, -- b [71]={place=4, accidental=0}, -- c [72]={place=5, accidental=0}, } return scale[midi_note] end -- little mode library... -- a mode is a thing with fields for enc, redraw, and key -- it can edit the state function state_call(state, name, args) if state.stack[#state.stack] then local fn = state.stack[#state.stack][name] if fn then local ret = fn(table.unpack(args or {})) if ret then ret(state) end end else print("empty state?") end end function state_init(state) -- destroy old timer, create a new one. -- we need to do this because e.g. if we transition -- to the next screen halfway between 1 and 2, then we'll -- pause for half a second less than we ought to. if state.timer then state.timer:stop() metro.free(state.timer.id) end state.timer = metro.init() state.timer.time = state.tick_interval or 1.0 state.timer.event = function(t) state_call(state, "tick", {t}) end state_call(state, "init") state.timer:start() end function state_redraw(state) -- we need to call redraw() here instead of trying to -- do anything, otherwise it'll draw over the settings -- menus. redraw() end function pop(state) table.remove(state.stack) state_init(state) end function pop_n(n) return function (state) if state.timer then state.timer:stop() end for i=1,n,1 do table.remove(state.stack) end state_init(state) end end function replace(modes) return function (state) top = table.remove(state.stack) push(modes)(state) end end function push(modes) return function (state) for i=1,#modes do table.insert(state.stack, modes[i]) end state_init(state) end end -- end mode library function message (msgs, tick_fn, midi_fn) return { init = function () return state_redraw end, redraw=function () screen.clear() local total_height = 10 * #msgs local y = (64 - total_height) / 2 local x = 64 for i=1,#msgs do screen.move(x, y + 10 * (i - 1) + 5) screen.text_center(msgs[i]) end screen.update() end, key=function (n, x) if n == 3 and x == 0 then return pop end end, tick=function(t) if tick_fn then return tick_fn(t) end end, midi=function(ev) if midi_fn then return midi_fn(ev) end end } end function correct_guess(known, guess) return { init=function () return state_redraw end, redraw=function() local known_note = c_major_treble(known) known_note.brightness = 10 local guess_note = c_major_treble(guess) guess_note.brightness = 10 staff({y=20, margin=5, notes={ known_note, guess_note }}) screen.move(64, 10) screen.text_center(interval_name(known, guess)) screen.update() end, key=function(n, x) if n == 3 and x == 0 then return pop end end, tick = function() return pop end } end function play_and_wait_3 (known, guess) return { init=function () return state_redraw end, redraw=function() screen.clear() local known_note = c_major_treble(known) known_note.brightness = 10 staff({y=20, margin=5, notes={ known_note, {place=0,accidental=0, brightness=0} }}) screen.update() end, key=function(n, x) if n == 3 and x == 0 then return pop end end, midi=function(ev) if ev.type == "note_on" and ev.note == guess then return pop end end } end function play_and_wait_2 (known, guess) return { init=function () play(guess) return state_redraw end, redraw=function() screen.clear() local known_note = c_major_treble(known) known_note.brightness = 5 staff({y=20, margin=5, notes={ known_note, {place=0,accidental=0, brightness=0} }}) screen.update() end, key=function(n, x) -- forward button if n == 3 and x == 0 then return pop_n(2) end -- back button if n == 2 and x == 0 then return push({play_and_wait_1(known, guess)}) end end, midi=function(ev) if ev.type == "note_on" and ev.note == known then return pop end end } end function play_and_wait_1 (midi_note) complete = nil return { init=function () play(midi_note) return state_redraw end, redraw=function() screen.clear() local known_note = c_major_treble(midi_note) known_note.brightness = 5 staff({y=20, margin=5, notes={ known_note, {place=0,accidental=0, brightness=0} }}) screen.update() end, tick=function() return pop end, key=function(n, x) -- forward button if n == 3 and x == 0 then return pop_n(3) end -- back button if n == 2 and x == 0 then print("back on 1") return replace({play_and_wait_1(midi_note)}) end end, } end function choose_interval () return { init=function () local guess = math.random(60, 72) return push({ correct_guess(60, guess), play_and_wait_3(60, guess), play_and_wait_2(60, guess), play_and_wait_1(60) }) end } end -- drawing, state, etc intervals_state = { stack = { choose_interval(), message({ "first I'll play middle c.", "then a random major interval.", "play both notes back", "as best as you can"}, function (t) if t > 2 then return pop end end), message({"yay."}, function () return pop end), message({ "plug in a midi keyboard", "and give me a middle c." }, nil, function (ev) if ev.type == "note_on" and ev.note == 60 then return pop end end) }, } engine.name = 'PolyPerc' -- TODO fix globals here function play_start(note, vel) if intervals_state.play_audio then -- TODO use velocity here, too engine.hz(MusicUtil.note_num_to_freq(note)) end if intervals_state.play_midi then intervals_state.midi_out:note_on(note, vel or intervals_state.note_vel) end end function play_stop(note, vel) if intervals_state.play_midi then intervals_state.midi_out:note_off(note, vel or intervals_state.note_vel) end end function play(note, vel) play_start(note, vel) if intervals_state.play_midi then -- if state.timer then -- state.timer:stop() -- metro.free(state.timer.id) -- end local t = metro.init() t.time = intervals_state.note_length t.event = function(time) t:stop() metro.free(t.id) play_stop(note, vel) end t:start() end end function init() params:add({ type = "option", id = "output", name = "Output", options = {"audio + midi out", "audio out only", "midi out only"}, action = function(value) if value == 1 then intervals_state.play_midi = true intervals_state.play_audio = true end if value == 2 then intervals_state.play_midi = false intervals_state.play_audio = true end if value == 3 then intervals_state.play_midi = true intervals_state.play_audio = false end end }) params:add({ type = "control", id = "wait", name = "Wait Time", default=60, controlspec = ControlSpec.new(0.0001, 3, 'lin', 0.1, 1.0, "secs"), action = function(value) intervals_state.tick_interval = value if intervals_state.timer then intervals_state.timer.time = value end end}) params:add({ type = "control", id = "note_length", name = "Note length", default=60, controlspec = ControlSpec.new(0.0001, 3, 'lin', 0.05, 0.55, "secs"), action = function(value) intervals_state.note_length = value end}) params:add({ type = "control", id = "note_vel", name = "Note velocity", default=60, controlspec = ControlSpec.new(1, 127, 'lin', 1.0, 20), action = function(value) intervals_state.note_vel = value end}) params:add({ type = "control", id = "midi_in_vport", name = "Midi in vport", default=60, controlspec = ControlSpec.new(1, 4, 'lin', 1, 1), action = function(value) intervals_state.midi_in = midi.connect(value) intervals_state.midi_in.event = function (a) b = midi.to_msg(a) -- monitor if b.type == "note_on" then play_start(b.note, b.vel) end if b.type == "note_off" or b.vel == 0 then play_stop(b.note, b.vel) end state_call(intervals_state, "midi", {b}) end end}) params:add({ type = "control", id = "midi_out_vport", name = "Midi out vport", default=60, controlspec = ControlSpec.new(1, 4, 'lin', 1, 2), action = function(value) intervals_state.midi_out = midi.connect(value) end}) params:default() play(60) end function cleanup() if intervals_state.timer then intervals_state.timer:stop() end end function redraw() screen.clear() screen.aa(0) screen.line_width(1) -- this needs to be the only place this gets called, -- otherwise the menu will be drawn over. state_call(intervals_state, "redraw") end function key(n, x) state_call(intervals_state, "key", {n, x}) end -- Local Variables: -- compile-command: "make upload" -- End: