Files
midigrid/test_lib.lua
T
Talon 8fd6f947ed Accessible grid-based MIDI entry mode for REAPER
Keyboard- and speech-driven step entry for the MIDI editor, built for use
with OSARA. Arrows walk grid cells and scale degrees, Enter toggles notes,
and every position is spoken and auditioned.

Uses REAPER's own edit cursor and active_note_row rather than private cursor
state, so grid mode and OSARA editing stay in sync. Grid size comes from
MIDI_GetGrid(), so existing grid keybindings drive it. When grid mode is off,
every bound key forwards to its previous action.

Preview timing lives in a background daemon so the action scripts can exit
immediately -- a script still alive on the next keypress triggers REAPER's
"already running" prompt and breaks key repeat.
2026-08-14 18:13:45 +02:00

71 lines
2.4 KiB
Lua

-- Stub just enough of the reaper API to exercise the pure logic in the lib.
local ext = { root = "0", scale = "1" }
reaper = {
GetExtState = function(_, k) return ext[k] or "" end,
SetExtState = function(_, k, v) ext[k] = v end,
APIExists = function() return false end,
ShowConsoleMsg = function() end,
SNM_GetIntConfigVar = function(_, d) return 1 end,
GetCursorPosition = function() return 0 end,
TimeMap_GetTimeSigAtTime = function() return 4, 4 end,
}
local G = dofile("D:\\code\\midigrid\\midigrid_lib.lua")
local fails = 0
local function eq(got, want, label)
if got ~= want then
print(("FAIL %s: got %s want %s"):format(label, tostring(got), tostring(want)))
fails = fails + 1
end
end
-- Note naming: with midioctoffs=1, note 60 reads as C5 (Reaper's default).
eq(G.noteName(60), "C5", "noteName 60")
eq(G.noteName(61), "C#5", "noteName 61")
eq(G.noteName(72), "C6", "noteName 72")
eq(G.noteName(48), "C4", "noteName 48")
-- C major: stepping up from C5 walks the scale, not semitones.
ext.root, ext.scale = "0", "1"
local p, seq = 60, {}
for _ = 1, 7 do p = G.scaleStep(p, 1); seq[#seq + 1] = p end
eq(table.concat(seq, ","), "62,64,65,67,69,71,72", "C major ascending")
p, seq = 60, {}
for _ = 1, 3 do p = G.scaleStep(p, -1); seq[#seq + 1] = p end
eq(table.concat(seq, ","), "59,57,55", "C major descending")
-- An off-scale note must not trap the cursor: C#5 in C major steps to D5.
eq(G.scaleStep(61, 1), 62, "off-scale up")
eq(G.scaleStep(61, -1), 60, "off-scale down")
-- Pentatonic skips more than a tone.
ext.scale = "10" -- major pentatonic
eq(G.scaleStep(60, 1), 62, "pentatonic C->D")
eq(G.scaleStep(64, 1), 67, "pentatonic E->G")
-- Chromatic behaves like plain semitones.
ext.scale = "14"
eq(G.scaleStep(60, 1), 61, "chromatic up")
-- Root transposition: A natural minor.
ext.root, ext.scale = "9", "2"
eq(G.inScale(69), true, "A in A minor")
eq(G.inScale(70), false, "A# not in A minor")
eq(G.scaleName(), "A natural minor", "scale name")
-- Range clamping at the extremes of the MIDI range.
ext.root, ext.scale = "0", "1"
eq(G.scaleStep(127, 1), nil, "top of range")
eq(G.scaleStep(0, -1), nil, "bottom of range")
-- Grid labels in 4/4: one bar is 4 QN.
eq(G.gridLabel(4), "1 bar", "grid 1 bar")
eq(G.gridLabel(8), "2 bars", "grid 2 bars")
eq(G.gridLabel(1), "1/4", "grid quarter")
eq(G.gridLabel(0.25), "1/16", "grid sixteenth")
eq(G.gridLabel(0.5), "1/8", "grid eighth")
print(fails == 0 and "ALL PASS" or (fails .. " FAILURES"))