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
+75 -11
View File
@@ -3,10 +3,59 @@
#include "render_settings.h"
#include <algorithm>
#include <cmath>
#include <sstream>
namespace reasampler {
double autoTrimEndRatio() {
// Amplitude ratio = 10^(dB/20). Derived from kAutoTrimThresholdDb so the dB is
// the single source of truth (header ~3062: RENDER_TRIMEND is an amplitude ratio,
// "0.5 means -6.02 dB"). For -72 dB this is ~= 0.00025119.
return std::pow(10.0, kAutoTrimThresholdDb / 20.0);
}
TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs) {
TailRenderSettings t;
switch (mode) {
case TailMode::None:
// Exact bounds — byte-identical to the pre-tail no-tail capture. Tail off,
// disable-all normalize (the current default), no trim.
t.tailFlag = kTailFlagNone;
t.tailMs = 0.0;
t.normalize = kNormalizeDisableAll;
t.trimEnd = 0.0;
return t;
case TailMode::Auto:
// Generous 8 s tail, then SURGICAL normalize: ONLY the trim-ending-silence
// bit (32768) — every other postprocessing bit clear. A fixed-threshold
// trailing-silence trim is a pure boundary decision (it scales/limits/fades
// nothing), so it re-introduces none of the coloring the disable-all bit
// guarded against, and two identical requests trim at the identical sample
// -> bit-identical repeats hold (spec §surgical normalize).
t.tailFlag = kTailFlagCustomBounds;
t.tailMs = kMaxTailMs;
t.normalize = kNormalizeTrimEnd;
t.trimEnd = autoTrimEndRatio();
return t;
case TailMode::Manual:
// Fixed tail, no trim -> keep the disable-all normalize exactly as the
// no-tail path does. Clamp to the 8 s cap even here: the runaway guard
// applies whether the length came from the Auto default or an explicit
// request (spec §Manual override). Negative requests floor to 0.
t.tailFlag = kTailFlagCustomBounds;
t.tailMs = std::clamp(manualTailMs, 0.0, kMaxTailMs);
t.normalize = kNormalizeDisableAll;
t.trimEnd = 0.0;
return t;
}
// Unreachable for a valid enum; fail closed to exact bounds (never a stray tail).
return t;
}
RenderSettingsChoice renderSettingsFor(SourceMode mode, double /*wetDry*/) {
// `wetDry` is accepted so CaptureRequest.wetDry remains the seam for future
// dry work (M10 null test), but it does not affect this mapping. FX scoping is
@@ -130,23 +179,38 @@ RazorRange razorUnionBounds(const std::vector<RazorRange>& ranges) {
}
const std::vector<CaptureActionDef>& captureActionTable() {
// Built once (function-local static): two SCOPE actions. Tail OFF for all
// (exact bounds). Ids are FOREVER-STABLE — never edit a shipped string. Each
// action infers its range (razor-else-time) at fire time and enforces its
// FX-scope invariant via fxBypassPlanFor. The M7 CAPTURE_TRACKS_WET /
// CAPTURE_ITEMS_WET / CAPTURE_RAZOR_WET ids are RETIRED (mirror-unregistered in
// main.cpp); the CAPTURE_MASTER scope action is REMOVED (its id is likewise
// mirror-unregistered) — to capture the master you render a track.
// Built once (function-local static): two SCOPE actions x two tail variants.
// The None rows are exact bounds (byte-identical to today); the …_TAIL rows are
// the paired TailMode::Auto "…with tail" variants (Daniel's lean over silently
// flipping the exact-bounds default — spec §Open questions). Ids are FOREVER-
// STABLE — never edit a shipped string. Each action infers its range
// (razor-else-time) at fire time and enforces its FX-scope invariant via
// fxBypassPlanFor. The M7 CAPTURE_TRACKS_WET / CAPTURE_ITEMS_WET /
// CAPTURE_RAZOR_WET ids are RETIRED (mirror-unregistered in main.cpp); the
// CAPTURE_MASTER scope action is REMOVED (its id is likewise mirror-
// unregistered) — to capture the master you render a track.
static const std::vector<CaptureActionDef> table = {
// Item scope — item/take FX only. NEW forever-stable id.
// Item scope, exact bounds — item/take FX only.
{"CEREBELLUM_REASAMPLER_CAPTURE_ITEM",
"ReaSampler: capture selected item(s)", "item",
CaptureScope::Item},
CaptureScope::Item, TailMode::None},
// Track scope — item FX + the track's own FX. NEW forever-stable id.
// Track scope, exact bounds — item FX + the track's own FX.
{"CEREBELLUM_REASAMPLER_CAPTURE_TRACK",
"ReaSampler: capture selected track(s)", "track",
CaptureScope::Track},
CaptureScope::Track, TailMode::None},
// Item scope with Auto tail — captures the take-FX decay past the range end,
// trimmed to -72 dB. NEW forever-stable id.
{"CEREBELLUM_REASAMPLER_CAPTURE_ITEM_TAIL",
"ReaSampler: capture selected item(s) with tail", "item",
CaptureScope::Item, TailMode::Auto},
// Track scope with Auto tail — captures the track's own reverb/delay decay
// past the range end, trimmed to -72 dB. NEW forever-stable id.
{"CEREBELLUM_REASAMPLER_CAPTURE_TRACK_TAIL",
"ReaSampler: capture selected track(s) with tail", "track",
CaptureScope::Track, TailMode::Auto},
};
return table;
}