60ms was too blunt: at 180 BPM a 32nd note is 42ms, so the lead swallowed the final 32nd of the bar, and it swallowed a whole 32nd from 125 BPM upward. Testing against GetPlayPosition2 rather than GetPlayPosition removes the render-ahead from the equation -- it reports the block about to be rendered rather than what is currently audible -- so the lead only has to cover the daemon's polling interval, not both delays. 35ms suffices, which is safe for 32nds to about 214 BPM. Polling cannot do better than its own interval, so for genuine precision Ctrl+Alt+Shift+Space loops the bar via the loop points instead, letting the audio engine own the boundary. That is sample accurate at any tempo or grid size. It borrows loop range and repeat state; the daemon restores both once transport stops, including when stopped with Space.
106 lines
3.8 KiB
Lua
106 lines
3.8 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
|
|
|
|
-- 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 if the daemon is terminated or Reaper quits.
|
|
reaper.atexit(allOff)
|
|
loop()
|