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:
@@ -0,0 +1,20 @@
|
||||
-- MIDI Grid: fuse a run of repeated notes at this pitch into one held note
|
||||
local dir = ({ reaper.get_action_context() })[2]:match("@?(.*[\\/])")
|
||||
local G = dofile(dir .. "midigrid_lib.lua")
|
||||
|
||||
if not G.isActive() then return end
|
||||
|
||||
local hwnd, take, err = G.editor()
|
||||
if not take then G.say(err) return end
|
||||
|
||||
local p = G.getPitch(hwnd)
|
||||
local n = G.joinRun(take, p)
|
||||
|
||||
if not n then
|
||||
G.say("No note here")
|
||||
elseif n == 0 then
|
||||
G.say(G.noteName(p) .. ", nothing adjacent to join")
|
||||
else
|
||||
G.say(G.noteName(p) .. ", joined " .. n .. " notes")
|
||||
G.preview({ p })
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
-- MIDI Grid: lengthen the note under the cursor by one grid cell
|
||||
local dir = ({ reaper.get_action_context() })[2]:match("@?(.*[\\/])")
|
||||
local G = dofile(dir .. "midigrid_lib.lua")
|
||||
|
||||
if not G.isActive() then G.passThrough(40446) return end -- Edit: Lengthen notes one grid unit
|
||||
|
||||
local hwnd, take, err = G.editor()
|
||||
if not take then G.say(err) return end
|
||||
|
||||
local p = G.getPitch(hwnd)
|
||||
local cells, absorbed = G.resizeNote(take, p, 1)
|
||||
if not cells then G.say("No note here") return end
|
||||
|
||||
local msg = G.noteName(p) .. ", " .. cells .. (cells == 1 and " cell" or " cells")
|
||||
if absorbed > 0 then msg = msg .. ", joined " .. absorbed end
|
||||
G.say(msg)
|
||||
G.preview({ p })
|
||||
@@ -0,0 +1,19 @@
|
||||
-- MIDI Grid: shorten the note under the cursor by one grid cell
|
||||
local dir = ({ reaper.get_action_context() })[2]:match("@?(.*[\\/])")
|
||||
local G = dofile(dir .. "midigrid_lib.lua")
|
||||
|
||||
if not G.isActive() then G.passThrough(40447) return end -- Edit: Shorten notes one grid unit
|
||||
|
||||
local hwnd, take, err = G.editor()
|
||||
if not take then G.say(err) return end
|
||||
|
||||
local p = G.getPitch(hwnd)
|
||||
local cells, _, clamped = G.resizeNote(take, p, -1)
|
||||
if not cells then G.say("No note here") return end
|
||||
|
||||
if clamped then
|
||||
G.say(G.noteName(p) .. ", 1 cell, minimum")
|
||||
else
|
||||
G.say(G.noteName(p) .. ", " .. cells .. (cells == 1 and " cell" or " cells"))
|
||||
end
|
||||
G.preview({ p })
|
||||
@@ -72,6 +72,8 @@ actions by hand in Actions → Show action list. That always works.
|
||||
| `Left` / `Right` | Cursor back / forward one grid cell | OSARA previous / next chord |
|
||||
| `Up` / `Down` | Pitch up / down one scale degree | OSARA higher / lower note in chord |
|
||||
| `Enter` | Toggle note at cursor cell | Edit: Event properties |
|
||||
| `L` / `Shift+L` | Lengthen / shorten note by one cell | Edit: Lengthen / shorten notes one grid unit |
|
||||
| `Alt+J` | Join a run of repeated notes into one | — |
|
||||
| `Alt+3` | Toggle diatonic triad on cursor pitch | — |
|
||||
| `Alt+7` | Toggle diatonic seventh chord | — |
|
||||
| `Alt+Shift+Up` / `Alt+Shift+Down` | Velocity up / down | — |
|
||||
@@ -115,6 +117,36 @@ it into one sustained note. Toggling a cell off in the middle of a sustained
|
||||
run splits it in two; at either end, it shortens it. So you can draw a long
|
||||
note by arrowing right and pressing Enter across several cells.
|
||||
|
||||
Hold mode governs **entry only** — it never retroactively merges notes that
|
||||
are already there. To sustain what you have already written, use the length
|
||||
keys below.
|
||||
|
||||
## Note length — sustaining notes you already entered
|
||||
|
||||
- **`L`** — lengthen the note under the cursor by one grid cell.
|
||||
- **`Shift+L`** — shorten it by one cell. It never shrinks below a single
|
||||
cell; it says *"minimum"* rather than deleting the note.
|
||||
- **`Alt+J`** — fuse an entire run of repeated notes at this pitch into one
|
||||
sustained note, in a single keystroke.
|
||||
|
||||
Lengthening **swallows** any same-pitch note it runs into, taking that note's
|
||||
tail if it reached further. So pressing `L` across a row of repeated notes
|
||||
fuses them rather than producing overlaps.
|
||||
|
||||
Each press reports the resulting length — *"E5, 3 cells"*, or *"E5, 2 cells,
|
||||
joined 1"* when it absorbed something.
|
||||
|
||||
### Why not re-toggle with hold on instead?
|
||||
|
||||
Turning hold on and re-entering existing notes does work. But toggling a note
|
||||
off and on **deletes and recreates it, resetting its velocity to the default**
|
||||
— if you have shaped dynamics, that quietly discards them.
|
||||
|
||||
The length keys edit the note in place, so velocity, channel and note identity
|
||||
survive. They also need no mode: `L` behaves identically whether hold is on or
|
||||
off, so there is no state to set up beforehand and none to remember to turn
|
||||
off afterwards.
|
||||
|
||||
## Chords
|
||||
|
||||
`Alt+3` stamps a **diatonic triad** on the cursor pitch; `Alt+7` a seventh
|
||||
|
||||
+8
-2
@@ -52,7 +52,10 @@ $actions = @(
|
||||
@('RSmidigrid_triad', 32060, 'MIDI Grid: Toggle diatonic triad', 'MidiGrid_ChordTriad.lua'),
|
||||
@('RSmidigrid_seventh',32060, 'MIDI Grid: Toggle diatonic seventh chord', 'MidiGrid_ChordSeventh.lua'),
|
||||
@('RSmidigrid_velup', 32060, 'MIDI Grid: Velocity up', 'MidiGrid_VelUp.lua'),
|
||||
@('RSmidigrid_veldn', 32060, 'MIDI Grid: Velocity down', 'MidiGrid_VelDown.lua')
|
||||
@('RSmidigrid_veldn', 32060, 'MIDI Grid: Velocity down', 'MidiGrid_VelDown.lua'),
|
||||
@('RSmidigrid_longer', 32060, 'MIDI Grid: Lengthen note one cell', 'MidiGrid_Lengthen.lua'),
|
||||
@('RSmidigrid_shorter',32060, 'MIDI Grid: Shorten note one cell', 'MidiGrid_Shorten.lua'),
|
||||
@('RSmidigrid_join', 32060, 'MIDI Grid: Join run of repeated notes', 'MidiGrid_Join.lua')
|
||||
)
|
||||
|
||||
# flags, keycode, section, id
|
||||
@@ -72,7 +75,10 @@ $keys = @(
|
||||
@(17, 51, 32060, 'RSmidigrid_triad'),
|
||||
@(17, 55, 32060, 'RSmidigrid_seventh'),
|
||||
@(21, 32806, 32060, 'RSmidigrid_velup'),
|
||||
@(21, 32808, 32060, 'RSmidigrid_veldn')
|
||||
@(21, 32808, 32060, 'RSmidigrid_veldn'),
|
||||
@(1, 76, 32060, 'RSmidigrid_longer'),
|
||||
@(5, 76, 32060, 'RSmidigrid_shorter'),
|
||||
@(17, 74, 32060, 'RSmidigrid_join')
|
||||
)
|
||||
|
||||
$backup = "$kb.midigrid-backup"
|
||||
|
||||
+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