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:
+31
-8
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
--[[
|
||||
MIDI Grid: loop the bar the cursor is in.
|
||||
|
||||
Sample-accurate at the bar line, because REAPER's audio engine owns the
|
||||
boundary rather than a polling loop -- so the next bar's downbeat never
|
||||
leaks in, at any tempo. Repeats until you press Space.
|
||||
]]
|
||||
local dir = ({ reaper.get_action_context() })[2]:match("@?(.*[\\/])")
|
||||
local G = dofile(dir .. "midigrid_lib.lua")
|
||||
|
||||
local bar = G.loopBar()
|
||||
G.say("Looping bar " .. bar)
|
||||
@@ -83,6 +83,7 @@ actions by hand in Actions → Show action list. That always works.
|
||||
| `Alt+H` | Toggle hold mode | — |
|
||||
| `Alt+Space` | Play from start of the current bar, onwards | works in both modes |
|
||||
| `Ctrl+Alt+Space` | Play the current bar only, then stop | works in both modes |
|
||||
| `Ctrl+Alt+Shift+Space` | Loop the current bar (exact) | works in both modes |
|
||||
| `Alt+A` | Repeat current cell (speak + sound) | — |
|
||||
| `Alt+S` | Set root and scale | — |
|
||||
| `1`–`9` | Grid size — REAPER's own grid actions | same |
|
||||
@@ -157,9 +158,36 @@ Both of these play from the **top of the bar the cursor is in**, and say which
|
||||
bar:
|
||||
|
||||
- **`Alt+Space`** — plays from the bar line and keeps going. Stop with `Space`.
|
||||
- **`Ctrl+Alt+Space`** — plays that bar **only**, stopping at the next bar
|
||||
line. Nothing to stop by hand, so you can fire it repeatedly while nudging a
|
||||
note and hear the same bar each time.
|
||||
- **`Ctrl+Alt+Space`** — plays that bar **only**, stopping just before the next
|
||||
bar line. Nothing to stop by hand, so you can fire it repeatedly while
|
||||
nudging a note and hear the same bar each time.
|
||||
- **`Ctrl+Alt+Shift+Space`** — **loops** that bar, exactly. Repeats until you
|
||||
press `Space`.
|
||||
|
||||
### Which of the two bar actions to use
|
||||
|
||||
`Ctrl+Alt+Space` stops by polling, and a polling loop can only be as precise as
|
||||
its own interval — about 33 ms at frame rate. It therefore stops a little
|
||||
early, and notes falling inside that window do not sound. In practice:
|
||||
|
||||
| Tempo | 16th note | 32nd note | 35 ms lead is |
|
||||
|---|---|---|---|
|
||||
| 120 BPM | 125 ms | 62 ms | 0.28 of a 16th, 0.56 of a 32nd |
|
||||
| 180 BPM | 83 ms | 42 ms | 0.42 of a 16th, 0.84 of a 32nd |
|
||||
| 240 BPM | 62 ms | 31 ms | 0.56 of a 16th, 1.12 of a 32nd |
|
||||
|
||||
So it is safe for 16ths at any sane tempo, and safe for 32nds up to about
|
||||
214 BPM. Past that, the final 32nd of the bar is inside the lead and will not
|
||||
be heard.
|
||||
|
||||
`Ctrl+Alt+Shift+Space` has no such limit. It hands the bar boundary to REAPER's
|
||||
audio engine through the loop points, which is sample accurate, so the next
|
||||
bar's downbeat can never leak in at any tempo or grid size. Use it when you are
|
||||
working at 32nds or faster, or whenever you want certainty. Looping also suits
|
||||
the job well — leave it running and edit while it plays.
|
||||
|
||||
It borrows the project's loop range and repeat state, and the daemon restores
|
||||
both once transport stops, including when you stop with `Space`.
|
||||
|
||||
Auditioning a cell tells you what a note *is*; playing from the downbeat tells
|
||||
you whether it is in the right *place*. At 16ths or 32nds that difference is
|
||||
@@ -177,15 +205,17 @@ script — a script that stayed alive waiting for the bar to end would trigger
|
||||
REAPER's "already running" prompt on the next press. Stopping can move the
|
||||
edit cursor depending on your preferences, so the daemon puts it back.
|
||||
|
||||
It stops **60 ms before** the bar line rather than on it. Two delays stack up
|
||||
otherwise: the daemon 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 reached the instrument by the time the stop lands — and you hear
|
||||
the downbeat you were trying to exclude. Leading by more than both delays keeps
|
||||
it silent, at the cost of a few inaudible milliseconds off the tail.
|
||||
It stops **35 ms before** the bar line rather than on it, or a note sitting
|
||||
exactly on the next bar line would sound. The test uses `GetPlayPosition2`,
|
||||
which reports the block REAPER is about to render rather than what you are
|
||||
currently hearing — that removes render-ahead from the equation, so the lead
|
||||
only has to cover the daemon's own polling interval. (Testing against
|
||||
`GetPlayPosition` instead needed 60 ms to cover both, which was enough to
|
||||
swallow a whole 32nd note from 125 BPM upward.)
|
||||
|
||||
If a next-bar note still sneaks through — a large audio buffer will do it —
|
||||
raise `stoplead` in `ExtState`.
|
||||
`stoplead` in `ExtState` tunes it. Raising it is the fix if a next-bar note
|
||||
still sneaks through; lowering it recovers a little more of the bar's tail at
|
||||
the risk of leaks.
|
||||
|
||||
## Shifting notes in time
|
||||
|
||||
@@ -278,7 +308,7 @@ In `ExtState` section `midigrid`:
|
||||
|---|---|---|
|
||||
| `vel` | 96 | Velocity of inserted notes |
|
||||
| `velstep` | 8 | Velocity change per keypress |
|
||||
| `stoplead` | 0.06 | How early to stop, in seconds, when playing one bar |
|
||||
| `stoplead` | 0.035 | How early to stop, in seconds, when playing one bar |
|
||||
| `chan` | 0 | MIDI channel (0-based) |
|
||||
| `previewdur` | 0.4 | Preview note length, seconds |
|
||||
|
||||
|
||||
+4
-2
@@ -61,7 +61,8 @@ $actions = @(
|
||||
@('RSmidigrid_shlf', 32060, 'MIDI Grid: Shift all notes left (force)', 'MidiGrid_ShiftLeftForce.lua'),
|
||||
@('RSmidigrid_shrf', 32060, 'MIDI Grid: Shift all notes right (force)', 'MidiGrid_ShiftRightForce.lua'),
|
||||
@('RSmidigrid_playbar',32060, 'MIDI Grid: Play from start of current bar', 'MidiGrid_PlayFromBar.lua'),
|
||||
@('RSmidigrid_playone',32060, 'MIDI Grid: Play current bar only', 'MidiGrid_PlayBarOnly.lua')
|
||||
@('RSmidigrid_playone',32060, 'MIDI Grid: Play current bar only', 'MidiGrid_PlayBarOnly.lua'),
|
||||
@('RSmidigrid_loopbar',32060, 'MIDI Grid: Loop current bar', 'MidiGrid_LoopBar.lua')
|
||||
)
|
||||
|
||||
# flags, keycode, section, id
|
||||
@@ -90,7 +91,8 @@ $keys = @(
|
||||
@(29, 32805, 32060, 'RSmidigrid_shlf'),
|
||||
@(29, 32807, 32060, 'RSmidigrid_shrf'),
|
||||
@(17, 32, 32060, 'RSmidigrid_playbar'),
|
||||
@(25, 32, 32060, 'RSmidigrid_playone')
|
||||
@(25, 32, 32060, 'RSmidigrid_playone'),
|
||||
@(29, 32, 32060, 'RSmidigrid_loopbar')
|
||||
)
|
||||
|
||||
$backup = "$kb.midigrid-backup"
|
||||
|
||||
@@ -401,6 +401,45 @@ function M.playFromBar(stopAtBarEnd)
|
||||
return barNum
|
||||
end
|
||||
|
||||
--[[
|
||||
Loop the cursor's bar.
|
||||
|
||||
Where the play-once action stops by polling -- and so can only be as precise
|
||||
as its polling interval -- this hands the boundary to REAPER's audio engine
|
||||
via the loop points, which is sample accurate. The next bar's downbeat can
|
||||
never leak in, at any tempo or grid size.
|
||||
|
||||
The trade is that it repeats until you stop it with Space, rather than
|
||||
stopping itself. For checking placement that is arguably better: leave it
|
||||
looping and edit while it plays.
|
||||
|
||||
Loop range and repeat state are saved and restored by the daemon once
|
||||
transport stops, so this does not quietly rearrange the project.
|
||||
]]
|
||||
function M.loopBar()
|
||||
local here = reaper.GetCursorPosition()
|
||||
local barPos, barEnd, barNum = M.barBounds(here)
|
||||
|
||||
local s, e = reaper.GetSet_LoopTimeRange(false, true, 0, 0, false)
|
||||
M.setTemp("savedloop", ("%.17g,%.17g"):format(s, e))
|
||||
M.setTemp("savedrep", reaper.GetSetRepeat(-1))
|
||||
M.setTemp("stoparm", reaper.time_precise() + 0.5)
|
||||
M.setTemp("stopat", "") -- cancel any pending play-once stop
|
||||
|
||||
reaper.PreventUIRefresh(1)
|
||||
reaper.GetSet_LoopTimeRange(true, true, barPos, barEnd, false)
|
||||
reaper.GetSetRepeat(1)
|
||||
reaper.SetEditCurPos(barPos, false, true)
|
||||
if (reaper.GetPlayState() % 2) == 0 then
|
||||
reaper.CSurf_OnPlay()
|
||||
end
|
||||
reaper.SetEditCurPos(here, false, false)
|
||||
reaper.PreventUIRefresh(-1)
|
||||
|
||||
M.ensureDaemon()
|
||||
return barNum
|
||||
end
|
||||
|
||||
---------------------------------------------------------------- time shift
|
||||
|
||||
--[[
|
||||
|
||||
Reference in New Issue
Block a user