diff options
| author | gretchen <gretchen@gnar.cool> | 2019-11-03 23:17:36 -0800 | 
|---|---|---|
| committer | gretchen <gretchen@gnar.cool> | 2019-11-03 23:19:11 -0800 | 
| commit | eb8c54345a7f3ffb413a0fb1028330b7f0921d84 (patch) | |
| tree | a76609609ecb8b6614f1adce39261e62934b7713 | |
| download | intervals-eb8c54345a7f3ffb413a0fb1028330b7f0921d84.tar.gz intervals-eb8c54345a7f3ffb413a0fb1028330b7f0921d84.zip | |
intervals v0.1v0.1
| -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 | 
