refactor(capture): expose offline tail as docked-panel toggle

Replace the ...-with-tail variant actions with a pure tail_control module +
a docked-panel footer toggle (Off/Auto/Manual) read by CAPTURE_ITEM/TRACK.
Default None (byte-identical); Manual fixed 2s, in-memory session lifetime.
Tail mechanism unchanged; retired the variant ids.
This commit is contained in:
2026-07-23 17:32:19 -04:00
parent 2d225258b4
commit 9a9339b90b
11 changed files with 363 additions and 90 deletions
+21 -34
View File
@@ -216,17 +216,16 @@ static void testTrackScopeKeepsSelfBypassesAncestorsAndMaster() {
CHECK(p.bypassMaster); // no master FX
}
// --- captureActionTable: the scope x tail-variant taxonomy -------------------
// --- captureActionTable: the scope taxonomy ----------------------------------
static void testTableHasScopeAndTailVariants() {
static void testTableHasBothScopes() {
const auto& table = captureActionTable();
// Four rows: item/track x (None exact-bounds, Auto "…with tail"). No master scope.
CHECK(table.size() == 4);
// 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;
// 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;
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).
@@ -235,38 +234,26 @@ static void testTableHasScopeAndTailVariants() {
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 && 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;
if (def.scope == CaptureScope::Item) ++item;
if (def.scope == CaptureScope::Track) ++track;
}
// 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);
// Exactly one row per scope — no dupes, no gaps, no tail variants.
CHECK(item == 1);
CHECK(track == 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).
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 itemNoneId, itemAutoId, trackNoneId, trackAutoId;
std::string itemId, trackId;
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;
if (def.scope == CaptureScope::Item) itemId = def.commandString;
if (def.scope == CaptureScope::Track) trackId = 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");
CHECK(itemId == "CEREBELLUM_REASAMPLER_CAPTURE_ITEM");
CHECK(trackId == "CEREBELLUM_REASAMPLER_CAPTURE_TRACK");
}
int main() {
@@ -289,8 +276,8 @@ int main() {
testRangeInference();
testItemScopeBypassesEverythingButTake();
testTrackScopeKeepsSelfBypassesAncestorsAndMaster();
testTableHasScopeAndTailVariants();
testTailVariantIdsAreDistinctFromExactRows();
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);
+83
View File
@@ -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;
}