Merge dev (tail-toggle panel refactor) into D2 Wave 2
# Conflicts: # src/bank_panel.cpp
This commit is contained in:
+101
-14
@@ -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,16 @@ static void testTrackScopeKeepsSelfBypassesAncestorsAndMaster() {
|
||||
CHECK(p.bypassMaster); // no master FX
|
||||
}
|
||||
|
||||
// --- captureActionTable: the two-scope taxonomy ------------------------------
|
||||
// --- captureActionTable: the scope taxonomy ----------------------------------
|
||||
|
||||
static void testTableHasTwoScopeRows() {
|
||||
static void testTableHasBothScopes() {
|
||||
const auto& table = captureActionTable();
|
||||
// Exactly 2 scope rows: item, track. There is no master scope.
|
||||
// Two rows: item + track. No master scope, and NO tail variants — tail is a
|
||||
// panel setting the capture reads at fire time, not a per-action row.
|
||||
CHECK(table.size() == 2);
|
||||
|
||||
std::set<std::string> ids;
|
||||
bool sawItem = false, sawTrack = false;
|
||||
int item = 0, track = 0;
|
||||
for (const auto& def : table) {
|
||||
// Every id is a non-empty CEREBELLUM_REASAMPLER_ string and is UNIQUE
|
||||
// (duplicate ids would collide on registration).
|
||||
@@ -168,11 +235,25 @@ static void testTableHasTwoScopeRows() {
|
||||
// Every scope resolves to a supported offline source.
|
||||
CHECK(renderSettingsFor(sourceModeForScope(def.scope), 1.0).supported);
|
||||
|
||||
if (def.scope == CaptureScope::Item) sawItem = true;
|
||||
if (def.scope == CaptureScope::Track) sawTrack = true;
|
||||
if (def.scope == CaptureScope::Item) ++item;
|
||||
if (def.scope == CaptureScope::Track) ++track;
|
||||
}
|
||||
CHECK(sawItem);
|
||||
CHECK(sawTrack);
|
||||
// Exactly one row per scope — no dupes, no gaps, no tail variants.
|
||||
CHECK(item == 1);
|
||||
CHECK(track == 1);
|
||||
}
|
||||
|
||||
static void testScopeActionIdsAreTheShippedStrings() {
|
||||
// Pin the shipped CAPTURE_ITEM / CAPTURE_TRACK ids so a future edit that silently
|
||||
// changes them (breaking user keybindings) fails the gate.
|
||||
const auto& table = captureActionTable();
|
||||
std::string itemId, trackId;
|
||||
for (const auto& def : table) {
|
||||
if (def.scope == CaptureScope::Item) itemId = def.commandString;
|
||||
if (def.scope == CaptureScope::Track) trackId = def.commandString;
|
||||
}
|
||||
CHECK(itemId == "CEREBELLUM_REASAMPLER_CAPTURE_ITEM");
|
||||
CHECK(trackId == "CEREBELLUM_REASAMPLER_CAPTURE_TRACK");
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -181,6 +262,11 @@ int main() {
|
||||
testSelectedItemsSingleFile();
|
||||
testRazorSingleFile();
|
||||
testRealtimeIsUnsupportedOffline();
|
||||
testTailNoneIsExactBounds();
|
||||
testTailAutoIsSurgicalTrim();
|
||||
testAutoTrimRatioDerivesFromDb();
|
||||
testTailManualFixedNoTrim();
|
||||
testTailManualClampsToCap();
|
||||
testParseSingleTrackAudioArea();
|
||||
testParseMultipleAreas();
|
||||
testParseSkipsEnvelopeLaneAreas();
|
||||
@@ -190,7 +276,8 @@ int main() {
|
||||
testRangeInference();
|
||||
testItemScopeBypassesEverythingButTake();
|
||||
testTrackScopeKeepsSelfBypassesAncestorsAndMaster();
|
||||
testTableHasTwoScopeRows();
|
||||
testTableHasBothScopes();
|
||||
testScopeActionIdsAreTheShippedStrings();
|
||||
|
||||
if (g_fail == 0) std::printf("render_settings: all tests passed\n");
|
||||
else std::printf("render_settings: %d CHECK(s) FAILED\n", g_fail);
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// Standalone tests for reasampler::tail_control — no REAPER, no framework. Covers
|
||||
// the pure pieces behind the docked panel's tail-mode toggle: the cycle order
|
||||
// (None -> Auto -> Manual -> None), the manual-length clamp to the 8 s cap, and the
|
||||
// toggle label text. The drawing / click hit-testing is DAW-verified in bank_panel.
|
||||
|
||||
#include "../src/tail_control.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
using namespace reasampler;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
// --- cycleTailMode: the toggle order ------------------------------------------
|
||||
|
||||
static void testCycleOrderIsNoneAutoManualNone() {
|
||||
// None -> Auto -> Manual -> None, wrapping. The click handler relies on exactly
|
||||
// this order; a reorder (e.g. skipping Manual) would fail here.
|
||||
CHECK(cycleTailMode(TailMode::None) == TailMode::Auto);
|
||||
CHECK(cycleTailMode(TailMode::Auto) == TailMode::Manual);
|
||||
CHECK(cycleTailMode(TailMode::Manual) == TailMode::None);
|
||||
}
|
||||
|
||||
static void testCycleThreeStepsReturnsToStart() {
|
||||
// Three cycles from any state land back on that state (a full lap of the 3-cycle).
|
||||
TailMode m = TailMode::None;
|
||||
m = cycleTailMode(m);
|
||||
m = cycleTailMode(m);
|
||||
m = cycleTailMode(m);
|
||||
CHECK(m == TailMode::None);
|
||||
}
|
||||
|
||||
// --- clampManualMs: the runaway guard -----------------------------------------
|
||||
|
||||
static void testManualClampInRangeIsUnchanged() {
|
||||
// A value inside [0, kMaxTailMs] passes through untouched.
|
||||
CHECK(clampManualMs(kDefaultManualTailMs) == kDefaultManualTailMs);
|
||||
CHECK(clampManualMs(0.0) == 0.0);
|
||||
CHECK(clampManualMs(kMaxTailMs) == kMaxTailMs);
|
||||
}
|
||||
|
||||
static void testManualClampCapsAtEightSeconds() {
|
||||
// Over the 8 s cap clamps to kMaxTailMs; negative floors to 0. This mirrors the
|
||||
// clamp in tailRenderSettingsFor, so the panel and the render agree on the bound.
|
||||
CHECK(clampManualMs(kMaxTailMs + 5000.0) == kMaxTailMs);
|
||||
CHECK(clampManualMs(-100.0) == 0.0);
|
||||
}
|
||||
|
||||
// --- tailToggleLabel: the exact strings the panel draws -----------------------
|
||||
|
||||
static void testLabelStringsPerMode() {
|
||||
TailSetting off; off.mode = TailMode::None;
|
||||
TailSetting autoM; autoM.mode = TailMode::Auto;
|
||||
TailSetting man; man.mode = TailMode::Manual;
|
||||
CHECK(tailToggleLabel(off) == "Tail: Off");
|
||||
CHECK(tailToggleLabel(autoM) == "Tail: Auto");
|
||||
CHECK(tailToggleLabel(man) == "Tail: Manual");
|
||||
}
|
||||
|
||||
static void testDefaultSettingIsOff() {
|
||||
// The zero-value setting is None (Off) with the 2 s manual default — the safe
|
||||
// default the panel starts in so captures stay exact-bounds until opt-in.
|
||||
TailSetting s;
|
||||
CHECK(s.mode == TailMode::None);
|
||||
CHECK(s.manualMs == kDefaultManualTailMs);
|
||||
CHECK(tailToggleLabel(s) == "Tail: Off");
|
||||
}
|
||||
|
||||
int main() {
|
||||
testCycleOrderIsNoneAutoManualNone();
|
||||
testCycleThreeStepsReturnsToStart();
|
||||
testManualClampInRangeIsUnchanged();
|
||||
testManualClampCapsAtEightSeconds();
|
||||
testLabelStringsPerMode();
|
||||
testDefaultSettingIsOff();
|
||||
|
||||
if (g_fail == 0) std::printf("tail_control: all tests passed\n");
|
||||
else std::printf("tail_control: %d CHECK(s) FAILED\n", g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user