2026-08-14 18:13:45 +02:00
|
|
|
--[[
|
|
|
|
|
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
|
|
|
|
|
|
2026-08-14 20:04:20 +02:00
|
|
|
-- 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
|
2026-08-14 20:43:24 +02:00
|
|
|
-- 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
|
2026-08-14 20:04:20 +02:00
|
|
|
-- 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
|
|
|
|
|
|
2026-08-24 00:55:25 +02:00
|
|
|
-- 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
|
|
|
|
|
|
2026-08-14 20:43:24 +02:00
|
|
|
-- 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
|
|
|
|
|
|
2026-08-14 18:13:45 +02:00
|
|
|
reaper.defer(loop)
|
|
|
|
|
end
|
|
|
|
|
|
2026-08-24 00:55:25 +02:00
|
|
|
-- 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)
|
2026-08-14 18:13:45 +02:00
|
|
|
loop()
|