diff options
Diffstat (limited to 'intervals.lua')
-rw-r--r-- | intervals.lua | 489 |
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 |