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
+115 -15
View File
@@ -1,12 +1,14 @@
// Standalone tests for reasampler::render_settings — no REAPER, no framework.
// Covers the pure pieces behind the two-scope capture family: the source-mode ->
// RENDER_SETTINGS bit mapping, P_RAZOREDITS parsing -> ranges + union, scope ->
// source mode, range inference (razor-else-time), the FX-bypass plan (corrects
// the "items captured through parent FX" defect), and the capture-action taxonomy
// table (stable ids, one row per scope).
// Covers the pure pieces behind the capture family: the source-mode ->
// RENDER_SETTINGS bit mapping, the TailMode -> RENDER_* (tail/normalize/trim-end)
// mapping + the -72 dB derived ratio + the 8 s manual clamp, P_RAZOREDITS parsing
// -> ranges + union, scope -> source mode, range inference (razor-else-time), the
// FX-bypass plan (corrects the "items captured through parent FX" defect), and the
// capture-action taxonomy table (stable ids, scope x tail-variant matrix).
#include "../src/render_settings.h"
#include <cmath>
#include <cstdio>
#include <set>
#include <string>
@@ -60,6 +62,70 @@ static void testRealtimeIsUnsupportedOffline() {
CHECK(!renderSettingsFor(SourceMode::Realtime, 1.0).supported);
}
// --- tail: TailMode -> RENDER_* mapping (docs/product/capture-tail.md) --------
static void testTailNoneIsExactBounds() {
// None -> exact bounds, byte-identical to the pre-tail capture: tail flag clear,
// 0 ms, disable-all normalize (the current default), no trim. Asserting the exact
// bit values (not just "some value") pins the byte-identical contract: if the
// mapping regressed to set a tail bit or a non-disable-all normalize, this fails.
TailRenderSettings t = tailRenderSettingsFor(TailMode::None, 0.0);
CHECK(t.tailFlag == kTailFlagNone); // 0
CHECK(t.tailMs == 0.0);
CHECK(t.normalize == kNormalizeDisableAll); // 262144
CHECK(t.trimEnd == 0.0);
// manualTailMs must be ignored for None (a stray tail from a leftover ms is the bug).
TailRenderSettings t2 = tailRenderSettingsFor(TailMode::None, 5000.0);
CHECK(t2.tailFlag == kTailFlagNone);
CHECK(t2.tailMs == 0.0);
}
static void testTailAutoIsSurgicalTrim() {
// Auto -> custom-bounds tail bit, 8 s cap, SURGICAL normalize (ONLY &32768), and
// the -72 dB TRIMEND ratio. The disable-all bit must NOT be set (it is semantically
// opposed to trim — this assertion catches a regression to the None normalize).
TailRenderSettings t = tailRenderSettingsFor(TailMode::Auto, 0.0);
CHECK(t.tailFlag == kTailFlagCustomBounds); // &1
CHECK(t.tailMs == kMaxTailMs); // 8000
CHECK(t.normalize == kNormalizeTrimEnd); // exactly 32768, nothing else
CHECK((t.normalize & kNormalizeDisableAll) == 0); // disable-all is NOT set
// TRIMEND is the derived -72 dB ratio ~= 0.00025119 (the DAW-confirm value).
CHECK(std::fabs(t.trimEnd - 0.00025119) < 1e-8);
// manualTailMs is ignored for Auto (Auto always uses the 8 s cap).
CHECK(tailRenderSettingsFor(TailMode::Auto, 3000.0).tailMs == kMaxTailMs);
}
static void testAutoTrimRatioDerivesFromDb() {
// The ratio must DERIVE from the -72 dB constant (10^(dB/20)), not be a hardcoded
// float — recompute it independently and require an exact match with the mapping.
double expected = std::pow(10.0, kAutoTrimThresholdDb / 20.0);
CHECK(autoTrimEndRatio() == expected);
CHECK(tailRenderSettingsFor(TailMode::Auto, 0.0).trimEnd == expected);
// Sanity: -72 dB is well below unity but above zero.
CHECK(expected > 0.0 && expected < 0.001);
}
static void testTailManualFixedNoTrim() {
// Manual -> custom-bounds tail, the requested ms (within cap), disable-all
// normalize (no trim). A Manual capture is a fixed tail, so it keeps today's
// disable-all exactly like the no-tail path.
TailRenderSettings t = tailRenderSettingsFor(TailMode::Manual, 2500.0);
CHECK(t.tailFlag == kTailFlagCustomBounds);
CHECK(t.tailMs == 2500.0);
CHECK(t.normalize == kNormalizeDisableAll);
CHECK(t.trimEnd == 0.0);
}
static void testTailManualClampsToCap() {
// The 8 s cap is a runaway guard that applies to Manual too: ms > 8000 -> 8000.
CHECK(tailRenderSettingsFor(TailMode::Manual, 9000.0).tailMs == kMaxTailMs);
CHECK(tailRenderSettingsFor(TailMode::Manual, 8000.0).tailMs == kMaxTailMs);
// Below the cap is passed through unchanged.
CHECK(tailRenderSettingsFor(TailMode::Manual, 100.0).tailMs == 100.0);
// A negative request floors to 0 (no negative tail leaks into RENDER_TAILMS).
CHECK(tailRenderSettingsFor(TailMode::Manual, -50.0).tailMs == 0.0);
}
// --- parseRazorEdits: P_RAZOREDITS string -> ranges --------------------------
static void testParseSingleTrackAudioArea() {
@@ -150,15 +216,17 @@ static void testTrackScopeKeepsSelfBypassesAncestorsAndMaster() {
CHECK(p.bypassMaster); // no master FX
}
// --- captureActionTable: the two-scope taxonomy ------------------------------
// --- captureActionTable: the scope x tail-variant taxonomy -------------------
static void testTableHasTwoScopeRows() {
static void testTableHasScopeAndTailVariants() {
const auto& table = captureActionTable();
// Exactly 2 scope rows: item, track. There is no master scope.
CHECK(table.size() == 2);
// Four rows: item/track x (None exact-bounds, Auto "…with tail"). No master scope.
CHECK(table.size() == 4);
std::set<std::string> ids;
bool sawItem = false, sawTrack = false;
// Count each (scope, tailMode) pairing so we assert the FULL matrix is present,
// not merely that some item + some track row exist.
int itemNone = 0, itemAuto = 0, trackNone = 0, trackAuto = 0;
for (const auto& def : table) {
// Every id is a non-empty CEREBELLUM_REASAMPLER_ string and is UNIQUE
// (duplicate ids would collide on registration).
@@ -167,12 +235,38 @@ static void testTableHasTwoScopeRows() {
CHECK(ids.insert(id).second); // false if duplicate
// Every scope resolves to a supported offline source.
CHECK(renderSettingsFor(sourceModeForScope(def.scope), 1.0).supported);
// Tail variants only ever ship None or Auto (Manual has no dedicated action yet).
CHECK(def.tailMode == TailMode::None || def.tailMode == TailMode::Auto);
if (def.scope == CaptureScope::Item) sawItem = true;
if (def.scope == CaptureScope::Track) sawTrack = true;
if (def.scope == CaptureScope::Item && def.tailMode == TailMode::None) ++itemNone;
if (def.scope == CaptureScope::Item && def.tailMode == TailMode::Auto) ++itemAuto;
if (def.scope == CaptureScope::Track && def.tailMode == TailMode::None) ++trackNone;
if (def.scope == CaptureScope::Track && def.tailMode == TailMode::Auto) ++trackAuto;
}
CHECK(sawItem);
CHECK(sawTrack);
// Exactly one row per (scope, tail) cell — the full 2x2 matrix, no dupes/gaps.
CHECK(itemNone == 1);
CHECK(itemAuto == 1);
CHECK(trackNone == 1);
CHECK(trackAuto == 1);
}
static void testTailVariantIdsAreDistinctFromExactRows() {
// The …_TAIL variants must be NEW forever-stable ids, not a rename of the exact
// rows (renaming would break shipped keybindings on the exact-bounds actions).
const auto& table = captureActionTable();
std::string itemNoneId, itemAutoId, trackNoneId, trackAutoId;
for (const auto& def : table) {
if (def.scope == CaptureScope::Item && def.tailMode == TailMode::None) itemNoneId = def.commandString;
if (def.scope == CaptureScope::Item && def.tailMode == TailMode::Auto) itemAutoId = def.commandString;
if (def.scope == CaptureScope::Track && def.tailMode == TailMode::None) trackNoneId = def.commandString;
if (def.scope == CaptureScope::Track && def.tailMode == TailMode::Auto) trackAutoId = def.commandString;
}
// The exact-bounds ids are the shipped CAPTURE_ITEM / CAPTURE_TRACK strings —
// pin them so a future edit that silently changes them fails the gate.
CHECK(itemNoneId == "CEREBELLUM_REASAMPLER_CAPTURE_ITEM");
CHECK(trackNoneId == "CEREBELLUM_REASAMPLER_CAPTURE_TRACK");
CHECK(itemAutoId == "CEREBELLUM_REASAMPLER_CAPTURE_ITEM_TAIL");
CHECK(trackAutoId == "CEREBELLUM_REASAMPLER_CAPTURE_TRACK_TAIL");
}
int main() {
@@ -181,6 +275,11 @@ int main() {
testSelectedItemsSingleFile();
testRazorSingleFile();
testRealtimeIsUnsupportedOffline();
testTailNoneIsExactBounds();
testTailAutoIsSurgicalTrim();
testAutoTrimRatioDerivesFromDb();
testTailManualFixedNoTrim();
testTailManualClampsToCap();
testParseSingleTrackAudioArea();
testParseMultipleAreas();
testParseSkipsEnvelopeLaneAreas();
@@ -190,7 +289,8 @@ int main() {
testRangeInference();
testItemScopeBypassesEverythingButTake();
testTrackScopeKeepsSelfBypassesAncestorsAndMaster();
testTableHasTwoScopeRows();
testTableHasScopeAndTailVariants();
testTailVariantIdsAreDistinctFromExactRows();
if (g_fail == 0) std::printf("render_settings: all tests passed\n");
else std::printf("render_settings: %d CHECK(s) FAILED\n", g_fail);