instrument: latch the note done at the read-head run-off, fit the migrated fades, and lift the curve dial and overlay selection into pure modules
This commit is contained in:
@@ -7,7 +7,8 @@
|
||||
// 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.
|
||||
// through, and its round trip against the exponent; and the inner dial's own travel — exact at
|
||||
// the neutral centre, and reachable there from a real drag grid.
|
||||
|
||||
#include "../src/core/util/curve_law.h"
|
||||
|
||||
@@ -106,6 +107,72 @@ static void testMidLevelInverseSaturates() {
|
||||
CHECK(std::fabs(curveFromMidLevel(0.5) - kCurveNeutral) < 1e-12);
|
||||
}
|
||||
|
||||
// --- The inner dial's travel ---------------------------------------------------
|
||||
|
||||
// The knob drag delivers `start - dy/kKnobDragRangePixels`. param_slider owns that constant and
|
||||
// this module deliberately does not link it, so the step is restated here; the structural
|
||||
// assertion below is what keeps the detent wide enough for whatever it is.
|
||||
static constexpr double kKnobStep = 1.0 / 128.0;
|
||||
|
||||
// The dial's centre must reach the neutral EXACTLY, in both directions — an exponent a hair off
|
||||
// 1.0 costs a std::pow per sample per voice forever on a stage the user believes is at rest.
|
||||
static void testKnobLawIsExactAtTheNeutralCentre() {
|
||||
CHECK(knobNormFromCurve(kCurveNeutral) == 0.5);
|
||||
CHECK(curveFromKnobNorm(0.5) == kCurveNeutral);
|
||||
// And the identity that exactness buys: curveMap takes its bit-identical fast path.
|
||||
for (int i = 0; i <= 100; ++i) {
|
||||
const double phi = static_cast<double>(i) / 100.0;
|
||||
CHECK(curveMap(phi, curveFromKnobNorm(0.5)) == phi);
|
||||
}
|
||||
}
|
||||
|
||||
// A dial swept THROUGH the centre has to land on the identity. The raw logarithmic travel does
|
||||
// not — the drag grid steps by 1/128 and only touches 0.5 by luck — so this is the detent's own
|
||||
// property, asserted against that raw travel as the reference.
|
||||
static void testADialSweptThroughNeutralLandsOnTheIdentity() {
|
||||
const auto rawTravel = [](double t) {
|
||||
return std::exp((2.0 * t - 1.0) * std::log(kCurveMax));
|
||||
};
|
||||
// A real drag: grabbed at a shaped value, dragged 40 steps down through the centre.
|
||||
const double grab = 0.5 + 17.0 * kKnobStep + 0.003; // deliberately off the grid
|
||||
int detented = 0;
|
||||
int rawHits = 0;
|
||||
for (int step = 0; step <= 40; ++step) {
|
||||
const double t = grab - step * kKnobStep;
|
||||
if (curveFromKnobNorm(t) == kCurveNeutral) ++detented;
|
||||
if (rawTravel(t) == kCurveNeutral) ++rawHits;
|
||||
}
|
||||
CHECK(detented >= 1); // the sweep reaches the identity
|
||||
CHECK(rawHits == 0); // and would not have without the detent
|
||||
// The structural reason it cannot be skipped: the band is wider than one drag step.
|
||||
CHECK(kCurveKnobDetent > kKnobStep);
|
||||
}
|
||||
|
||||
// Outside the detent the pair are inverses, so the dial reads back what it wrote and the
|
||||
// endpoints saturate on the domain rather than past it.
|
||||
static void testKnobLawRoundTripsOutsideTheDetent() {
|
||||
const double exps[] = {kCurveMin, 0.2, 0.5, 0.8, 1.3, 2.0, 5.0, kCurveMax};
|
||||
for (double e : exps) {
|
||||
const double back = curveFromKnobNorm(knobNormFromCurve(e));
|
||||
CHECK(std::fabs(back - e) < 1e-9);
|
||||
}
|
||||
CHECK(curveFromKnobNorm(0.0) == kCurveMin);
|
||||
CHECK(curveFromKnobNorm(-3.0) == kCurveMin); // out-of-range norm saturates
|
||||
CHECK(std::fabs(curveFromKnobNorm(1.0) - kCurveMax) < 1e-12);
|
||||
// The ENDS need only land on the domain, not on an exact norm — 0.1 is not exactly 1/10 in
|
||||
// binary, so log(kCurveMin) is a hair off -log(kCurveMax). Only the centre carries an
|
||||
// exactness requirement, and only because the neutral is a bit-identity.
|
||||
CHECK(std::fabs(knobNormFromCurve(kCurveMin)) < 1e-12);
|
||||
CHECK(knobNormFromCurve(kCurveMax) == 1.0);
|
||||
// Monotone rising across the whole travel, so the dial has one unambiguous direction.
|
||||
double prev = 0.0;
|
||||
for (int i = 0; i <= 500; ++i) {
|
||||
const double v = curveFromKnobNorm(static_cast<double>(i) / 500.0);
|
||||
CHECK(v >= prev);
|
||||
prev = v;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
testNeutralExponentIsTheIdentity();
|
||||
testEndpointsAreExactAtEveryExponent();
|
||||
@@ -114,6 +181,9 @@ int main() {
|
||||
testClampCurveHoldsTheDomain();
|
||||
testMidLevelRoundTripsAgainstTheExponent();
|
||||
testMidLevelInverseSaturates();
|
||||
testKnobLawIsExactAtTheNeutralCentre();
|
||||
testADialSweptThroughNeutralLandsOnTheIdentity();
|
||||
testKnobLawRoundTripsOutsideTheDetent();
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user