instrument: the filter's velocity depth knob returns and multiplies the bipolar curve; the pre-v12 lift is a pure domain re-tag

This commit is contained in:
2026-07-31 20:24:40 -04:00
parent cfb53aade3
commit 5e290119c5
22 changed files with 243 additions and 166 deletions
+11 -14
View File
@@ -4,8 +4,8 @@
// * eval — flat y=1 unipolar default (EVERY velocity -> 1.0), linear ramp, curved shape
// between points, box-clamp of an out-of-range velocity, monotonic-in-x over the whole domain.
// * the BIPOLAR domain — zero() is exactly 0 everywhere, the negative half evaluates and clamps
// at -1, eval is homogeneous in y (what the pre-v12 filter lift rests on), and the pixel maps
// put value 0 on the box's centre line rather than its floor.
// at -1, knots inside [0,1] evaluate identically in either domain (what the codec's v12
// re-tag rests on), and the pixel maps put value 0 on the box's centre line, not its floor.
// * editing — addPoint keeps X-order + box-clamp; movePoint clamps an interior point between its
// neighbours (can't cross) and box-clamps the value; endpoints are X-pinned (velocity 0 / 127)
// with only the value mobile; deletePoint removes interior points but REFUSES the two endpoints.
@@ -333,19 +333,16 @@ static void testUnipolarClampsAtZeroWhereBipolarDoesNot() {
CHECK(near(b2.eval(127), 1.0));
}
static void testEvalIsHomogeneousInY() {
// The property the pre-v12 filter lift rests on: scaling every knot's y by k scales the
// whole evaluated curve by k. Asserted against a CURVED (non-collinear) knot set, where
// the Fritsch-Carlson tangents are actually doing work.
static void testTheSameKnotsEvaluateIdenticallyInEitherDomain() {
// What the codec's v12 domain re-tag rests on: a curve whose y values all lie in [0,1] is
// read the same way in either domain — the domain governs the CLAMP, not the evaluation.
// Asserted against a CURVED (non-collinear) knot set, where the tangents are doing work,
// and with ==: the re-tag is bit-identical, not merely close.
const std::vector<VelocityPoint> knots = {
{0.0, 0.1}, {30.0, 0.15}, {64.0, 0.9}, {100.0, 0.4}, {127.0, 1.0}};
const VelocityCurve base = VelocityCurve::fromPoints(knots, CurveDomain::Unipolar);
for (const double k : {0.75, -0.4, 1.0}) {
std::vector<VelocityPoint> scaled = knots;
for (VelocityPoint& p : scaled) p.value *= k;
const VelocityCurve s = VelocityCurve::fromPoints(scaled, CurveDomain::Bipolar);
for (int v = 0; v <= 127; ++v) CHECK(near(s.eval(v), k * base.eval(v), 1e-12));
}
const VelocityCurve u = VelocityCurve::fromPoints(knots, CurveDomain::Unipolar);
const VelocityCurve b = VelocityCurve::fromPoints(knots, CurveDomain::Bipolar);
for (int v = 0; v <= 127; ++v) CHECK(b.eval(v) == u.eval(v));
}
static void testBipolarPixelMapPutsZeroOnTheCentreLine() {
@@ -471,7 +468,7 @@ int main() {
testZeroIsExactlyZeroAtEveryVelocity();
testBipolarEvalSpansTheNegativeHalf();
testUnipolarClampsAtZeroWhereBipolarDoesNot();
testEvalIsHomogeneousInY();
testTheSameKnotsEvaluateIdenticallyInEitherDomain();
testBipolarPixelMapPutsZeroOnTheCentreLine();
testBipolarDragCoversTwiceTheValueRange();
testPixelFromPointMapsCornersAndMidpoint();