Add octave jumps and jumping to notes that exist
Arrowing cell by cell is right inside a dense bar and tedious across an empty one. Three pairs of keys now jump straight to a note that is really there, one pair for each axis: Alt+Up/Down the column -- next pitch sounding in this cell Alt+Left/Right the row -- next note at the cursor's pitch Ctrl+Alt+Left/Right anywhere -- next note in the take, any pitch plus Ctrl+Alt+Up/Down for whole-octave movement, which needs no scale awareness because an octave preserves the pitch class. All the jumps ignore muted notes, which are not audible content, and the row and any-note jumps anchor on note starts, so a note held across eight cells is one stop rather than eight. Landing in a chord takes whichever note is nearest the pitch you came from rather than always the bottom one. They sit on Ctrl+Alt rather than plain Ctrl because REAPER already uses Ctrl+arrows to move between items. The logic lives in the lib as usual, with four run* drivers following the runShift pattern, so the eight new action scripts are seven lines each. test_lib.lua previously dofile'd an absolute D:\ path and so could not run outside one machine; it now resolves the lib relative to itself. Stubbing MIDI_GetNote and the PPQ conversions reaches further than the old "pure logic only" boundary, so the new tests cover the search functions and the drivers' speech and cursor movement too.
This commit is contained in:
@@ -285,6 +285,175 @@ function M.pitchesInCell(take, ppqA, ppqB)
|
||||
return uniq
|
||||
end
|
||||
|
||||
------------------------------------------------------------ octave jumps
|
||||
|
||||
-- Jump a whole octave, keeping the pitch class. Because the octave is
|
||||
-- transpositionally neutral, a scale tone stays a scale tone, so this needs
|
||||
-- no scale awareness at all -- unlike scaleStep it just adds 12.
|
||||
-- Returns nil at the ends of the MIDI range rather than clamping, so the
|
||||
-- caller can say "top" instead of pretending it moved.
|
||||
function M.octaveStep(pitch, dir)
|
||||
local p = pitch + 12 * dir
|
||||
if p < 0 or p > 127 then return nil end
|
||||
return p
|
||||
end
|
||||
|
||||
------------------------------------------------------ jumping to real notes
|
||||
|
||||
--[[
|
||||
Arrowing cell by cell is right inside a dense bar and tedious across an
|
||||
empty one. These jump straight to a note that is actually there, along the
|
||||
three axes you can be looking down:
|
||||
|
||||
column -- the pitches sounding in the cell you are on, up or down
|
||||
row -- the same pitch, forwards or backwards in time
|
||||
any -- the next note in the take whatever its pitch
|
||||
|
||||
Two rules they all share. They ignore muted notes, because a muted note is
|
||||
not audible content and stopping on one would be a lie. And, apart from the
|
||||
column jump, they anchor on note *starts*, so a note sustained over eight
|
||||
cells is one stop rather than eight.
|
||||
]]
|
||||
|
||||
-- Nearest pitch sounding in the cell strictly above (dir 1) or below (dir -1)
|
||||
-- `pitch`. The cell's pitches arrive sorted, so the first match in the right
|
||||
-- direction is the nearest one.
|
||||
function M.pitchInCellToward(take, ppqA, ppqB, pitch, dir)
|
||||
local ps = M.pitchesInCell(take, ppqA, ppqB)
|
||||
if dir > 0 then
|
||||
for i = 1, #ps do if ps[i] > pitch then return ps[i] end end
|
||||
else
|
||||
for i = #ps, 1, -1 do if ps[i] < pitch then return ps[i] end end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Nearest note start beyond the current cell in direction dir.
|
||||
-- opts.pitch -- restrict to this one pitch (row navigation)
|
||||
-- opts.near -- when several notes share the winning position, prefer the
|
||||
-- one closest to this pitch, so jumping into a chord lands
|
||||
-- where you were rather than at its bottom
|
||||
-- Returns startPPQ, pitch, or nil when there is nothing that way.
|
||||
function M.nextNoteStart(take, ppqA, ppqB, dir, opts)
|
||||
opts = opts or {}
|
||||
local bestPos, bestPitch
|
||||
local _, notecnt = reaper.MIDI_CountEvts(take)
|
||||
|
||||
for i = 0, notecnt - 1 do
|
||||
local ok, _, muted, sppq, _, _, p = reaper.MIDI_GetNote(take, i)
|
||||
local wanted = ok and not muted and (not opts.pitch or p == opts.pitch)
|
||||
local beyond = wanted and
|
||||
(dir > 0 and sppq > ppqB - SLACK or dir < 0 and sppq < ppqA - SLACK)
|
||||
|
||||
if beyond then
|
||||
local better
|
||||
if not bestPos then
|
||||
better = true
|
||||
elseif math.abs(sppq - bestPos) <= SLACK then
|
||||
-- Same position: a chord. Break the tie towards opts.near.
|
||||
better = opts.near ~= nil and
|
||||
math.abs(p - opts.near) < math.abs(bestPitch - opts.near)
|
||||
elseif dir > 0 then
|
||||
better = sppq < bestPos
|
||||
else
|
||||
better = sppq > bestPos
|
||||
end
|
||||
if better then bestPos, bestPitch = sppq, p end
|
||||
end
|
||||
end
|
||||
|
||||
return bestPos, bestPitch
|
||||
end
|
||||
|
||||
-- Which grid cell a PPQ position falls in. Converted through quarter notes,
|
||||
-- like everything else here, so it survives tempo changes.
|
||||
function M.cellIndexAtPPQ(take, ppq)
|
||||
local g = M.gridQN(take)
|
||||
local qn = reaper.MIDI_GetProjQNFromPPQPos(take, ppq)
|
||||
return math.floor(qn / g + 1e-9)
|
||||
end
|
||||
|
||||
-- Speak and sound the cell the cursor has landed in, naming the pitch cursor
|
||||
-- as well when the cell holds more than one note and the answer is therefore
|
||||
-- not obvious.
|
||||
local function announceCell(take, pitch)
|
||||
local ppqA, ppqB = M.cellPPQ(take)
|
||||
local ps = M.pitchesInCell(take, ppqA, ppqB)
|
||||
local msg = M.cellPosText(take) .. ", " .. M.cellContentText(take)
|
||||
if pitch and #ps > 1 then msg = msg .. ", cursor " .. M.noteName(pitch) end
|
||||
M.say(msg)
|
||||
M.preview(ps)
|
||||
end
|
||||
|
||||
-- Shared driver for the two octave actions.
|
||||
function M.runOctave(dir)
|
||||
local hwnd, take, err = M.editor()
|
||||
if not take then M.say(err) return end
|
||||
|
||||
local p = M.octaveStep(M.getPitch(hwnd), dir)
|
||||
if not p then M.say(dir > 0 and "top" or "bottom") return end
|
||||
M.setPitch(hwnd, p)
|
||||
|
||||
local ppqA, ppqB = M.cellPPQ(take)
|
||||
local on = M.noteInCell(take, p, ppqA, ppqB) ~= nil
|
||||
M.say(M.noteName(p) .. (on and ", on" or ""))
|
||||
M.preview({ p })
|
||||
end
|
||||
|
||||
-- Shared driver for the two column actions: next note up or down within the
|
||||
-- cell the cursor is already on. Time does not move.
|
||||
function M.runColumnJump(dir)
|
||||
local hwnd, take, err = M.editor()
|
||||
if not take then M.say(err) return end
|
||||
|
||||
local ppqA, ppqB = M.cellPPQ(take)
|
||||
local p = M.pitchInCellToward(take, ppqA, ppqB, M.getPitch(hwnd), dir)
|
||||
if not p then
|
||||
M.say(dir > 0 and "No note above in this cell" or "No note below in this cell")
|
||||
return
|
||||
end
|
||||
|
||||
M.setPitch(hwnd, p)
|
||||
M.say(M.noteName(p) .. ", on")
|
||||
M.preview({ p })
|
||||
end
|
||||
|
||||
-- Shared driver for the two row actions: next note at the cursor's own pitch,
|
||||
-- forwards or backwards. The pitch cursor does not move.
|
||||
function M.runRowJump(dir)
|
||||
local hwnd, take, err = M.editor()
|
||||
if not take then M.say(err) return end
|
||||
|
||||
local pitch = M.getPitch(hwnd)
|
||||
local ppqA, ppqB = M.cellPPQ(take)
|
||||
local pos = M.nextNoteStart(take, ppqA, ppqB, dir, { pitch = pitch })
|
||||
if not pos then
|
||||
M.say(("No %s %s"):format(dir > 0 and "later" or "earlier", M.noteName(pitch)))
|
||||
return
|
||||
end
|
||||
|
||||
M.gotoCell(take, M.cellIndexAtPPQ(take, pos))
|
||||
announceCell(take, pitch)
|
||||
end
|
||||
|
||||
-- Shared driver for the two any-note actions: the next note anywhere in the
|
||||
-- take, moving both cursors.
|
||||
function M.runNoteJump(dir)
|
||||
local hwnd, take, err = M.editor()
|
||||
if not take then M.say(err) return end
|
||||
|
||||
local ppqA, ppqB = M.cellPPQ(take)
|
||||
local pos, p = M.nextNoteStart(take, ppqA, ppqB, dir, { near = M.getPitch(hwnd) })
|
||||
if not pos then
|
||||
M.say(dir > 0 and "No later notes" or "No earlier notes")
|
||||
return
|
||||
end
|
||||
|
||||
M.setPitch(hwnd, p)
|
||||
M.gotoCell(take, M.cellIndexAtPPQ(take, pos))
|
||||
announceCell(take, p)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------- edits
|
||||
|
||||
-- Toggle a note on/off at (current cell, pitch).
|
||||
|
||||
Reference in New Issue
Block a user