feat(capture): offline auto/manual tail via pure TailMode mapping

Add TailMode{None,Auto,Manual} + derived -72dB TRIMEND + 8s cap in render_settings;
wire into OfflineRenderBackend with snapshot/restore of the tail settings; add
CAPTURE_ITEM_TAIL/CAPTURE_TRACK_TAIL variant actions (plain actions stay exact-bounds).
This commit is contained in:
2026-07-23 17:08:24 -04:00
parent 92762e2b6c
commit 2d225258b4
6 changed files with 313 additions and 63 deletions
+31 -23
View File
@@ -74,10 +74,9 @@ constexpr int kActionRenderUsingMostRecentSettings = 42230;
// ourselves for exact, unrounded bounds). Verified: SDK header line ~3042.
constexpr double kBoundsCustom = 0.0;
// RENDER_TAILFLAG bit &1 = apply tail for custom time bounds. We clear it for
// the spike (exact bounds, no added silence — precision invariant).
constexpr double kTailFlagNone = 0.0;
constexpr double kTailFlagCustomBounds = 1.0; // &1, used only if renderTail set
// RENDER_TAILFLAG / RENDER_TAILMS / RENDER_NORMALIZE / RENDER_TRIMEND for the tail
// are driven from the pure tailRenderSettingsFor mapping (render_settings.h),
// unit-tested outside the DAW. See the tail-driving block in capture() below.
// RENDER_DITHER disable-all: &16 = disable all dither/noise-shaping.
// Verified: SDK header line ~3050: "&16=disable all".
@@ -85,12 +84,6 @@ constexpr double kTailFlagCustomBounds = 1.0; // &1, used only if renderTail se
// enabled the render would obey it, breaking bit-identical repeats. Force off.
constexpr double kDitherDisableAll = 16.0;
// RENDER_NORMALIZE disable-all: &(4<<16) = disable all render postprocessing.
// Verified: SDK header line ~3051: "(&(4<<16))==disable all render postprocessing".
// This masks out normalization, brickwall, fades, pad/trim — every post-process
// that is nondeterministic relative to the source signal.
constexpr double kNormalizeDisableAll = static_cast<double>(4 << 16); // 262144
// --- WAV render sink configuration ------------------------------------------
//
// FORMAT CHOICE (CONTEXT.md open question — surfaced for Daniel to confirm):
@@ -147,6 +140,7 @@ struct RenderSettingsSnapshot {
double addToProj = 0.0;
double dither = 0.0; // RENDER_DITHER — snapshotted so user's setting is restored
double normalize = 0.0; // RENDER_NORMALIZE — snapshotted so user's setting is restored
double trimEnd = 0.0; // RENDER_TRIMEND — snapshotted so the Auto trim threshold is restored
// String settings (GetSetProjectInfo_String). Big buffers: REAPER writes the
// full value in, and RENDER_FORMAT is a base64 blob that can be long.
@@ -183,6 +177,7 @@ void snapshotRenderSettings(RenderSettingsSnapshot& s, ReaProject* proj) {
s.addToProj = GetSetProjectInfo(proj, "RENDER_ADDTOPROJ", 0.0, false);
s.dither = GetSetProjectInfo(proj, "RENDER_DITHER", 0.0, false);
s.normalize = GetSetProjectInfo(proj, "RENDER_NORMALIZE", 0.0, false);
s.trimEnd = GetSetProjectInfo(proj, "RENDER_TRIMEND", 0.0, false);
s.renderFile = getProjString(proj, "RENDER_FILE");
s.renderPattern = getProjString(proj, "RENDER_PATTERN");
s.renderFormat = getProjString(proj, "RENDER_FORMAT");
@@ -207,6 +202,7 @@ void restoreRenderSettings(const RenderSettingsSnapshot& s) {
GetSetProjectInfo(s.proj, "RENDER_ADDTOPROJ", s.addToProj, true);
GetSetProjectInfo(s.proj, "RENDER_DITHER", s.dither, true);
GetSetProjectInfo(s.proj, "RENDER_NORMALIZE", s.normalize, true);
GetSetProjectInfo(s.proj, "RENDER_TRIMEND", s.trimEnd, true);
}
// RAII wrapper: guarantees restore on every return path from capture().
@@ -353,13 +349,18 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
GetSetProjectInfo(proj, "RENDER_STARTPOS", request.startSeconds, true);
GetSetProjectInfo(proj, "RENDER_ENDPOS", request.endSeconds, true);
if (request.renderTail) {
GetSetProjectInfo(proj, "RENDER_TAILFLAG", kTailFlagCustomBounds, true);
GetSetProjectInfo(proj, "RENDER_TAILMS", request.tailMs, true);
} else {
GetSetProjectInfo(proj, "RENDER_TAILFLAG", kTailFlagNone, true);
GetSetProjectInfo(proj, "RENDER_TAILMS", 0.0, true);
}
// Tail: TAILFLAG / TAILMS / NORMALIZE / TRIMEND all come from the pure mapping
// (render_settings.h, unit-tested). None -> exact bounds + disable-all normalize
// (byte-identical to the pre-tail path); Auto -> 8 s tail + surgical trim-end
// normalize + -72 dB TRIMEND; Manual -> clamped fixed tail + disable-all, no trim.
// RENDER_NORMALIZE is driven HERE from the mapping (not the determinism block
// below) so the Auto surgical value is not clobbered — the snapshot guard restores
// the user's original RENDER_NORMALIZE / RENDER_TRIMEND on every exit path.
const TailRenderSettings tail =
tailRenderSettingsFor(request.tailMode, request.tailMs);
GetSetProjectInfo(proj, "RENDER_TAILFLAG",
static_cast<double>(tail.tailFlag), true);
GetSetProjectInfo(proj, "RENDER_TAILMS", tail.tailMs, true);
// Source-selection bits for this mode, from the pure render_settings mapping
// (verified against SDK header ~3041). All M7 actions are wet-only:
@@ -394,13 +395,20 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
// item. Clearing RENDER_ADDTOPROJ&1 keeps capture out of the arrange.
GetSetProjectInfo(proj, "RENDER_ADDTOPROJ", 0.0, true);
// Determinism: disable dither and all render post-processing so identical
// inputs produce bit-identical files and a dry capture nulls to silence.
// RENDER_DITHER &16 = disable all dither/noise-shaping (SDK header line ~3050).
// RENDER_NORMALIZE &(4<<16) = disable all render postprocessing (line ~3051).
// Both are snapshotted above and restored by the RAII guard on every path.
// Determinism: disable dither so identical inputs produce bit-identical files
// and a dry capture nulls to silence. RENDER_DITHER &16 = disable all dither/
// noise-shaping (SDK header line ~3050). Snapshotted above; restored by the guard.
GetSetProjectInfo(proj, "RENDER_DITHER", kDitherDisableAll, true);
GetSetProjectInfo(proj, "RENDER_NORMALIZE", kNormalizeDisableAll, true);
// RENDER_NORMALIZE + RENDER_TRIMEND come from the tail mapping (above). None /
// Manual -> disable-all (byte-identical to the pre-tail path); Auto -> surgical
// trim-end (only &32768) + the -72 dB TRIMEND. A fixed-threshold trailing-silence
// trim scales/limits/fades nothing, so Auto stays deterministic and un-coloring
// (spec §surgical normalize). TRIMEND is only consulted when the trim bit is set,
// but we write it unconditionally (harmless when clear) so the value is explicit.
GetSetProjectInfo(proj, "RENDER_NORMALIZE",
static_cast<double>(tail.normalize), true);
GetSetProjectInfo(proj, "RENDER_TRIMEND", tail.trimEnd, true);
// Output location: directory (RENDER_FILE) + file stem (RENDER_PATTERN).
// RENDER_PATTERN with no wildcards is a literal stem; REAPER appends the