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:
2026-08-24 00:55:25 +02:00
co-authored by Claude Opus 5
parent 2fe9fa75b2
commit 0acb7d58f6
7 changed files with 180 additions and 8 deletions
+87
View File
@@ -681,6 +681,90 @@ function M.nudgeVelocity(take, pitch, delta)
return "default", v
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
--[[
@@ -694,6 +778,9 @@ end
]]
function M.preview(pitches, dur)
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("dur", dur or M.getNum("previewdur", 0.4))
M.setTemp("seq", (tonumber(M.getTemp("seq", "0")) or 0) + 1)