2026-08-14 18:13:45 +02:00
|
|
|
-- 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")
|
|
|
|
|
|
2026-08-14 18:18:12 +02:00
|
|
|
-- Diatonic chords: stacking scale thirds must yield the right quality for
|
|
|
|
|
-- each degree, without any chord-quality table.
|
|
|
|
|
ext.root, ext.scale = "0", "1" -- C major
|
|
|
|
|
eq(table.concat(G.chordPitches(60, 3), ","), "60,64,67", "C major triad")
|
|
|
|
|
eq(table.concat(G.chordPitches(62, 3), ","), "62,65,69", "D minor triad")
|
|
|
|
|
eq(table.concat(G.chordPitches(71, 3), ","), "71,74,77", "B diminished triad")
|
|
|
|
|
eq(table.concat(G.chordPitches(60, 4), ","), "60,64,67,71", "C major seventh")
|
|
|
|
|
eq(table.concat(G.chordPitches(67, 4), ","), "67,71,74,77", "G dominant seventh")
|
|
|
|
|
|
|
|
|
|
-- A minor triad from its own root.
|
|
|
|
|
ext.root, ext.scale = "9", "2"
|
|
|
|
|
eq(table.concat(G.chordPitches(69, 3), ","), "69,72,76", "A minor triad")
|
|
|
|
|
|
|
|
|
|
-- Running off the top of the MIDI range yields nil rather than a partial chord.
|
|
|
|
|
ext.root, ext.scale = "0", "1"
|
|
|
|
|
eq(G.chordPitches(127, 3), nil, "chord past top of range")
|
|
|
|
|
|
2026-08-14 18:13:45 +02:00
|
|
|
print(fails == 0 and "ALL PASS" or (fails .. " FAILURES"))
|