Halve the bar-stop lead, and add exact bar looping

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.
This commit is contained in:
2026-08-14 20:43:24 +02:00
parent 39cdbeef1d
commit bf69961412
5 changed files with 128 additions and 22 deletions
+31 -8
View File
@@ -53,14 +53,20 @@ local function loop()
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
-- 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()
@@ -74,6 +80,23 @@ local function loop()
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