Play preview through the edited track's instrument
StuffMIDIMessage feeds the virtual MIDI keyboard, which only reaches tracks that are armed and monitoring -- normally not the track whose item you are editing. Opening an item in grid mode therefore sounded every preview through whatever instrument happened to be armed, which is confusing when the notes you are writing come back in the wrong voice. Grid mode now borrows the record routing: the edited take's track is armed and monitoring with all MIDI inputs (anything narrower can exclude the virtual keyboard), and every other armed track is stood down so it cannot answer as well. The prior arm, monitor and input of each track touched is saved by GUID in transient ExtState. routePreview re-points lazily, so moving the editor to an item on another track costs nothing until the target actually changes. The daemon hands the routing back when grid mode goes off, when the editor closes, or on exit -- the same way it already restores the loop and repeat state the loop-bar action borrows -- so the project is put back even if grid mode is never toggled off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,9 +53,16 @@ loop/repeat state — add it to the daemon loop, not to an action.
|
|||||||
**ExtState section `midigrid`** is the only state. Persisted (`M.set`/`M.get`):
|
**ExtState section `midigrid`** is the only state. Persisted (`M.set`/`M.get`):
|
||||||
`active`, `hold`, `root`, `scale`, `vel`, `velstep`, `chan`, `previewdur`,
|
`active`, `hold`, `root`, `scale`, `vel`, `velstep`, `chan`, `previewdur`,
|
||||||
`stoplead`. Transient (`M.setTemp`/`M.getTemp`, persist flag false): `seq`,
|
`stoplead`. Transient (`M.setTemp`/`M.getTemp`, persist flag false): `seq`,
|
||||||
`pitches`, `dur`, `hb`, `stopat`, `stoparm`, `savedloop`, `savedrep`. Preview
|
`pitches`, `dur`, `hb`, `stopat`, `stoparm`, `savedloop`, `savedrep`,
|
||||||
traffic must stay transient — persisting it would write `reaper.ini` on every
|
`savedarm`, `routed`. Preview traffic must stay transient — persisting it
|
||||||
keypress.
|
would write `reaper.ini` on every keypress.
|
||||||
|
|
||||||
|
**Preview is routed to the edited track.** `StuffMIDIMessage` feeds the virtual
|
||||||
|
MIDI keyboard, which only reaches armed, monitoring tracks, so `M.routePreview`
|
||||||
|
borrows the record arm/monitor/input of the take's track (standing down other
|
||||||
|
armed tracks) and `M.restoreRouting` hands it all back. `M.preview` re-points
|
||||||
|
lazily on every call, so switching items just works. The daemon restores when
|
||||||
|
grid mode goes off, the editor closes, or REAPER exits.
|
||||||
|
|
||||||
**Every grid key falls back.** When grid mode is off, each bound key forwards to
|
**Every grid key falls back.** When grid mode is off, each bound key forwards to
|
||||||
what it did before via `G.passThrough` (e.g. `_OSARA_PREVCHORD`, or a numeric
|
what it did before via `G.passThrough` (e.g. `_OSARA_PREVCHORD`, or a numeric
|
||||||
|
|||||||
+12
-2
@@ -80,6 +80,15 @@ local function loop()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Grid mode borrows the record arm/monitor state of the edited track so
|
||||||
|
-- preview plays through its instrument. Give it back as soon as grid mode
|
||||||
|
-- is off or the editor is gone -- here rather than in an action script, so
|
||||||
|
-- closing the editor puts the project back too.
|
||||||
|
if G.getTemp("savedarm", "") ~= ""
|
||||||
|
and (not G.isActive() or not reaper.MIDIEditor_GetActive()) then
|
||||||
|
G.restoreRouting()
|
||||||
|
end
|
||||||
|
|
||||||
-- Restore the loop range and repeat state the loop-bar action borrowed,
|
-- Restore the loop range and repeat state the loop-bar action borrowed,
|
||||||
-- once transport has stopped. Doing it here rather than in the action
|
-- once transport has stopped. Doing it here rather than in the action
|
||||||
-- script means the project is put back even if you stop with Space.
|
-- script means the project is put back even if you stop with Space.
|
||||||
@@ -100,6 +109,7 @@ local function loop()
|
|||||||
reaper.defer(loop)
|
reaper.defer(loop)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Never leave a note hanging if the daemon is terminated or Reaper quits.
|
-- Never leave a note hanging, or a borrowed record arm unreturned, if the
|
||||||
reaper.atexit(allOff)
|
-- daemon is terminated or Reaper quits.
|
||||||
|
reaper.atexit(function() allOff() ; G.restoreRouting() end)
|
||||||
loop()
|
loop()
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ if not hwnd then G.say("Could not open MIDI editor") return end
|
|||||||
|
|
||||||
G.set("active", 1)
|
G.set("active", 1)
|
||||||
|
|
||||||
|
-- Preview must sound through this track's instrument, whatever else is armed.
|
||||||
|
G.routePreview(reaper.MIDIEditor_GetTake(hwnd) or take)
|
||||||
|
|
||||||
-- Start at the item's beginning so the first cell is the item's first cell.
|
-- Start at the item's beginning so the first cell is the item's first cell.
|
||||||
local pos = reaper.GetMediaItemInfo_Value(item, "D_POSITION")
|
local pos = reaper.GetMediaItemInfo_Value(item, "D_POSITION")
|
||||||
reaper.SetEditCurPos(pos, true, false)
|
reaper.SetEditCurPos(pos, true, false)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ local now = not G.isActive()
|
|||||||
G.set("active", now and 1 or 0)
|
G.set("active", now and 1 or 0)
|
||||||
|
|
||||||
if not now then
|
if not now then
|
||||||
|
-- Hand back the record arm/monitor state grid mode borrowed for preview.
|
||||||
|
G.restoreRouting()
|
||||||
G.say("Grid mode off")
|
G.say("Grid mode off")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -17,6 +19,9 @@ if not take then
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Preview must sound through the instrument of the track being edited.
|
||||||
|
G.routePreview(take)
|
||||||
|
|
||||||
-- Park the pitch cursor on a scale tone so the very first up/down press
|
-- Park the pitch cursor on a scale tone so the very first up/down press
|
||||||
-- moves by a sensible degree rather than off a foreign note.
|
-- moves by a sensible degree rather than off a foreign note.
|
||||||
local p = G.getPitch(hwnd)
|
local p = G.getPitch(hwnd)
|
||||||
|
|||||||
@@ -374,6 +374,19 @@ daemon releases everything on `atexit`, so notes cannot hang.
|
|||||||
Preview requests use **non-persisted** `ExtState` — persisting them would write
|
Preview requests use **non-persisted** `ExtState` — persisting them would write
|
||||||
to `reaper.ini` on every keypress.
|
to `reaper.ini` on every keypress.
|
||||||
|
|
||||||
|
**Preview routing.** Preview notes go out through `StuffMIDIMessage`, i.e.
|
||||||
|
REAPER's virtual MIDI keyboard, which is only heard by tracks that are armed
|
||||||
|
and monitoring — normally not the track you are editing, so notes would sound
|
||||||
|
through whatever unrelated instrument happened to be armed. Grid mode therefore
|
||||||
|
borrows the record routing: on entering grid mode, and again whenever the
|
||||||
|
editor moves to an item on a different track, the edited take's track is armed
|
||||||
|
and monitoring with MIDI input, and every other armed track is stood down so it
|
||||||
|
cannot answer as well. The previous arm, monitor and input setting of every
|
||||||
|
track touched is remembered and handed straight back when grid mode is switched
|
||||||
|
off, when the MIDI editor closes, or when REAPER exits — the daemon does the
|
||||||
|
restoring, in the same way it restores the loop and repeat state the loop-bar
|
||||||
|
action borrows.
|
||||||
|
|
||||||
## Tunables
|
## Tunables
|
||||||
|
|
||||||
In `ExtState` section `midigrid`:
|
In `ExtState` section `midigrid`:
|
||||||
@@ -400,9 +413,11 @@ itself.
|
|||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
- Preview uses `StuffMIDIMessage`, which plays through the track's instrument
|
- While grid mode is on, the edited track is held record-armed and monitoring
|
||||||
via the virtual-keyboard input, so the track must be armed / monitored —
|
with MIDI input so that preview can reach its instrument (see *Preview
|
||||||
exactly as with REAPER's own MIDI preview.
|
routing* above), and any other armed track is stood down. Both are handed
|
||||||
|
back when grid mode ends, but a REAPER crash mid-session would lose that
|
||||||
|
arm state.
|
||||||
- The grid aligns to the project timeline (bar lines), not the item start. For
|
- The grid aligns to the project timeline (bar lines), not the item start. For
|
||||||
an item starting off-grid, the first cell will be partial.
|
an item starting off-grid, the first cell will be partial.
|
||||||
- Note octave naming follows the `midioctoffs` preference; with the default,
|
- Note octave naming follows the `midioctoffs` preference; with the default,
|
||||||
|
|||||||
@@ -681,6 +681,90 @@ function M.nudgeVelocity(take, pitch, delta)
|
|||||||
return "default", v
|
return "default", v
|
||||||
end
|
end
|
||||||
|
|
||||||
|
------------------------------------------------------------ preview routing
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Preview notes leave through StuffMIDIMessage, i.e. the virtual MIDI
|
||||||
|
keyboard, and Reaper feeds that to whatever tracks happen to be armed and
|
||||||
|
monitoring. That is usually NOT the track whose item you are editing, so
|
||||||
|
the grid would sound your notes through some unrelated instrument.
|
||||||
|
|
||||||
|
Grid mode therefore borrows the record routing: the edited take's track is
|
||||||
|
armed and monitoring with MIDI input, every other armed track is stood
|
||||||
|
down so it cannot answer as well, and the previous state of every track we
|
||||||
|
touched is remembered so it can be handed straight back.
|
||||||
|
|
||||||
|
The borrow is undone when grid mode is switched off, when the MIDI editor
|
||||||
|
closes, and on Reaper exit -- all three by the daemon, in the same way it
|
||||||
|
restores the loop/repeat state the loop-bar action borrows.
|
||||||
|
]]
|
||||||
|
|
||||||
|
-- "All MIDI inputs, all channels": 4096 | (device << 5) | channel, with
|
||||||
|
-- device 63 meaning all. Anything narrower may exclude the virtual keyboard.
|
||||||
|
local ALL_MIDI_IN = 4096 + 63 * 32
|
||||||
|
|
||||||
|
local function trackByGUID(guid)
|
||||||
|
for i = 0, reaper.CountTracks(0) - 1 do
|
||||||
|
local tr = reaper.GetTrack(0, i)
|
||||||
|
if reaper.GetTrackGUID(tr) == guid then return tr end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function armState(tr)
|
||||||
|
return ("%s,%d,%d,%d"):format(
|
||||||
|
reaper.GetTrackGUID(tr),
|
||||||
|
reaper.GetMediaTrackInfo_Value(tr, "I_RECARM"),
|
||||||
|
reaper.GetMediaTrackInfo_Value(tr, "I_RECMON"),
|
||||||
|
reaper.GetMediaTrackInfo_Value(tr, "I_RECINPUT"))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hand back every track's arm/monitor/input exactly as we found it.
|
||||||
|
function M.restoreRouting()
|
||||||
|
local saved = M.getTemp("savedarm", "")
|
||||||
|
M.setTemp("savedarm", "")
|
||||||
|
M.setTemp("routed", "")
|
||||||
|
if saved == "" then return false end
|
||||||
|
for guid, arm, mon, inp in saved:gmatch("({[^}]*}),(%-?%d+),(%-?%d+),(%-?%d+)") do
|
||||||
|
local tr = trackByGUID(guid)
|
||||||
|
if tr then
|
||||||
|
reaper.SetMediaTrackInfo_Value(tr, "I_RECARM", tonumber(arm))
|
||||||
|
reaper.SetMediaTrackInfo_Value(tr, "I_RECMON", tonumber(mon))
|
||||||
|
reaper.SetMediaTrackInfo_Value(tr, "I_RECINPUT", tonumber(inp))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Point preview at the track this take lives on. Cheap to call on every
|
||||||
|
-- keypress: it only touches the project when the target has changed.
|
||||||
|
function M.routePreview(take)
|
||||||
|
if not take then return end
|
||||||
|
local tr = reaper.GetMediaItemTake_Track(take)
|
||||||
|
if not tr then return end
|
||||||
|
local guid = reaper.GetTrackGUID(tr)
|
||||||
|
if M.getTemp("routed", "") == guid then return end
|
||||||
|
|
||||||
|
-- Give the previously borrowed track back before borrowing another.
|
||||||
|
M.restoreRouting()
|
||||||
|
|
||||||
|
local saved = {}
|
||||||
|
for i = 0, reaper.CountTracks(0) - 1 do
|
||||||
|
local t = reaper.GetTrack(0, i)
|
||||||
|
local armed = reaper.GetMediaTrackInfo_Value(t, "I_RECARM") == 1
|
||||||
|
if t == tr or armed then
|
||||||
|
saved[#saved + 1] = armState(t)
|
||||||
|
if t ~= tr then reaper.SetMediaTrackInfo_Value(t, "I_RECARM", 0) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
reaper.SetMediaTrackInfo_Value(tr, "I_RECINPUT", ALL_MIDI_IN)
|
||||||
|
reaper.SetMediaTrackInfo_Value(tr, "I_RECARM", 1)
|
||||||
|
reaper.SetMediaTrackInfo_Value(tr, "I_RECMON", 1)
|
||||||
|
|
||||||
|
M.setTemp("savedarm", table.concat(saved, ";"))
|
||||||
|
M.setTemp("routed", guid)
|
||||||
|
end
|
||||||
|
|
||||||
------------------------------------------------------------------ audition
|
------------------------------------------------------------------ audition
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
@@ -694,6 +778,9 @@ end
|
|||||||
]]
|
]]
|
||||||
function M.preview(pitches, dur)
|
function M.preview(pitches, dur)
|
||||||
if not pitches or #pitches == 0 then return end
|
if not pitches or #pitches == 0 then return end
|
||||||
|
-- Sound through the instrument of the track being edited, not through
|
||||||
|
-- whatever else happened to be armed.
|
||||||
|
M.routePreview(select(2, M.editor()))
|
||||||
M.setTemp("pitches", table.concat(pitches, ","))
|
M.setTemp("pitches", table.concat(pitches, ","))
|
||||||
M.setTemp("dur", dur or M.getNum("previewdur", 0.4))
|
M.setTemp("dur", dur or M.getNum("previewdur", 0.4))
|
||||||
M.setTemp("seq", (tonumber(M.getTemp("seq", "0")) or 0) + 1)
|
M.setTemp("seq", (tonumber(M.getTemp("seq", "0")) or 0) + 1)
|
||||||
|
|||||||
@@ -84,4 +84,49 @@ eq(table.concat(G.chordPitches(69, 3), ","), "69,72,76", "A minor triad")
|
|||||||
ext.root, ext.scale = "0", "1"
|
ext.root, ext.scale = "0", "1"
|
||||||
eq(G.chordPitches(127, 3), nil, "chord past top of range")
|
eq(G.chordPitches(127, 3), nil, "chord past top of range")
|
||||||
|
|
||||||
|
-- Preview routing: the edited track takes over arm/monitor so preview sounds
|
||||||
|
-- through its instrument, any other armed track stands down so it cannot
|
||||||
|
-- answer as well, and restore puts every one of them back.
|
||||||
|
local tracks = {
|
||||||
|
{ guid = "{A}", arm = 0, mon = 0, input = 0 },
|
||||||
|
{ guid = "{B}", arm = 0, mon = 0, input = 0 }, -- the track being edited
|
||||||
|
{ guid = "{C}", arm = 1, mon = 1, input = 6112 }, -- somebody else's armed track
|
||||||
|
}
|
||||||
|
local FIELD = { I_RECARM = "arm", I_RECMON = "mon", I_RECINPUT = "input" }
|
||||||
|
reaper.CountTracks = function() return #tracks end
|
||||||
|
reaper.GetTrack = function(_, i) return tracks[i + 1] end
|
||||||
|
reaper.GetTrackGUID = function(t) return t.guid end
|
||||||
|
reaper.GetMediaTrackInfo_Value = function(t, k) return t[FIELD[k]] end
|
||||||
|
reaper.SetMediaTrackInfo_Value = function(t, k, v) t[FIELD[k]] = v end
|
||||||
|
reaper.GetMediaItemTake_Track = function(tk) return tk.track end
|
||||||
|
|
||||||
|
local take = { track = tracks[2] }
|
||||||
|
G.routePreview(take)
|
||||||
|
eq(tracks[2].arm, 1, "edited track armed")
|
||||||
|
eq(tracks[2].mon, 1, "edited track monitoring")
|
||||||
|
eq(tracks[2].input, 6112, "edited track takes MIDI input")
|
||||||
|
eq(tracks[3].arm, 0, "other armed track stood down")
|
||||||
|
eq(tracks[1].arm, 0, "unarmed bystander untouched")
|
||||||
|
|
||||||
|
-- Re-pointing at the same track must not re-save the borrowed state as if it
|
||||||
|
-- were the original, or restore would hand back grid mode's own settings.
|
||||||
|
G.routePreview(take)
|
||||||
|
G.restoreRouting()
|
||||||
|
eq(tracks[2].arm, 0, "edited track disarmed again")
|
||||||
|
eq(tracks[2].mon, 0, "edited track monitoring restored")
|
||||||
|
eq(tracks[2].input, 0, "edited track input restored")
|
||||||
|
eq(tracks[3].arm, 1, "other track re-armed")
|
||||||
|
eq(tracks[3].mon, 1, "other track monitoring restored")
|
||||||
|
|
||||||
|
-- Restoring twice is harmless, and switching to a take on another track
|
||||||
|
-- returns the first one before borrowing the second.
|
||||||
|
eq(G.restoreRouting(), false, "restore with nothing borrowed")
|
||||||
|
G.routePreview(take)
|
||||||
|
G.routePreview({ track = tracks[1] })
|
||||||
|
eq(tracks[1].arm, 1, "second track armed")
|
||||||
|
eq(tracks[2].arm, 0, "first track handed back on switch")
|
||||||
|
G.restoreRouting()
|
||||||
|
eq(tracks[1].arm, 0, "second track handed back")
|
||||||
|
eq(tracks[3].arm, 1, "bystander still armed at the end")
|
||||||
|
|
||||||
print(fails == 0 and "ALL PASS" or (fails .. " FAILURES"))
|
print(fails == 0 and "ALL PASS" or (fails .. " FAILURES"))
|
||||||
|
|||||||
Reference in New Issue
Block a user