Add note length editing for sustaining already-entered notes
Hold mode only governs entry, so sustaining existing notes previously meant toggling hold on and re-entering them. That works but deletes and recreates each note, silently resetting velocity to the default. L and Shift+L resize the note under the cursor by whole grid cells, editing it in place so velocity, channel and note identity survive. They are independent of hold mode, so there is no state to set up or unset. Alt+J fuses a whole run of repeated notes at one pitch in a single keystroke. Lengthening absorbs same-pitch notes it runs into, taking their tail if longer, so growing across a repeated run fuses it instead of creating overlaps. Also makes noteEndingAt/noteStartingAt public and fixes their two call sites in toggleCell, which would otherwise have been nil-call errors in hold mode.
This commit is contained in:
+104
-4
@@ -248,7 +248,7 @@ function M.noteInCell(take, pitch, ppqA, ppqB)
|
||||
return nil
|
||||
end
|
||||
|
||||
local function noteEndingAt(take, pitch, ppq)
|
||||
function M.noteEndingAt(take, pitch, ppq)
|
||||
local _, notecnt = reaper.MIDI_CountEvts(take)
|
||||
for i = 0, notecnt - 1 do
|
||||
local ok, _, _, sppq, eppq, _, p = reaper.MIDI_GetNote(take, i)
|
||||
@@ -257,7 +257,7 @@ local function noteEndingAt(take, pitch, ppq)
|
||||
return nil
|
||||
end
|
||||
|
||||
local function noteStartingAt(take, pitch, ppq)
|
||||
function M.noteStartingAt(take, pitch, ppq)
|
||||
local _, notecnt = reaper.MIDI_CountEvts(take)
|
||||
for i = 0, notecnt - 1 do
|
||||
local ok, _, _, sppq, eppq, _, p = reaper.MIDI_GetNote(take, i)
|
||||
@@ -324,8 +324,8 @@ function M.toggleCell(take, pitch)
|
||||
if M.isHold() then
|
||||
-- Join with neighbours of the same pitch so runs of adjacent cells
|
||||
-- become one sustained note rather than repeated attacks.
|
||||
local pi, _, _ = noteEndingAt(take, pitch, ppqA)
|
||||
local ni, nsppq, neppq = noteStartingAt(take, pitch, ppqB)
|
||||
local pi, _, _ = M.noteEndingAt(take, pitch, ppqA)
|
||||
local ni, nsppq, neppq = M.noteStartingAt(take, pitch, ppqB)
|
||||
if pi and ni then
|
||||
reaper.MIDI_SetNote(take, pi, nil, nil, nil, neppq, nil, nil, nil, true)
|
||||
reaper.MIDI_DeleteNote(take, ni)
|
||||
@@ -352,6 +352,106 @@ function M.toggleCell(take, pitch)
|
||||
return result, detail
|
||||
end
|
||||
|
||||
--------------------------------------------------------------- note length
|
||||
|
||||
--[[
|
||||
Grow or shrink the note under the cursor by whole grid cells.
|
||||
|
||||
This is how you sustain notes that already exist. It is deliberately
|
||||
independent of hold mode -- hold mode governs how *new* notes join as you
|
||||
enter them, whereas this edits an existing note's length in place, so it
|
||||
preserves velocity, channel and note identity. Toggling a note off and on
|
||||
again to make it join would silently reset its velocity to the default.
|
||||
|
||||
Extending swallows any same-pitch notes it runs into, absorbing their tail
|
||||
if they reach further than the new end, so growing a note across repeated
|
||||
notes fuses the run rather than producing overlaps.
|
||||
]]
|
||||
function M.resizeNote(take, pitch, cells)
|
||||
local ppqA, ppqB = M.cellPPQ(take)
|
||||
local idx, sppq, eppq = M.noteInCell(take, pitch, ppqA, ppqB)
|
||||
if not idx then return nil end
|
||||
|
||||
local g = M.gridQN(take)
|
||||
local startQN = reaper.MIDI_GetProjQNFromPPQPos(take, sppq)
|
||||
local endQN = reaper.MIDI_GetProjQNFromPPQPos(take, eppq)
|
||||
|
||||
local newEndQN = endQN + cells * g
|
||||
local clamped = false
|
||||
if newEndQN < startQN + g - 1e-9 then
|
||||
newEndQN = startQN + g -- never shrink below a single cell
|
||||
clamped = true
|
||||
end
|
||||
local newEnd = reaper.MIDI_GetPPQPosFromProjQN(take, newEndQN)
|
||||
|
||||
reaper.Undo_BeginBlock2(0)
|
||||
|
||||
local absorbed = 0
|
||||
if cells > 0 then
|
||||
-- Collect first, delete descending: deleting shifts later indices.
|
||||
local victims = {}
|
||||
local _, notecnt = reaper.MIDI_CountEvts(take)
|
||||
for i = 0, notecnt - 1 do
|
||||
local ok, _, _, s, e, _, p = reaper.MIDI_GetNote(take, i)
|
||||
if ok and p == pitch and i ~= idx and s > sppq + SLACK and s < newEnd - SLACK then
|
||||
victims[#victims + 1] = i
|
||||
if e > newEnd then newEnd = e end
|
||||
end
|
||||
end
|
||||
table.sort(victims, function(a, b) return a > b end)
|
||||
for _, i in ipairs(victims) do
|
||||
reaper.MIDI_DeleteNote(take, i)
|
||||
if i < idx then idx = idx - 1 end
|
||||
absorbed = absorbed + 1
|
||||
end
|
||||
end
|
||||
|
||||
reaper.MIDI_SetNote(take, idx, nil, nil, nil, newEnd, nil, nil, nil, true)
|
||||
reaper.MIDI_Sort(take)
|
||||
reaper.Undo_EndBlock2(0, "MIDI Grid: resize note", -1)
|
||||
|
||||
local lenCells = (reaper.MIDI_GetProjQNFromPPQPos(take, newEnd) - startQN) / g
|
||||
return math.floor(lenCells + 0.5), absorbed, clamped
|
||||
end
|
||||
|
||||
-- Fuse every contiguous same-pitch note touching the one under the cursor
|
||||
-- into a single sustained note. Returns how many notes were merged.
|
||||
function M.joinRun(take, pitch)
|
||||
local ppqA, ppqB = M.cellPPQ(take)
|
||||
local idx, sppq, eppq = M.noteInCell(take, pitch, ppqA, ppqB)
|
||||
if not idx then return nil end
|
||||
|
||||
local minS, maxE = sppq, eppq
|
||||
local victims = {}
|
||||
local seen = { [idx] = true }
|
||||
|
||||
while true do
|
||||
local i, s = M.noteEndingAt(take, pitch, minS)
|
||||
if not i or seen[i] then break end
|
||||
seen[i] = true ; victims[#victims + 1] = i ; minS = s
|
||||
end
|
||||
while true do
|
||||
local i, _, e = M.noteStartingAt(take, pitch, maxE)
|
||||
if not i or seen[i] then break end
|
||||
seen[i] = true ; victims[#victims + 1] = i ; maxE = e
|
||||
end
|
||||
|
||||
if #victims == 0 then return 0 end
|
||||
|
||||
reaper.Undo_BeginBlock2(0)
|
||||
table.sort(victims, function(a, b) return a > b end)
|
||||
for _, i in ipairs(victims) do reaper.MIDI_DeleteNote(take, i) end
|
||||
-- Indices shifted; re-find the survivor by its unchanged start position.
|
||||
local keep = M.noteStartingAt(take, pitch, sppq)
|
||||
if keep then
|
||||
reaper.MIDI_SetNote(take, keep, nil, nil, minS, maxE, nil, nil, nil, true)
|
||||
end
|
||||
reaper.MIDI_Sort(take)
|
||||
reaper.Undo_EndBlock2(0, "MIDI Grid: join notes", -1)
|
||||
|
||||
return #victims + 1
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------- chords
|
||||
|
||||
-- Stack `count` diatonic chord tones on `pitch`, skipping a scale degree
|
||||
|
||||
Reference in New Issue
Block a user