capture: render a ranged item capture time-bounded — the selected-items source can't narrow a window, only a full-extent one uses it

sourceModeForScope now takes the item extent vs. the requested window. Full-extent item captures and the batch keep the old path unchanged.
This commit is contained in:
2026-08-01 19:43:06 -04:00
parent 8bf6841f7b
commit 5e3ea6c851
16 changed files with 407 additions and 20 deletions
+9
View File
@@ -51,6 +51,7 @@ Detail specific to these pure modules:
- `capture_paths` — the REAPER-free path arithmetic behind offline capture: bank-subfolder + unique-filename derivation (`deriveBankPaths`, forward-slash form, no filesystem touch), the absolute-render-dir vs. project-relative-index-path split (`BankPaths`), the persist-side inverse (`resolveBankFile`, `projectDirOfRpp`), the Save-As bank-relocation plan (`deriveRelocationPlan`), and the GUID-primary project-identity classifier (`classifyProjectTransition``NoOp`/`Load`/`SaveAsRelocate`) the persist-poll timer drives.
- `insert_plan` — the REAPER-free logic behind the `insert` shell (M6): computes the `InsertMedia` `mode` bitmask from an `InsertOptions` struct (placement target, tempo-conform ratio, preserve-pitch flag), guaranteeing the &4 stretch-to-time-selection bit is never set and that no tempo bits are set when `conform == None`.
- `render_settings` — the REAPER-free logic behind the capture action family: `SourceMode``RENDER_SETTINGS` bit mapping, `P_RAZOREDITS` string parsing + range-union bounds, razor-else-time range inference, the FX-scope bypass plan (`fxBypassPlanFor`), the tail-mode → `RENDER_TAILFLAG`/`RENDER_NORMALIZE`/`RENDER_TRIMEND` mapping (`tailRenderSettingsFor`) and its realtime-window analog (`realtimeRecordWindowEnd`), and the capture-action taxonomy table (`captureActionTable`) `main.cpp` iterates to register the CAPTURE_ITEM/CAPTURE_TRACK family.
- `render_window` — the REAPER-free frame arithmetic behind exact capture bounds: `frameCountFor` (the frame count a project-time window occupies at the project rate — the number a capture's file must match) and `itemExtentPrintsWindow`, the predicate `render_settings::sourceModeForScope` consults to decide whether REAPER's selected-items render source can express a requested window at all.
- `tail_control` — the REAPER-free logic behind the docked `bank_panel`'s tail-mode toggle: the cycle order (None → Auto → Manual → None), the Manual-length clamp/scroll-wheel fine-adjust (`clampManualMs`/`adjustManualMs`, 250 ms/notch, 2000 ms default), the toggle's label text (e.g. "Tail: Manual 2.0s"), and the `TailSetting` JSON round-trip persist stores per-project.
## Gotchas
@@ -60,6 +61,14 @@ Detail specific to these pure modules:
(`reaper_plugin_functions.h` lines ~3041/~3047/~3051/~3062) — re-verify
against the header before changing any bit value, per the root `CLAUDE.md`
API-verification rule.
- **The selected-items render source (`&32`) cannot narrow a window** — REAPER
derives that render's bounds from the selected items' own extents, so
`RENDER_BOUNDSFLAG=0` + `RENDER_STARTPOS`/`RENDER_ENDPOS` do not constrain it.
This is an inference from the observed defect (a time selection inside a long
item captured the whole item), NOT a header-confirmed fact. It is why
`sourceModeForScope` routes item scope to `&32` only when the item extent
already IS the requested window — do not re-point item scope unconditionally at
`&32`, and do not widen the `&32` branch to windows it cannot express.
- `kRenderPreFaderStems` (&8192) is deliberately **not** used — REAPER offline
render has no true pre-FX "dry" bit; FX scoping is done entirely by the
FX-bypass-around-render mechanism, never by a render bit.
+3
View File
@@ -7,6 +7,9 @@ reasampler_test(insert_plan LINK insert_plan)
reasampler_pure_library(render_settings SOURCES render_settings.cpp LINK PUBLIC bank_model)
reasampler_test(render_settings LINK render_settings)
reasampler_pure_library(render_window SOURCES render_window.cpp)
reasampler_test(render_window LINK render_window)
reasampler_pure_library(batch_capture SOURCES batch_capture.cpp)
reasampler_test(batch_capture LINK batch_capture)
+7 -3
View File
@@ -100,12 +100,16 @@ RenderSettingsChoice renderSettingsFor(SourceMode mode, double /*wetDry*/) {
return c;
}
SourceMode sourceModeForScope(CaptureScope scope) {
SourceMode sourceModeForScope(CaptureScope scope, bool itemExtentIsWindow) {
switch (scope) {
case CaptureScope::Item: return SourceMode::SelectedItems;
case CaptureScope::Item:
return itemExtentIsWindow ? SourceMode::SelectedItems
: SourceMode::SelectedTracks;
case CaptureScope::Track: return SourceMode::SelectedTracks;
}
return SourceMode::SelectedItems; // unreachable for a valid enum; fail closed
// Unreachable for a valid enum; fail closed to the time-bounded render, which
// honors the requested bounds whatever the selection is.
return SourceMode::SelectedTracks;
}
RangeSource inferRangeSource(bool hasRazorArea) {
+12 -3
View File
@@ -105,9 +105,18 @@ enum class CaptureScope {
Track,
};
// The render source mode each scope drives. Item captures selected items, Track
// captures selected tracks (via master).
SourceMode sourceModeForScope(CaptureScope scope);
// The render source mode each scope drives. Track scope always captures its
// selected tracks (via master), time-bounded by RENDER_STARTPOS/ENDPOS.
//
// Item scope captures the selected items ONLY when `itemExtentIsWindow` — i.e.
// when those items' own extent already prints the requested window (see
// render_window::itemExtentPrintsWindow). REAPER's selected-items render source
// derives the render's bounds from the item extents, so a window strictly inside
// (or wider than) a selected item cannot be expressed through it; that case
// renders time-bounded through the items' own tracks. The FX scope is unaffected
// either way — fxBypassPlanFor is keyed on CaptureScope, not on the source mode,
// so an item capture still hears take/item FX only.
SourceMode sourceModeForScope(CaptureScope scope, bool itemExtentIsWindow);
// --- Range inference: razor-else-time (orthogonal to scope) -------------------
//
+36
View File
@@ -0,0 +1,36 @@
// render_window.cpp — see the header.
#include "core/capture/render_window.h"
#include <cmath>
namespace reasampler::capture {
namespace {
// Round-to-nearest, so a position that sits mid-frame maps to the frame a render
// of it prints rather than to the frame below it.
long long frameIndexAt(double seconds, int sampleRate) {
return std::llround(seconds * static_cast<double>(sampleRate));
}
} // namespace
long long frameCountFor(double startSeconds, double endSeconds, int sampleRate) {
if (sampleRate <= 0) return 0;
if (!(endSeconds > startSeconds)) return 0;
const long long frames =
frameIndexAt(endSeconds, sampleRate) - frameIndexAt(startSeconds, sampleRate);
return frames > 0 ? frames : 0;
}
bool itemExtentPrintsWindow(double reqStart, double reqEnd,
double itemStart, double itemEnd,
int sampleRate) {
if (sampleRate <= 0)
return reqStart == itemStart && reqEnd == itemEnd;
return frameIndexAt(reqStart, sampleRate) == frameIndexAt(itemStart, sampleRate)
&& frameIndexAt(reqEnd, sampleRate) == frameIndexAt(itemEnd, sampleRate);
}
} // namespace reasampler::capture
+27
View File
@@ -0,0 +1,27 @@
#pragma once
// render_window — pure frame arithmetic for a capture's requested window: the
// frame count a project-time range occupies, and whether a render whose bounds
// come from the selected items' own extent already prints that window.
// NO REAPER types; unit-tested by tests/test_render_window.cpp.
namespace reasampler::capture {
// Frames the [startSeconds, endSeconds) window occupies at `sampleRate`. Both
// edges are resolved to the NEAREST frame boundary and subtracted, so the answer
// is a difference of frame indices rather than a rounded duration — two windows
// of equal length at different offsets can legitimately differ by one frame.
// Returns 0 for a non-positive rate or an empty/inverted window.
long long frameCountFor(double startSeconds, double endSeconds, int sampleRate);
// True when a render bounded by the selected items' own extent
// [itemStart, itemEnd) already prints exactly the requested
// [reqStart, reqEnd) window — the one case where REAPER's selected-items render
// source needs no correction. Compared at frame resolution, because a sub-frame
// difference prints the same frames. An unknown rate (<= 0) falls back to exact
// equality, which can only send a window to the time-bounded render, never widen
// one.
bool itemExtentPrintsWindow(double reqStart, double reqEnd,
double itemStart, double itemEnd,
int sampleRate);
} // namespace reasampler::capture