aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--intervals.lua489
1 files changed, 489 insertions, 0 deletions
diff --git a/intervals.lua b/intervals.lua
new file mode 100644
index 0000000..b29c5c9
--- /dev/null
+++ b/intervals.lua
@@ -0,0 +1,489 @@
+-- interval ear training
+--
+-- hear two notes;
+-- play two notes
+--
+-- enc2: repeat
+-- enc3: skip
+--
+-- to do:
+-- 1) keep a score
+-- 2) select scales
+-- 3) select base tone
+-- 4) key signatures
+-- 5) play from input
+
+-- graphics and audio utilities
+function midi_to_hz(note)
+ local hz = (440 / 32) * (2 ^ ((note - 9) / 12))
+ return hz
+end
+
+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)
+ state_call(state, "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
+
+engine.name = 'PolyPerc'
+function play(hz)
+ engine.hz(hz)
+end
+
+
+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(midi_to_hz(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_to_hz(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)
+ },
+}
+
+function init()
+ play(midi_to_hz(60))
+ -- initialize midi
+ m = midi.connect()
+ m.event = function (a)
+ b = midi.to_msg(a)
+ -- monitor
+ if b.type == "note_on" then
+ play(midi_to_hz(b.note))
+ end
+ state_call(intervals_state, "midi", {b})
+ end
+end
+
+function cleanup()
+ intervals_state.timer:stop()
+end
+
+function redraw()
+ screen.clear()
+ screen.aa(0)
+ screen.line_width(1)
+
+ state_call(intervals_state, "redraw")
+end
+
+function key(n, x)
+ state_call(intervals_state, "key", {n, x})
+end