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:
2026-07-27 14:45:50 -04:00
parent a1f9dcf6f8
commit 026ca90217
3 changed files with 147 additions and 14 deletions
+12 -7
View File
@@ -59,11 +59,13 @@ struct VelocityPoint {
inline constexpr int kCurveNodeGrabRadius = 6;
// A velocity->amp transfer curve: an X-ORDERED list of control points spanning [0,127], evaluated by
// LINEAR interpolation between adjacent points (a monotonic polyline: each velocity maps to exactly
// one amp). Linear-between-knots is deliberate — it makes linear() an EXACT straight line y =
// velocity/127 (the Option-B / null-response contract) and keeps monotonicity trivial; the "curved"
// shape a sound wants comes from placing more control points, not from bending one segment. The two
// endpoints (velocity 0 and 127) are load-bearing: they keep eval total and are never deletable.
// a MONOTONE cubic Hermite spline (FritschCarlson slope limiting) through the knots — a genuine
// curved response (Daniel 2026-07-27: "straight lines sound like shit"), not a polyline. Each
// velocity still maps to exactly one amp: the interpolant is single-valued and provably stays within
// each segment's amp range, so the curve never overshoots below 0 or above 1. For COLLINEAR knots the
// FritschCarlson tangents reduce to the secant slope, so the spline IS the straight line — that
// keeps linear() an EXACT y = velocity/127 (the Option-B / null-response contract). The two endpoints
// (velocity 0 and 127) are load-bearing: they keep eval total and are never deletable.
class VelocityCurve {
public:
// R10-F1 default (Option A): flat y=1 — endpoints (0,1) and (127,1); every velocity -> unity.
@@ -86,8 +88,11 @@ public:
// Evaluate the curve at `velocity` -> amp in [0,1]. Velocity is box-clamped to [0,127] first,
// so an out-of-range note (shouldn't occur) reads the nearest endpoint. Between two adjacent
// points the amp interpolates LINEARLY across the normalized X position — monotonic in X. A
// degenerate curve (0 or 1 point, shouldn't occur post-construction) returns kAmpMax (flat).
// points the amp follows a MONOTONE cubic Hermite spline (FritschCarlson slope limiting) — a
// true curve that provably stays within the two knots' amp range (no overshoot below 0 / above
// 1) and reduces to the exact straight line for collinear knots. Single-valued / monotonic in X.
// Degenerate cases (shouldn't occur post-construction): an EMPTY curve returns kAmpMax (flat
// unity); a ONE-point curve returns that point's amp.
double eval(double velocity) const;
// --- Editing (for the S-VIEW-10 editor UI) --------------------------------------------------