diff --git a/MidiGrid_OpenInGrid.lua b/MidiGrid_OpenInGrid.lua index 7afcaaf..325c28f 100644 --- a/MidiGrid_OpenInGrid.lua +++ b/MidiGrid_OpenInGrid.lua @@ -14,20 +14,26 @@ if not item then G.say("No item selected") return end local take = reaper.GetActiveTake(item) if not take or not reaper.TakeIsMIDI(take) then G.say("Selected item is not MIDI") return end +-- Opening the editor parks the cursor at the item start; remember where the +-- user actually was so it can be handed back. +local cur = reaper.GetCursorPosition() + -- Item: Open in built-in MIDI editor (set default behaviour in preferences) reaper.Main_OnCommand(40153, 0) local hwnd = reaper.MIDIEditor_GetActive() if not hwnd then G.say("Could not open MIDI editor") return end +take = reaper.MIDIEditor_GetTake(hwnd) or take + G.set("active", 1) -- Preview must sound through this track's instrument, whatever else is armed. -G.routePreview(reaper.MIDIEditor_GetTake(hwnd) or take) +G.routePreview(take) --- Start at the item's beginning so the first cell is the item's first cell. -local pos = reaper.GetMediaItemInfo_Value(item, "D_POSITION") -reaper.SetEditCurPos(pos, true, false) +-- Stay where you were listening, on the nearest cell boundary, rather than +-- jumping back to the top of the item. +G.snapCursorToCell(take, cur) local p = G.getPitch(hwnd) if not G.inScale(p) then diff --git a/README.md b/README.md index 4fce5e1..78e5377 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,10 @@ the editor to toggle back off. the MIDI editor's `active_note_row`. No private cursor state exists, so grid mode and OSARA cannot drift out of sync — you can switch between them mid-phrase. +- **Opening keeps your place.** `Alt+Shift+G` leaves the edit cursor where it + already was, rounded to the nearest grid line, instead of dropping it back to + the start of the item — so you can listen to a bar, hit it, and start writing + right there. A cursor outside the item lands on the item's first or last cell. - **Turning grid mode off restores everything.** Each bound key forwards to what it did before, so nothing is permanently taken away. diff --git a/midigrid_lib.lua b/midigrid_lib.lua index 449afca..14a41d8 100644 --- a/midigrid_lib.lua +++ b/midigrid_lib.lua @@ -229,6 +229,37 @@ function M.gotoCell(take, idx) reaper.SetEditCurPos(t, true, false) end +--[[ + Put the cursor on the nearest cell boundary to time t (the edit cursor by + default), without leaving the item. + + Entering grid mode uses this instead of jumping to the item start: wherever + you were already listening stays where you are, only tidied onto a real cell + so the first left/right press moves a whole cell rather than a remainder. + + Returns the cell index landed on. +]] +function M.snapCursorToCell(take, t) + local g = M.gridQN(take) + local item = reaper.GetMediaItemTake_Item(take) + local lo = reaper.GetMediaItemInfo_Value(item, "D_POSITION") + local hi = lo + reaper.GetMediaItemInfo_Value(item, "D_LENGTH") + + t = t or reaper.GetCursorPosition() + local idx = math.floor(reaper.TimeMap2_timeToQN(0, t) / g + 0.5) + + -- A cursor outside the item pulls back to the first or last cell the item + -- covers. The first is the cell containing the item start, which is where + -- plain "open in editor" used to land. + local loIdx = math.floor(reaper.TimeMap2_timeToQN(0, lo) / g + 1e-9) + local hiIdx = math.ceil(reaper.TimeMap2_timeToQN(0, hi) / g - 1e-9) - 1 + if hiIdx < loIdx then hiIdx = loIdx end + if idx < loIdx then idx = loIdx elseif idx > hiIdx then idx = hiIdx end + + M.gotoCell(take, idx) + return idx +end + ------------------------------------------------------------- note queries -- A cell counts as occupied by a note if the note overlaps it at all, so diff --git a/test_lib.lua b/test_lib.lua index 37edd20..a76a05a 100644 --- a/test_lib.lua +++ b/test_lib.lua @@ -304,4 +304,39 @@ eq(row, 67, "any back landed nearest the pitch it came from") said = at(6, 69, function() G.runNoteJump(1) end) eq(said, "No later notes", "any forward past the last note") +--------------------------------------------------- entering grid mode + +-- Opening an item in grid mode keeps the cursor where it already was, rounded +-- to the nearest cell, instead of dropping back to the item start. +local itemPos, itemLen = 0, 10 +reaper.GetMediaItemTake_Item = function() return "item" end +reaper.GetMediaItemInfo_Value = function(_, k) + return k == "D_POSITION" and itemPos or itemLen +end + +local function snap(t) return G.snapCursorToCell(take, t) end +eq(snap(2.4), 2, "snap rounds back to the nearer cell") +eq(snap(2.6), 3, "snap rounds forward to the nearer cell") +eq(snap(3), 3, "snap leaves a cursor already on a cell alone") +eq(cursor, 3, "snap moves the edit cursor to the cell start") +cursor = 6.7 +eq(snap(nil), 7, "snap defaults to the edit cursor") + +-- A cursor outside the item lands on the item's first or last cell. +itemPos, itemLen = 2, 4 -- the item spans cells 2 to 5 +eq(snap(0), 2, "snap from before the item lands on its first cell") +eq(snap(99), 5, "snap from past the item lands on its last cell") +eq(snap(6), 5, "snap at the item end lands on the last cell inside it") + +-- Cells stay aligned to the timeline, not the item, so an item that starts +-- off-grid still uses the cell that contains its start as the floor. +itemPos, itemLen = 2.5, 4 +eq(snap(0), 2, "snap floors to the cell containing an off-grid item start") + +-- The rounding follows the grid size, not a fixed quarter note. +grid, itemPos, itemLen = 2, 0, 10 +eq(snap(2.9), 1, "snap rounds to half-note cells on a half-note grid") +eq(snap(3.1), 2, "snap rounds up to half-note cells on a half-note grid") +eq(cursor, 4, "half-note cell 2 starts at QN 4") + print(fails == 0 and "ALL PASS" or (fails .. " FAILURES"))