Add whole-take time shifting
Ctrl+Shift+Left/Right move every note in the take by one grid cell, for clips that landed off the beat. Since the whole take moves together, notes cannot collide with each other; the hazard is the item boundary. Notes pushed past either end stop sounding but are not deleted, so the operation is reversible -- shifting back restores them. The plain keys still refuse when that would happen and report the count, because a clip that has silently lost its first bar is a bad thing to discover later. Ctrl+Alt+Shift forces it and names the consequence. Positions are converted through QN rather than offset in raw ticks, so shifts stay musically correct across tempo changes.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
-- MIDI Grid: shift all notes one cell left, refusing if any would be lost
|
||||
local dir = ({ reaper.get_action_context() })[2]:match("@?(.*[\\/])")
|
||||
local G = dofile(dir .. "midigrid_lib.lua")
|
||||
if not G.isActive() then return end
|
||||
G.runShift(-1, false)
|
||||
@@ -0,0 +1,5 @@
|
||||
-- MIDI Grid: shift all notes one cell left even if some leave the item
|
||||
local dir = ({ reaper.get_action_context() })[2]:match("@?(.*[\\/])")
|
||||
local G = dofile(dir .. "midigrid_lib.lua")
|
||||
if not G.isActive() then return end
|
||||
G.runShift(-1, true)
|
||||
@@ -0,0 +1,5 @@
|
||||
-- MIDI Grid: shift all notes one cell right, refusing if any would be lost
|
||||
local dir = ({ reaper.get_action_context() })[2]:match("@?(.*[\\/])")
|
||||
local G = dofile(dir .. "midigrid_lib.lua")
|
||||
if not G.isActive() then return end
|
||||
G.runShift(1, false)
|
||||
@@ -0,0 +1,5 @@
|
||||
-- MIDI Grid: shift all notes one cell right even if some leave the item
|
||||
local dir = ({ reaper.get_action_context() })[2]:match("@?(.*[\\/])")
|
||||
local G = dofile(dir .. "midigrid_lib.lua")
|
||||
if not G.isActive() then return end
|
||||
G.runShift(1, true)
|
||||
@@ -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 |
|
||||
| `Ctrl+Shift+Left` / `Ctrl+Shift+Right` | Shift **all** notes one cell, refusing if any would leave the item | — |
|
||||
| `Ctrl+Alt+Shift+Left` / `Ctrl+Alt+Shift+Right` | Same, but shift anyway | — |
|
||||
| `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 | — |
|
||||
@@ -147,6 +149,27 @@ 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.
|
||||
|
||||
## Shifting notes in time
|
||||
|
||||
If a whole clip landed a couple of cells off the beat, `Ctrl+Shift+Left` and
|
||||
`Ctrl+Shift+Right` move **every note in the take** by one grid cell.
|
||||
|
||||
Because the entire take moves together, notes can never collide with each
|
||||
other. What is at stake is the **item boundary**: notes pushed past either end
|
||||
stop sounding. They are *not* deleted — shifting back the other way brings
|
||||
them straight back — but a clip that has quietly lost its first bar is not
|
||||
something you want to discover two hours later.
|
||||
|
||||
So the plain keys **refuse** the shift if any note would end up outside, and
|
||||
say how many: *"Not shifted left, 3 notes would fall outside the item. Add Alt
|
||||
to force."*
|
||||
|
||||
Adding **Alt** (`Ctrl+Alt+Shift+Left` / `Right`) does it anyway, reporting the
|
||||
consequence: *"Shifted left, 3 notes now outside the item and silent."*
|
||||
|
||||
Positions are converted through quarter-notes rather than offset in raw ticks,
|
||||
so a shift stays musically correct across tempo changes.
|
||||
|
||||
## Chords
|
||||
|
||||
`Alt+3` stamps a **diatonic triad** on the cursor pitch; `Alt+7` a seventh
|
||||
|
||||
+10
-2
@@ -55,7 +55,11 @@ $actions = @(
|
||||
@('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')
|
||||
@('RSmidigrid_join', 32060, 'MIDI Grid: Join run of repeated notes', 'MidiGrid_Join.lua'),
|
||||
@('RSmidigrid_shl', 32060, 'MIDI Grid: Shift all notes one cell left', 'MidiGrid_ShiftLeft.lua'),
|
||||
@('RSmidigrid_shr', 32060, 'MIDI Grid: Shift all notes one cell right', 'MidiGrid_ShiftRight.lua'),
|
||||
@('RSmidigrid_shlf', 32060, 'MIDI Grid: Shift all notes left (force)', 'MidiGrid_ShiftLeftForce.lua'),
|
||||
@('RSmidigrid_shrf', 32060, 'MIDI Grid: Shift all notes right (force)', 'MidiGrid_ShiftRightForce.lua')
|
||||
)
|
||||
|
||||
# flags, keycode, section, id
|
||||
@@ -78,7 +82,11 @@ $keys = @(
|
||||
@(21, 32808, 32060, 'RSmidigrid_veldn'),
|
||||
@(1, 76, 32060, 'RSmidigrid_longer'),
|
||||
@(5, 76, 32060, 'RSmidigrid_shorter'),
|
||||
@(17, 74, 32060, 'RSmidigrid_join')
|
||||
@(17, 74, 32060, 'RSmidigrid_join'),
|
||||
@(13, 32805, 32060, 'RSmidigrid_shl'),
|
||||
@(13, 32807, 32060, 'RSmidigrid_shr'),
|
||||
@(29, 32805, 32060, 'RSmidigrid_shlf'),
|
||||
@(29, 32807, 32060, 'RSmidigrid_shrf')
|
||||
)
|
||||
|
||||
$backup = "$kb.midigrid-backup"
|
||||
|
||||
@@ -352,6 +352,83 @@ function M.toggleCell(take, pitch)
|
||||
return result, detail
|
||||
end
|
||||
|
||||
---------------------------------------------------------------- time shift
|
||||
|
||||
--[[
|
||||
Move every note in the take by whole grid cells.
|
||||
|
||||
Because the entire take moves together, notes cannot collide with each
|
||||
other -- the only thing at stake is the item's boundaries. Notes pushed
|
||||
outside them are not deleted, they simply stop sounding, and shifting back
|
||||
the other way restores them. The safe variant still refuses, because
|
||||
silently silencing part of a clip is exactly the kind of thing you want to
|
||||
be told about rather than discover later.
|
||||
|
||||
Positions are converted through QN rather than offset in ticks, so a shift
|
||||
stays musically correct across tempo changes.
|
||||
|
||||
Returns moved, outside. moved is nil when the shift was refused.
|
||||
]]
|
||||
function M.shiftAllNotes(take, cells, force)
|
||||
local _, notecnt = reaper.MIDI_CountEvts(take)
|
||||
if notecnt == 0 then return 0, 0 end
|
||||
|
||||
local g = M.gridQN(take)
|
||||
local item = reaper.GetMediaItemTake_Item(take)
|
||||
local itemPos = reaper.GetMediaItemInfo_Value(item, "D_POSITION")
|
||||
local itemLen = reaper.GetMediaItemInfo_Value(item, "D_LENGTH")
|
||||
local loPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, itemPos)
|
||||
local hiPPQ = reaper.MIDI_GetPPQPosFromProjTime(take, itemPos + itemLen)
|
||||
|
||||
local moved, outside = {}, 0
|
||||
for i = 0, notecnt - 1 do
|
||||
local ok, _, _, s, e = reaper.MIDI_GetNote(take, i)
|
||||
if ok then
|
||||
local ns = reaper.MIDI_GetPPQPosFromProjQN(take,
|
||||
reaper.MIDI_GetProjQNFromPPQPos(take, s) + cells * g)
|
||||
local ne = reaper.MIDI_GetPPQPosFromProjQN(take,
|
||||
reaper.MIDI_GetProjQNFromPPQPos(take, e) + cells * g)
|
||||
moved[i] = { ns, ne }
|
||||
if ns < loPPQ - SLACK or ne > hiPPQ + SLACK then outside = outside + 1 end
|
||||
end
|
||||
end
|
||||
|
||||
if outside > 0 and not force then return nil, outside end
|
||||
|
||||
reaper.Undo_BeginBlock2(0)
|
||||
for i = 0, notecnt - 1 do
|
||||
local np = moved[i]
|
||||
if np then
|
||||
reaper.MIDI_SetNote(take, i, nil, nil, np[1], np[2], nil, nil, nil, true)
|
||||
end
|
||||
end
|
||||
reaper.MIDI_Sort(take)
|
||||
reaper.Undo_EndBlock2(0, "MIDI Grid: shift notes", -1)
|
||||
|
||||
return notecnt, outside
|
||||
end
|
||||
|
||||
-- Shared driver for the four shift actions: does the work and the speech.
|
||||
function M.runShift(cells, force)
|
||||
local hwnd, take, err = M.editor()
|
||||
if not take then M.say(err) return end
|
||||
|
||||
local moved, outside = M.shiftAllNotes(take, cells, force)
|
||||
local dir = cells < 0 and "left" or "right"
|
||||
|
||||
if moved == nil then
|
||||
M.say(("Not shifted %s, %d %s would fall outside the item. Add Alt to force.")
|
||||
:format(dir, outside, outside == 1 and "note" or "notes"))
|
||||
elseif moved == 0 then
|
||||
M.say("No notes to shift")
|
||||
elseif outside > 0 then
|
||||
M.say(("Shifted %s, %d %s now outside the item and silent")
|
||||
:format(dir, outside, outside == 1 and "note" or "notes"))
|
||||
else
|
||||
M.say(("Shifted %s, %d %s"):format(dir, moved, moved == 1 and "note" or "notes"))
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------- note length
|
||||
|
||||
--[[
|
||||
|
||||
Reference in New Issue
Block a user