Playing a single bar still triggered notes placed exactly on the next bar line. Two delays stack: the daemon polls at roughly frame rate, so it can notice up to ~33ms late, and REAPER renders audio ahead of the play position it reports, so the note has already reached the instrument before the stop lands. Stopping 60ms early clears both. The cost is a few milliseconds off the tail of the bar, which is inaudible, and the benefit is that the downbeat you were trying to exclude stays excluded. Tunable via the stoplead ExtState for setups with large audio buffers.
83 lines
2.9 KiB
Lua
83 lines
2.9 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. Two delays stack up
|
|
-- otherwise: this loop only runs at about frame rate, and REAPER renders
|
|
-- audio ahead of the play position it reports -- so a note sitting exactly
|
|
-- on the next bar line has already been sent to the instrument by the time
|
|
-- we notice. Leading by more than both delays keeps that note silent.
|
|
-- Costs a few ms off the tail of the bar, which is inaudible.
|
|
local lead = G.getNum("stoplead", 0.06)
|
|
if playing and reaper.GetPlayPosition() >= 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
|
|
|
|
reaper.defer(loop)
|
|
end
|
|
|
|
-- Never leave a note hanging if the daemon is terminated or Reaper quits.
|
|
reaper.atexit(allOff)
|
|
loop()
|