// Standalone tests for reasampler::instrument::ui::param_taper — no VST3, no REAPER, no // framework. The taper is the one map the knob's needle, the AHDSR schematic axis and (later) the // host's normalization all read, so what is asserted here is what all three obey. // // Covers: the modifier truth table (Shift beats Ctrl); the stage-time taper (exact endpoints, // monotone, the two landmark bands, and the EXACT-PREIMAGE guarantee swept over the whole // quantum grid rather than sampled at the defaults); the depth taper (exact centre and ends, // exact symmetry, the +/-7 st landmark, whole-semitone preimages); and the four whole-unit snaps. #include "../src/core/instrument/ui/param_taper.h" #include "../src/core/instrument/ui/envelope_overlay.h" // gateStageSlotPx: finest drag surface #include "../src/core/instrument/ui/sample_bands.h" // kEditorMinWidth/kPad: the editor floor #include #include #include using namespace reasampler; using namespace reasampler::instrument::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) static constexpr double kDepth = 24.0; // the pitch-depth throw the deck passes in today // --- modifiers ----------------------------------------------------------------------------- // Shift+Ctrl is SHIFT: with the output quantized to whole units a finer drag produces the same // sequence, so Ctrl is ignored there. Asserted rather than left to a comment because the // "obvious fix" is to compound the two. static void testShiftBeatsCtrlForTheFineDragRate() { CHECK(!fineDrag(DragModifiers{false, false})); CHECK(fineDrag(DragModifiers{false, true})); CHECK(!fineDrag(DragModifiers{true, false})); CHECK(!fineDrag(DragModifiers{true, true})); CHECK((DragModifiers{true, false} != DragModifiers{false, false})); CHECK((DragModifiers{true, true} == DragModifiers{true, true})); } // --- the stage-time taper ------------------------------------------------------------------ // Zero is a REQUIRED value a pure log cannot express, and the ceiling has to be reachable by // hand — both endpoints are exact, not merely close. static void testStageTimeEndpointsAreExact() { CHECK(timeSecondsFromNorm(0.0) == 0.0); CHECK(timeSecondsFromNorm(1.0) == kStageTimeMaxSeconds); CHECK(timeNormFromSeconds(0.0) == 0.0); CHECK(timeNormFromSeconds(kStageTimeMaxSeconds) == 1.0); // Out of domain clamps rather than extrapolating. CHECK(timeSecondsFromNorm(-1.0) == 0.0); CHECK(timeSecondsFromNorm(2.0) == kStageTimeMaxSeconds); CHECK(timeNormFromSeconds(-1.0) == 0.0); CHECK(timeNormFromSeconds(1e9) == 1.0); } // The ceiling this phase raised it to. Pinned as a literal: this endpoint becomes a frozen host // normalization, so a silent change to it is exactly what a test has to refuse. static void testStageTimeCeilingIsTenSeconds() { CHECK(kStageTimeMaxSeconds == 10.0); } // The two landmarks the taper is fitted to, at the NEW ceiling. They are what make the low end // dialable at a 10 s range, and they are also the overlay's legibility guarantee. static void testStageTimeLandmarksLandInTheirBands() { const double at10ms = timeNormFromSeconds(0.010); const double at100ms = timeNormFromSeconds(0.100); CHECK(at10ms >= 0.12 && at10ms <= 0.20); CHECK(at100ms >= 0.42 && at100ms <= 0.52); // And the two are ordered with real separation, not merely inside their bands. CHECK(at100ms > at10ms + 0.2); } static void testStageTimeIsMonotone() { double prev = -1.0; for (int i = 0; i <= 200000; ++i) { const double v = timeSecondsFromNorm(static_cast(i) / 200000.0); CHECK(v >= prev); if (v < prev) return; // one report is enough prev = v; } } // The FINEST drag a user can make on ANY surface this taper serves — not the knob's own 128 px // travel, which is coarser than the AHDSR schematic's node drag at the editor floor. Derived from // the floor constant and the overlay's own slot-width formula, so a later floor change sharpens // (or coarsens) the step this test exercises automatically instead of leaving a copied number // silently stale. static void testEveryFinestDragStepMovesTheValue() { const Rect floorArea = Rect::ltrb(0, 0, kEditorMinWidth - 2 * kPad, 100); const double slot = gateStageSlotPx(floorArea); const int steps = static_cast(slot / kFineDragScale); for (int i = 0; i < steps; ++i) { const double lo = timeSecondsFromNorm(static_cast(i) / steps); const double hi = timeSecondsFromNorm(static_cast(i + 1) / steps); CHECK(hi > lo); if (!(hi > lo)) return; } } // THE sharpest requirement in the track. A host's reset-to-default arrives as // toPlain(defaultNorm) with no bypass available, so the preimage has to be EXACT. Swept over the // whole quantum grid at the resolution the defaults live at, not sampled at the two the parameter // set happens to carry today — that is what makes the guarantee structural. static void testEveryWholeMicrosecondRoundTripsExactly() { for (int us = 0; us <= 200000; us += 7) { // 0 .. 200 ms, a prime stride to avoid alignment const double seconds = static_cast(us) / 1e6; CHECK(timeSecondsFromNorm(timeNormFromSeconds(seconds)) == seconds); if (timeSecondsFromNorm(timeNormFromSeconds(seconds)) != seconds) return; } // And across the rest of the range, where the map is coarsest. for (int ms = 200; ms <= 10000; ms += 13) { const double seconds = static_cast(ms) / 1e3; CHECK(timeSecondsFromNorm(timeNormFromSeconds(seconds)) == seconds); if (timeSecondsFromNorm(timeNormFromSeconds(seconds)) != seconds) return; } } // MODE-INDEPENDENCE, the whole point of resolveTo's std::round over std::nearbyint. First shows // the defect directly, generically: under round-toward-zero, the RETIRED std::nearbyint reads // that mode and truncates a value whose fraction is well past half, while std::round (specified // to round half-away-from-zero REGARDLESS of the current mode) does not. Then proves the // production round trip itself — not a stand-in — survives the same hostile mode across the grid. static void testRoundingSurvivesAHostileFpRoundingMode() { const int saved = std::fegetround(); CHECK(std::fesetround(FE_TOWARDZERO) == 0); CHECK(std::nearbyint(12.9) == 12.0); // the RETIRED behaviour: mode-dependent, wrong here CHECK(std::round(12.9) == 13.0); // the fix: mode-independent, rounds to nearest for (int us = 0; us <= 200000; us += 7) { const double seconds = static_cast(us) / 1e6; CHECK(timeSecondsFromNorm(timeNormFromSeconds(seconds)) == seconds); if (timeSecondsFromNorm(timeNormFromSeconds(seconds)) != seconds) break; } for (int milli = -24000; milli <= 24000; milli += 37) { const double d = static_cast(milli) / 1000.0; CHECK(depthSemitonesFromNorm(depthNormFromSemitones(d, kDepth), kDepth) == d); if (depthSemitonesFromNorm(depthNormFromSemitones(d, kDepth), kDepth) != d) break; } std::fesetround(saved); // restore — every other test in this binary assumes the default } // The converse round trip is NOT required, but its residual is worth pinning: it is bounded by // the output quantum read back through the map, which stays four orders below one drag pixel. // Pinned so a future quantum change cannot make the needle visibly lag the hand unnoticed. static void testNormRoundTripResidualStaysBelowOneDragPixel() { for (int i = 0; i <= 100000; ++i) { const double n = static_cast(i) / 100000.0; const double back = timeNormFromSeconds(timeSecondsFromNorm(n)); CHECK(std::fabs(back - n) < 1e-7); if (!(std::fabs(back - n) < 1e-7)) return; } } // The two stage-time defaults the parameter set actually carries, named so a reader can see the // values the sweep above covers generically. static void testTheStageTimeDefaultsRoundTripExactly() { CHECK(timeSecondsFromNorm(timeNormFromSeconds(0.003)) == 0.003); CHECK(timeSecondsFromNorm(timeNormFromSeconds(0.060)) == 0.060); CHECK(timeSecondsFromNorm(timeNormFromSeconds(0.0)) == 0.0); } // --- the depth taper ----------------------------------------------------------------------- static void testDepthCentreAndEndsAreExact() { CHECK(depthNormFromSemitones(0.0, kDepth) == 0.5); CHECK(depthSemitonesFromNorm(0.5, kDepth) == 0.0); CHECK(depthNormFromSemitones(kDepth, kDepth) == 1.0); CHECK(depthNormFromSemitones(-kDepth, kDepth) == 0.0); CHECK(depthSemitonesFromNorm(1.0, kDepth) == kDepth); CHECK(depthSemitonesFromNorm(0.0, kDepth) == -kDepth); // Beyond the throw clamps rather than extrapolating. CHECK(depthNormFromSemitones(100.0, kDepth) == 1.0); CHECK(depthSemitonesFromNorm(5.0, kDepth) == kDepth); } // Symmetric BITWISE, not approximately: a bipolar knob whose two halves disagreed by an ulp // would read a different depth up than down at the same distance from centre. static void testDepthIsExactlySymmetric() { for (int i = 0; i <= 1000; ++i) { const double n = static_cast(i) / 1000.0; CHECK(depthSemitonesFromNorm(n, kDepth) == -depthSemitonesFromNorm(1.0 - n, kDepth)); if (depthSemitonesFromNorm(n, kDepth) != -depthSemitonesFromNorm(1.0 - n, kDepth)) return; } } // Centre expansion: the musically useful +/-7 st gets more than half of each half-travel. static void testDepthLandmarkLandsInItsBand() { const double halfTravel = (depthNormFromSemitones(7.0, kDepth) - 0.5) * 2.0; CHECK(halfTravel >= 0.50 && halfTravel <= 0.58); // The negative half is the same distance out. Compared with a tolerance, not bitwise: 0.5+h // and 0.5-h round differently, and the mirror that has to be EXACT is the one in the plain // direction (testDepthIsExactlySymmetric) — a sub-ulp difference in a needle angle is not. CHECK(std::fabs((0.5 - depthNormFromSemitones(-7.0, kDepth)) * 2.0 - halfTravel) < 1e-15); } static void testDepthIsMonotone() { double prev = -1e9; for (int i = 0; i <= 200000; ++i) { const double v = depthSemitonesFromNorm(static_cast(i) / 200000.0, kDepth); CHECK(v >= prev); if (v < prev) return; prev = v; } } // Same exact-preimage guarantee as the time taper: every value on the depth quantum grid comes // back bitwise. Whole semitones are the case a Shift-snap produces, so they are swept explicitly. static void testEveryWholeSemitoneRoundTripsExactly() { for (int st = -24; st <= 24; ++st) { const double d = static_cast(st); CHECK(depthSemitonesFromNorm(depthNormFromSemitones(d, kDepth), kDepth) == d); } for (int milli = -24000; milli <= 24000; milli += 37) { const double d = static_cast(milli) / 1000.0; CHECK(depthSemitonesFromNorm(depthNormFromSemitones(d, kDepth), kDepth) == d); if (depthSemitonesFromNorm(depthNormFromSemitones(d, kDepth), kDepth) != d) return; } } // A degenerate throw is a caller bug, not a crash: the map collapses to the centre. static void testDegenerateThrowCollapsesToCentre() { CHECK(depthNormFromSemitones(3.0, 0.0) == 0.5); CHECK(depthSemitonesFromNorm(0.9, 0.0) == 0.0); } // --- the whole-unit snaps ------------------------------------------------------------------- static void testMillisecondSnap() { CHECK(snapSecondsToWholeMs(0.0124) == 0.012); CHECK(snapSecondsToWholeMs(0.0126) == 0.013); CHECK(snapSecondsToWholeMs(0.0004) == 0.0); CHECK(snapSecondsToWholeMs(-1.0) == 0.0); CHECK(snapSecondsToWholeMs(9.9996) == 10.0); // The snapped value is itself on the taper's grid, so a snap followed by a round trip holds. CHECK(timeSecondsFromNorm(timeNormFromSeconds(snapSecondsToWholeMs(0.0347))) == 0.035); } static void testPercentSnap() { CHECK(snapFractionToWholePercent(0.514) == 0.51); CHECK(snapFractionToWholePercent(0.516) == 0.52); CHECK(snapFractionToWholePercent(-0.514) == -0.51); CHECK(snapFractionToWholePercent(1.0) == 1.0); CHECK(snapFractionToWholePercent(0.0) == 0.0); } static void testSemitoneSnap() { CHECK(snapSemitonesToWhole(6.6) == 7.0); CHECK(snapSemitonesToWhole(-6.6) == -7.0); CHECK(snapSemitonesToWhole(0.4) == 0.0); CHECK(depthSemitonesFromNorm(depthNormFromSemitones(snapSemitonesToWhole(6.6), kDepth), kDepth) == 7.0); } // The exponent snap reaches 1.0, the linear neutral — one snap from the dial's centre — and // clamps into curve_law's own domain rather than rounding to a zero that is not an exponent. static void testExponentSnap() { CHECK(snapExponentToWhole(1.4) == 1.0); CHECK(snapExponentToWhole(2.6) == 3.0); CHECK(snapExponentToWhole(0.3) == util::kCurveMin); CHECK(snapExponentToWhole(0.6) == 1.0); CHECK(snapExponentToWhole(1e9) == util::kCurveMax); } int main() { testShiftBeatsCtrlForTheFineDragRate(); testStageTimeEndpointsAreExact(); testStageTimeCeilingIsTenSeconds(); testStageTimeLandmarksLandInTheirBands(); testStageTimeIsMonotone(); testEveryFinestDragStepMovesTheValue(); testEveryWholeMicrosecondRoundTripsExactly(); testRoundingSurvivesAHostileFpRoundingMode(); testNormRoundTripResidualStaysBelowOneDragPixel(); testTheStageTimeDefaultsRoundTripExactly(); testDepthCentreAndEndsAreExact(); testDepthIsExactlySymmetric(); testDepthLandmarkLandsInItsBand(); testDepthIsMonotone(); testEveryWholeSemitoneRoundTripsExactly(); testDegenerateThrowCollapsesToCentre(); testMillisecondSnap(); testPercentSnap(); testSemitoneSnap(); testExponentSnap(); if (g_fail == 0) std::printf("param_taper: all tests passed\n"); else std::printf("param_taper: %d FAILED\n", g_fail); return g_fail == 0 ? 0 : 1; }