Files
midigrid/MidiGrid_Daemon.lua
T
TalonandClaude Opus 5 0acb7d58f6 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>
2026-08-24 00:55:25 +02:00

116 lines
4.3 KiB
Lua

--[[
MIDI Grid: preview daemon.
Runs quietly in the background and owns every preview note-on/note-off.
The action scripts never defer, so they always exit instantly and Reaper
never shows its "already running" prompt no matter how fast keys repeat.
Started automatically by the action scripts; you never need to run it.
]]
local dir = ({ reaper.get_action_context() })[2]:match("@?(.*[\\/])")
local G = dofile(dir .. "midigrid_lib.lua")
-- Refuse to start a second copy.
local hb = tonumber(G.getTemp("hb", "0")) or 0
if os.time() - hb <= 3 then return end
local sounding = {}
local releaseAt = 0
local lastSeq = tonumber(G.getTemp("seq", "0")) or 0
local function allOff()
for _, p in ipairs(sounding) do
reaper.StuffMIDIMessage(0, 0x80, p, 0)
end
sounding = {}
end
local function loop()
G.setTemp("hb", os.time())
local seq = tonumber(G.getTemp("seq", "0")) or 0
if seq ~= lastSeq then
lastSeq = seq
-- A new request cuts whatever is still sounding, so fast arrowing
-- retriggers cleanly instead of piling notes up.
allOff()
local vel = math.floor(G.getNum("vel", 96))
for s in tostring(G.getTemp("pitches", "")):gmatch("%d+") do
local p = tonumber(s)
if p and p >= 0 and p <= 127 then
sounding[#sounding + 1] = p
reaper.StuffMIDIMessage(0, 0x90, p, vel)
end
end
releaseAt = reaper.time_precise() + (tonumber(G.getTemp("dur", "0.4")) or 0.4)
end
if #sounding > 0 and reaper.time_precise() >= releaseAt then
allOff()
end
-- Pending "stop at end of bar" request, posted by the play-one-bar action.
local stopAt = tonumber(G.getTemp("stopat", ""))
if stopAt then
local playing = (reaper.GetPlayState() % 2) == 1
-- Stop a little BEFORE the bar line, not on it, or a note sitting exactly
-- on the next bar line sounds.
--
-- GetPlayPosition2 reports the block REAPER is about to render, rather
-- than what you are currently hearing. Testing against that removes the
-- render-ahead entirely, so the lead only has to cover this loop's own
-- polling interval (~33ms at frame rate) instead of both. That is why
-- 35ms suffices here where 60ms was needed against GetPlayPosition.
--
-- Polling cannot do better than its own interval; for a sample-accurate
-- bar boundary use the loop-bar action instead, which lets REAPER's audio
-- engine handle the edge.
local lead = G.getNum("stoplead", 0.035)
if playing and reaper.GetPlayPosition2() >= stopAt - lead then
-- Stopping can move the edit cursor depending on preferences, and the
-- edit cursor is the grid cursor, so put it back.
local cur = reaper.GetCursorPosition()
reaper.CSurf_OnStop()
reaper.SetEditCurPos(cur, false, false)
G.setTemp("stopat", "")
elseif not playing
and reaper.time_precise() > (tonumber(G.getTemp("stoparm", "0")) or 0) then
-- Transport stopped by other means; drop the pending request.
G.setTemp("stopat", "")
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,
-- 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.
local savedLoop = G.getTemp("savedloop", "")
if savedLoop ~= "" then
local playing = (reaper.GetPlayState() % 2) == 1
if not playing
and reaper.time_precise() > (tonumber(G.getTemp("stoparm", "0")) or 0) then
local a, b = savedLoop:match("^([%-%d%.eE+]+),([%-%d%.eE+]+)$")
if a and b then
reaper.GetSet_LoopTimeRange(true, true, tonumber(a), tonumber(b), false)
end
reaper.GetSetRepeat(tonumber(G.getTemp("savedrep", "0")) or 0)
G.setTemp("savedloop", "")
end
end
reaper.defer(loop)
end
-- Never leave a note hanging, or a borrowed record arm unreturned, if the
-- daemon is terminated or Reaper quits.
reaper.atexit(function() allOff() ; G.restoreRouting() end)
loop()