-- 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") -- 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") -- Preview routing: the edited track takes over arm/monitor so preview sounds -- through its instrument, any other armed track stands down so it cannot -- answer as well, and restore puts every one of them back. local tracks = { { guid = "{A}", arm = 0, mon = 0, input = 0 }, { guid = "{B}", arm = 0, mon = 0, input = 0 }, -- the track being edited { guid = "{C}", arm = 1, mon = 1, input = 6112 }, -- somebody else's armed track } local FIELD = { I_RECARM = "arm", I_RECMON = "mon", I_RECINPUT = "input" } reaper.CountTracks = function() return #tracks end reaper.GetTrack = function(_, i) return tracks[i + 1] end reaper.GetTrackGUID = function(t) return t.guid end reaper.GetMediaTrackInfo_Value = function(t, k) return t[FIELD[k]] end reaper.SetMediaTrackInfo_Value = function(t, k, v) t[FIELD[k]] = v end reaper.GetMediaItemTake_Track = function(tk) return tk.track end local take = { track = tracks[2] } G.routePreview(take) eq(tracks[2].arm, 1, "edited track armed") eq(tracks[2].mon, 1, "edited track monitoring") eq(tracks[2].input, 6112, "edited track takes MIDI input") eq(tracks[3].arm, 0, "other armed track stood down") eq(tracks[1].arm, 0, "unarmed bystander untouched") -- Re-pointing at the same track must not re-save the borrowed state as if it -- were the original, or restore would hand back grid mode's own settings. G.routePreview(take) G.restoreRouting() eq(tracks[2].arm, 0, "edited track disarmed again") eq(tracks[2].mon, 0, "edited track monitoring restored") eq(tracks[2].input, 0, "edited track input restored") eq(tracks[3].arm, 1, "other track re-armed") eq(tracks[3].mon, 1, "other track monitoring restored") -- Restoring twice is harmless, and switching to a take on another track -- returns the first one before borrowing the second. eq(G.restoreRouting(), false, "restore with nothing borrowed") G.routePreview(take) G.routePreview({ track = tracks[1] }) eq(tracks[1].arm, 1, "second track armed") eq(tracks[2].arm, 0, "first track handed back on switch") G.restoreRouting() eq(tracks[1].arm, 0, "second track handed back") eq(tracks[3].arm, 1, "bystander still armed at the end") print(fails == 0 and "ALL PASS" or (fails .. " FAILURES"))