// Standalone tests for reasampler::util::curve_law — no VST3, no REAPER, no framework. Same // fast assert loop as the sibling pure tests. This is the ONE law behind the engine's segment // evaluator, the overlay's knot geometry, and the deck's inner dial, so what it guarantees is // what all three inherit. // // Covers: the LINEAR NEUTRAL (exponent 1.0 returns its input BIT-IDENTICALLY, which is what // makes a pre-existing instance play unchanged); endpoint exactness at every exponent (no // segment can overshoot its own endpoint levels); monotonicity and finiteness across the full // 0.1..10 domain including both endpoints; the mid-level inverse the overlay knot drags // through, and its round trip against the exponent. #include "../src/core/util/curve_law.h" #include #include using namespace reasampler::util; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // The neutral is not merely "close to linear" — it must be the identity, bit for bit, or a // blob that loaded at 1.0 would render differently from the engine that wrote it. static void testNeutralExponentIsTheIdentity() { for (int i = 0; i <= 1000; ++i) { const double phi = static_cast(i) / 1000.0; CHECK(curveMap(phi, kCurveNeutral) == phi); } // Including the values a fractional stage position actually takes. CHECK(curveMap(1.0 / 3.0, 1.0) == 1.0 / 3.0); CHECK(curveMap(0.1234567890123, 1.0) == 0.1234567890123); } // Both endpoints are exact at every exponent, which is the whole overshoot guarantee: a curved // stage starts where the previous one ended and ends where the next one starts. static void testEndpointsAreExactAtEveryExponent() { for (int i = 0; i <= 100; ++i) { const double e = kCurveMin + (kCurveMax - kCurveMin) * (i / 100.0); CHECK(curveMap(0.0, e) == 0.0); CHECK(curveMap(1.0, e) == 1.0); } } // The full domain, both endpoints included: finite, in range, and strictly rising. static void testSweepIsFiniteMonotoneAndInRange() { const double exps[] = {kCurveMin, 0.25, 0.5, kCurveNeutral, 2.0, 4.0, kCurveMax}; for (double e : exps) { double prev = -1.0; for (int i = 0; i <= 500; ++i) { const double phi = static_cast(i) / 500.0; const double v = curveMap(phi, e); CHECK(std::isfinite(v)); CHECK(v >= 0.0 && v <= 1.0); CHECK(v > prev - 1e-15); // non-decreasing prev = v; } CHECK(std::fabs(prev - 1.0) < 1e-12); } } // Which side of the neutral an exponent falls on is the SHAPE, and the two directions must not // collapse into each other. static void testExponentDirectionShapesTheSegment() { CHECK(curveMap(0.5, 4.0) < curveMap(0.5, kCurveNeutral)); CHECK(curveMap(0.5, 0.25) > curveMap(0.5, kCurveNeutral)); CHECK(std::fabs(curveMap(0.5, kCurveNeutral) - 0.5) < 1e-15); } static void testClampCurveHoldsTheDomain() { CHECK(clampCurve(-5.0) == kCurveMin); CHECK(clampCurve(0.0) == kCurveMin); CHECK(clampCurve(1e9) == kCurveMax); CHECK(clampCurve(std::nan("")) == kCurveMin); // a corrupt blob degrades, never propagates CHECK(clampCurve(2.5) == 2.5); } // The mid-level inverse is what a knot drag resolves through: it must be the exact inverse of // the forward reading over the whole domain, or the knot and the dial could drift. static void testMidLevelRoundTripsAgainstTheExponent() { for (int i = 0; i <= 200; ++i) { const double e = kCurveMin + (kCurveMax - kCurveMin) * (i / 200.0); const double mid = curveMidLevel(e); CHECK(mid > 0.0 && mid < 1.0); CHECK(std::fabs(curveFromMidLevel(mid) - e) < 1e-9); } // The mid-level is strictly DECREASING in the exponent, so a drag has one unambiguous // direction at every point of the domain. double prev = 1.0; for (int i = 0; i <= 200; ++i) { const double e = kCurveMin + (kCurveMax - kCurveMin) * (i / 200.0); const double mid = curveMidLevel(e); CHECK(mid < prev); prev = mid; } } // A knot dragged past what the domain can express saturates rather than producing a // non-finite exponent. static void testMidLevelInverseSaturates() { CHECK(curveFromMidLevel(0.0) == kCurveMax); CHECK(curveFromMidLevel(-1.0) == kCurveMax); CHECK(curveFromMidLevel(1.0) == kCurveMin); CHECK(curveFromMidLevel(5.0) == kCurveMin); CHECK(curveFromMidLevel(std::nan("")) == kCurveMax); CHECK(std::fabs(curveFromMidLevel(0.5) - kCurveNeutral) < 1e-12); } int main() { testNeutralExponentIsTheIdentity(); testEndpointsAreExactAtEveryExponent(); testSweepIsFiniteMonotoneAndInRange(); testExponentDirectionShapesTheSegment(); testClampCurveHoldsTheDomain(); testMidLevelRoundTripsAgainstTheExponent(); testMidLevelInverseSaturates(); if (g_fail == 0) std::printf("curve_law: all tests passed\n"); else std::printf("curve_law: %d FAILED\n", g_fail); return g_fail == 0 ? 0 : 1; }