Add tail manual fine-adjust (scroll) + per-project tail persistence
Relocate TailSetting into ReaSamplerSession; persist serializes it to a new forever-stable "tail_setting" ext-state key (load-per-project, save-on-save). Footer scroll-wheel adjusts Manual length in 250ms steps; label now shows "Tail: Manual X.Xs". Pure step/label/JSON round-trip covered by tests.
This commit is contained in:
@@ -49,15 +49,53 @@ static void testManualClampCapsAtEightSeconds() {
|
||||
CHECK(clampManualMs(-100.0) == 0.0);
|
||||
}
|
||||
|
||||
// --- adjustManualMs: the scroll-wheel fine-adjust arithmetic ------------------
|
||||
|
||||
static void testAdjustUpAndDownBySteps() {
|
||||
// Positive notches lengthen, negative shorten, in whole kManualStepMs increments.
|
||||
CHECK(adjustManualMs(2000.0, 1, kManualStepMs) == 2250.0);
|
||||
CHECK(adjustManualMs(2000.0, -1, kManualStepMs) == 1750.0);
|
||||
CHECK(adjustManualMs(2000.0, 4, kManualStepMs) == 3000.0); // 4 * 250
|
||||
CHECK(adjustManualMs(2000.0, 0, kManualStepMs) == 2000.0); // no notch, no move
|
||||
}
|
||||
|
||||
static void testAdjustClampsAtUpperBound() {
|
||||
// Scrolling up past the 8 s cap saturates AT the cap, never beyond.
|
||||
CHECK(adjustManualMs(kMaxTailMs, 1, kManualStepMs) == kMaxTailMs);
|
||||
CHECK(adjustManualMs(kMaxTailMs - 100.0, 10, kManualStepMs) == kMaxTailMs);
|
||||
}
|
||||
|
||||
static void testAdjustClampsAtLowerBound() {
|
||||
// Scrolling down past 0 floors at 0, never negative.
|
||||
CHECK(adjustManualMs(0.0, -1, kManualStepMs) == 0.0);
|
||||
CHECK(adjustManualMs(100.0, -10, kManualStepMs) == 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;
|
||||
// Off/Auto carry NO length regardless of manualMs.
|
||||
off.manualMs = 5000.0;
|
||||
autoM.manualMs = 5000.0;
|
||||
CHECK(tailToggleLabel(off) == "Tail: Off");
|
||||
CHECK(tailToggleLabel(autoM) == "Tail: Auto");
|
||||
CHECK(tailToggleLabel(man) == "Tail: Manual");
|
||||
}
|
||||
|
||||
static void testManualLabelRendersLengthInSeconds() {
|
||||
// Manual appends the length in seconds to one decimal — pin the format and the
|
||||
// boundary values (0.0s, the 2 s default, the 8 s cap).
|
||||
TailSetting man; man.mode = TailMode::Manual;
|
||||
man.manualMs = 0.0;
|
||||
CHECK(tailToggleLabel(man) == "Tail: Manual 0.0s");
|
||||
man.manualMs = kDefaultManualTailMs; // 2000 ms
|
||||
CHECK(tailToggleLabel(man) == "Tail: Manual 2.0s");
|
||||
man.manualMs = kMaxTailMs; // 8000 ms
|
||||
CHECK(tailToggleLabel(man) == "Tail: Manual 8.0s");
|
||||
// An over-cap stored value renders at the CLAMPED length, never past the cap.
|
||||
man.manualMs = kMaxTailMs + 3000.0;
|
||||
CHECK(tailToggleLabel(man) == "Tail: Manual 8.0s");
|
||||
}
|
||||
|
||||
static void testDefaultSettingIsOff() {
|
||||
@@ -69,13 +107,65 @@ static void testDefaultSettingIsOff() {
|
||||
CHECK(tailToggleLabel(s) == "Tail: Off");
|
||||
}
|
||||
|
||||
// --- serialize/deserialize: per-project persistence round-trip ----------------
|
||||
|
||||
static bool settingsEqual(const TailSetting& a, const TailSetting& b) {
|
||||
return a.mode == b.mode && a.manualMs == b.manualMs;
|
||||
}
|
||||
|
||||
static void testRoundTripNoneDefault() {
|
||||
TailSetting s; // None + 2 s default
|
||||
auto back = deserializeTailSetting(serializeTailSetting(s));
|
||||
CHECK(back.has_value());
|
||||
CHECK(back && settingsEqual(*back, s));
|
||||
}
|
||||
|
||||
static void testRoundTripManualArbitraryMs() {
|
||||
// A non-round manual length must round-trip bit-for-bit (17-sig-digit emit).
|
||||
TailSetting s; s.mode = TailMode::Manual; s.manualMs = 3141.592653589793;
|
||||
auto back = deserializeTailSetting(serializeTailSetting(s));
|
||||
CHECK(back.has_value());
|
||||
CHECK(back && settingsEqual(*back, s));
|
||||
}
|
||||
|
||||
static void testRoundTripAuto() {
|
||||
TailSetting s; s.mode = TailMode::Auto; s.manualMs = 500.0;
|
||||
auto back = deserializeTailSetting(serializeTailSetting(s));
|
||||
CHECK(back.has_value());
|
||||
CHECK(back && settingsEqual(*back, s));
|
||||
}
|
||||
|
||||
static void testDeserializeEmptyIsDefault() {
|
||||
// An absent/empty stored value (older project) -> nullopt, so the caller falls
|
||||
// back to the default. This is the graceful-old-project path the brief requires.
|
||||
CHECK(!deserializeTailSetting("").has_value());
|
||||
}
|
||||
|
||||
static void testDeserializeMalformedIsDefault() {
|
||||
// Garbage, a missing key, or an unknown mode enumerant -> nullopt (no crash).
|
||||
CHECK(!deserializeTailSetting("not json at all").has_value());
|
||||
CHECK(!deserializeTailSetting("{\"mode\":1}").has_value()); // manualMs missing
|
||||
CHECK(!deserializeTailSetting("{\"manualMs\":2000}").has_value()); // mode missing
|
||||
CHECK(!deserializeTailSetting("{\"mode\":9,\"manualMs\":2000}").has_value()); // bad enum
|
||||
CHECK(!deserializeTailSetting("{\"mode\":x,\"manualMs\":2000}").has_value()); // non-numeric
|
||||
}
|
||||
|
||||
int main() {
|
||||
testCycleOrderIsNoneAutoManualNone();
|
||||
testCycleThreeStepsReturnsToStart();
|
||||
testManualClampInRangeIsUnchanged();
|
||||
testManualClampCapsAtEightSeconds();
|
||||
testAdjustUpAndDownBySteps();
|
||||
testAdjustClampsAtUpperBound();
|
||||
testAdjustClampsAtLowerBound();
|
||||
testLabelStringsPerMode();
|
||||
testManualLabelRendersLengthInSeconds();
|
||||
testDefaultSettingIsOff();
|
||||
testRoundTripNoneDefault();
|
||||
testRoundTripManualArbitraryMs();
|
||||
testRoundTripAuto();
|
||||
testDeserializeEmptyIsDefault();
|
||||
testDeserializeMalformedIsDefault();
|
||||
|
||||
if (g_fail == 0) std::printf("tail_control: all tests passed\n");
|
||||
else std::printf("tail_control: %d CHECK(s) FAILED\n", g_fail);
|
||||
|
||||
Reference in New Issue
Block a user