// Standalone tests for reasampler::instrument::ui::bake_hold — the Hold knob's map onto the // note-length ladder. No VST3, no REAPER, no framework. // // Covers: both ends of the knob, the round trip from every rung, out-of-range and non-finite // input, that every rung is reachable (no rung is skipped by the rounding), that the travel is // monotone in DURATION, and that each rung owns an equal slice of it. #include "../src/core/instrument/ui/bake_hold.h" #include #include #include using namespace reasampler::instrument::ui; using namespace reasampler::instrument::note; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) namespace { // The shortest and longest lengths ON THE LADDER, found by scanning rather than by indexing an // end of the picker order — which is exactly the assumption under test. Division shortestRung() { Division best = divisionAt(0); for (int i = 1; i < kDivisionCount; ++i) if (divisionBeats(divisionAt(i)) < divisionBeats(best)) best = divisionAt(i); return best; } Division longestRung() { Division best = divisionAt(0); for (int i = 1; i < kDivisionCount; ++i) if (divisionBeats(divisionAt(i)) > divisionBeats(best)) best = divisionAt(i); return best; } } // namespace int main() { // --- The ends of the travel are the SHORTEST and LONGEST notes ------------------- // Not divisionAt(0) / divisionAt(kDivisionCount - 1): those are the ends of the picker's // presentation order, which is not length order. CHECK(bakeHoldFromNorm(0.0) == shortestRung()); CHECK(bakeHoldFromNorm(1.0) == longestRung()); // --- Out of range clamps rather than wrapping ------------------------------------ CHECK(bakeHoldFromNorm(-3.0) == shortestRung()); CHECK(bakeHoldFromNorm(9.5) == longestRung()); CHECK(bakeHoldFromNorm(std::nan("")) == shortestRung()); // --- Round trip: a knob painted from a stored rung and released reproduces it ----- for (int i = 0; i < kDivisionCount; ++i) { const Division d = divisionAt(i); CHECK(bakeHoldFromNorm(bakeHoldNorm(d)) == d); } // --- Every rung is reachable from the knob, and each owns a contiguous slice ------ // Swept finely enough to catch a rounding that skipped one: 39 rungs over [0,1]. { std::vector seen(static_cast(kDivisionCount), false); for (int step = 0; step <= 4000; ++step) { const Division d = bakeHoldFromNorm(static_cast(step) / 4000.0); seen[static_cast(divisionIndex(d))] = true; } for (int i = 0; i < kDivisionCount; ++i) CHECK(seen[static_cast(i)]); } // --- The map is monotone IN DURATION: turning the knob up never shortens the note -- // Asserted over divisionBeats, not divisionIndex: the index is presentation order, in which // a rung's triplet sits after (and is shorter than) the previous rung's dotted — so an // index sweep can be non-decreasing while the note it selects gets shorter. { double previous = 0.0; for (int step = 0; step <= 4000; ++step) { const double beats = divisionBeats(bakeHoldFromNorm(static_cast(step) / 4000.0)); CHECK(beats >= previous); previous = beats; } } // --- Each rung owns an EQUAL slice, centred on its own position ------------------- // A floor-based map passes every property above while giving each rung the slice ABOVE its // position; only the boundaries tell the two apart. Slice k spans [(k-0.5)/L, (k+0.5)/L). { constexpr int last = kDivisionCount - 1; const double slice = 1.0 / static_cast(last); const double eps = slice / 1000.0; for (int k = 1; k < last; ++k) { const double centre = static_cast(k) * slice; CHECK(bakeHoldFromNorm(centre) == bakeHoldFromNorm(centre - slice * 0.5 + eps)); CHECK(bakeHoldFromNorm(centre) == bakeHoldFromNorm(centre + slice * 0.5 - eps)); // …and one step past the upper boundary is already the NEXT rung. CHECK(divisionBeats(bakeHoldFromNorm(centre + slice * 0.5 + eps)) > divisionBeats(bakeHoldFromNorm(centre))); } } if (g_fail == 0) std::printf("bake_hold: all tests passed\n"); return g_fail ? 1 : 0; }