--[[ 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()