vst: curve velocity_curve eval with a monotone cubic Hermite (Fritsch-Carlson) spline
True curved response, provably no overshoot outside [0,1]; collinear knots reproduce the exact linear ramp so linear()'s null-response stays bit-exact. Eval-only; v7 wire-format untouched. Adds spline tests.
This commit is contained in:
@@ -78,6 +78,71 @@ static void testEvalMonotonicInX() {
|
||||
CHECK(near(c.eval(64), 0.0)); // the trough sits exactly on the moved point
|
||||
}
|
||||
|
||||
// --- eval: monotone cubic Hermite spline (Fritsch–Carlson) --------------------
|
||||
|
||||
static void testCollinearControlPointsReproduceExactLinearRamp() {
|
||||
// The Option-B guarantee: for COLLINEAR knots the FC tangents reduce to the secant slope, so the
|
||||
// spline IS the straight line y = velocity/127 — bit-exact, not merely close. Add an interior
|
||||
// point that sits EXACTLY on the linear ramp so all three knots are collinear.
|
||||
VelocityCurve c = VelocityCurve::linear(); // (0,0),(127,1)
|
||||
c.addPoint(60.0, 60.0 / 127.0); // on the line -> still collinear
|
||||
// Every velocity must equal velocity/127 to full double precision (bit-exact reproduction).
|
||||
for (int v = 0; v <= 127; ++v) CHECK(near(c.eval(v), v / 127.0, 1e-12));
|
||||
// And the untouched linear() with only its two endpoints, too.
|
||||
const VelocityCurve line = VelocityCurve::linear();
|
||||
for (int v = 0; v <= 127; ++v) CHECK(near(line.eval(v), v / 127.0, 1e-12));
|
||||
}
|
||||
|
||||
static void testNoOvershootWithSharpInteriorDip() {
|
||||
// A sharp interior dip is the classic overshoot trap: a NON-monotone interpolant (Catmull-Rom /
|
||||
// natural cubic) would bulge the curve below 0 near the trough. Fritsch–Carlson must keep every
|
||||
// sampled amp inside [0,1] across the whole domain. Build (0,1)->(64,0)->(127,1).
|
||||
VelocityCurve c = VelocityCurve::flat();
|
||||
c.movePoint(0, 0, 1.0);
|
||||
c.addPoint(64.0, 0.0);
|
||||
c.movePoint(2, 127, 1.0);
|
||||
for (int v = 0; v <= 127; ++v) {
|
||||
const double y = c.eval(v);
|
||||
CHECK(y >= 0.0 - 1e-12 && y <= 1.0 + 1e-12);
|
||||
}
|
||||
// A dense sub-integer sweep too (the spline could overshoot between integer velocities).
|
||||
for (int k = 0; k <= 1270; ++k) {
|
||||
const double y = c.eval(k / 10.0);
|
||||
CHECK(y >= 0.0 - 1e-12 && y <= 1.0 + 1e-12);
|
||||
}
|
||||
CHECK(near(c.eval(64), 0.0)); // knot honored exactly
|
||||
}
|
||||
|
||||
static void testSplineStaysSingleValuedMonotoneInEachSegment() {
|
||||
// A rising staircase of knots: the spline must be non-decreasing across the whole domain (the FC
|
||||
// limiter forbids overshoot, so a monotone-increasing knot set yields a monotone-increasing
|
||||
// curve — no local wiggles that would make eval multi-valued in feel).
|
||||
VelocityCurve c = VelocityCurve::linear();
|
||||
c.addPoint(30.0, 0.1);
|
||||
c.addPoint(60.0, 0.15); // a near-flat run then a steep rise: overshoot bait for a plain cubic
|
||||
c.addPoint(90.0, 0.9);
|
||||
double prev = c.eval(0);
|
||||
for (int k = 1; k <= 1270; ++k) {
|
||||
const double cur = c.eval(k / 10.0);
|
||||
CHECK(cur >= prev - 1e-9); // non-decreasing everywhere -> single-valued, no wiggle
|
||||
CHECK(cur >= 0.0 - 1e-12 && cur <= 1.0 + 1e-12);
|
||||
prev = cur;
|
||||
}
|
||||
}
|
||||
|
||||
static void testSplinePinsEndpointsExactly() {
|
||||
// The curve must pass THROUGH every control point, endpoints included, regardless of curvature.
|
||||
VelocityCurve c = VelocityCurve::flat();
|
||||
c.movePoint(0, 0, 0.2); // first endpoint amp 0.2
|
||||
c.addPoint(40.0, 0.9);
|
||||
c.addPoint(80.0, 0.1);
|
||||
c.movePoint(3, 127, 0.7); // last endpoint amp 0.7
|
||||
CHECK(near(c.eval(0), 0.2));
|
||||
CHECK(near(c.eval(40), 0.9));
|
||||
CHECK(near(c.eval(80), 0.1));
|
||||
CHECK(near(c.eval(127), 0.7));
|
||||
}
|
||||
|
||||
// --- editing: addPoint --------------------------------------------------------
|
||||
|
||||
static void testAddPointKeepsXOrderAndClamps() {
|
||||
@@ -223,6 +288,10 @@ int main() {
|
||||
testLinearRamp();
|
||||
testEvalBoxClampsOutOfRangeVelocity();
|
||||
testEvalMonotonicInX();
|
||||
testCollinearControlPointsReproduceExactLinearRamp();
|
||||
testNoOvershootWithSharpInteriorDip();
|
||||
testSplineStaysSingleValuedMonotoneInEachSegment();
|
||||
testSplinePinsEndpointsExactly();
|
||||
testAddPointKeepsXOrderAndClamps();
|
||||
testMoveInteriorClampsToNeighbours();
|
||||
testMoveEndpointsArePinnedInX();
|
||||
|
||||
Reference in New Issue
Block a user