instrument: spline EGs — hard points on the one shared spline, a drawn contour per envelope beside its staged state, payload v13

This commit is contained in:
2026-07-31 21:33:59 -04:00
parent f115904e4f
commit e44bd42dd9
33 changed files with 1739 additions and 364 deletions
+41 -56
View File
@@ -62,9 +62,18 @@ VelocityCurve VelocityCurve::zero() {
return c;
}
VelocityCurve VelocityCurve::rampDown() {
VelocityCurve c;
c.points_ = {{kVelMin, kCurveYMax, false}, {kVelMax, 0.0, false}};
return c;
}
VelocityCurve VelocityCurve::fromPoints(std::vector<VelocityPoint> pts, CurveDomain domain) {
// Stable sort so coincident-X points keep their wire order (eval stays well-defined for
// duplicate-X knots).
// Trim before the endpoint synthesis below can add up to two more, then again after, so a
// corrupt over-long blob lands at exactly the ceiling with its two endpoints intact.
if (pts.size() > kMaxCurvePoints) pts.resize(kMaxCurvePoints);
for (VelocityPoint& p : pts) {
p.velocity = clampVelocity(p.velocity);
p.value = clampValue(p.value, domain);
@@ -77,42 +86,34 @@ VelocityCurve VelocityCurve::fromPoints(std::vector<VelocityPoint> pts, CurveDom
return domain == CurveDomain::Bipolar ? zero() : flat();
}
if (pts.front().velocity > kVelMin) {
pts.insert(pts.begin(), VelocityPoint{kVelMin, pts.front().value});
pts.insert(pts.begin(), VelocityPoint{kVelMin, pts.front().value, pts.front().hard});
} else {
pts.front().velocity = kVelMin;
}
if (pts.back().velocity < kVelMax) {
pts.push_back(VelocityPoint{kVelMax, pts.back().value});
pts.push_back(VelocityPoint{kVelMax, pts.back().value, pts.back().hard});
} else {
pts.back().velocity = kVelMax;
}
if (pts.size() > kMaxCurvePoints) {
// Drop the interior points nearest the end, never an endpoint.
pts.erase(pts.begin() + static_cast<std::ptrdiff_t>(kMaxCurvePoints) - 1,
pts.end() - 1);
}
VelocityCurve c;
c.domain_ = domain;
c.points_ = std::move(pts);
return c;
}
namespace {
// Fritsch-Carlson monotone-cubic tangent: a sign change (or flat) neighbour is a local extremum,
// so the tangent pins to 0 to avoid overshoot; otherwise the weighted-harmonic-mean tangent,
// which for collinear knots (dPrev==dNext) reduces exactly to the shared secant — this is what
// makes the spline reproduce a straight line for linear()-style input.
double fritschCarlsonTangent(double dPrev, double dNext, double spanPrev, double spanNext) {
if (dPrev * dNext <= 0.0) return 0.0;
const double w1 = 2.0 * spanNext + spanPrev;
const double w2 = spanNext + 2.0 * spanPrev;
return (w1 + w2) / (w1 / dPrev + w2 / dNext);
}
} // namespace
double VelocityCurve::eval(double velocity) const {
if (points_.empty()) return curveNeutral(domain_);
if (points_.size() == 1) return clampValue(points_[0].value, domain_);
const double v = clampVelocity(velocity);
if (v <= points_.front().velocity) return clampValue(points_.front().value, domain_);
if (v >= points_.back().velocity) return clampValue(points_.back().value, domain_);
// Linear walk: this overload is the COLD one (a note-on, a paint column). The per-sample
// reader is SplineCursor, which shares the same tangent + Hermite functions.
for (std::size_t i = 0; i + 1 < points_.size(); ++i) {
const VelocityPoint& a = points_[i];
const VelocityPoint& b = points_[i + 1];
@@ -120,55 +121,38 @@ double VelocityCurve::eval(double velocity) const {
const double span = b.velocity - a.velocity;
// Coincident-X neighbours (a step): zero-width segment, no interior to blend.
if (span <= 0.0) return clampValue(b.value, domain_);
// Monotone cubic Hermite (Fritsch-Carlson): provably stays within [a.value, b.value]
// between the two knots (no overshoot), reproducing a straight line for collinear input.
const double d = (b.value - a.value) / span;
double mA = d;
if (i > 0) {
const VelocityPoint& prev = points_[i - 1];
const double spanPrev = a.velocity - prev.velocity;
if (spanPrev > 0.0) {
const double dPrev = (a.value - prev.value) / spanPrev;
mA = fritschCarlsonTangent(dPrev, d, spanPrev, span);
} else {
mA = 0.0;
}
}
double mB = d;
if (i + 2 < points_.size()) {
const VelocityPoint& next = points_[i + 2];
const double spanNext = next.velocity - b.velocity;
if (spanNext > 0.0) {
const double dNext = (next.value - b.value) / spanNext;
mB = fritschCarlsonTangent(d, dNext, span, spanNext);
} else {
mB = 0.0;
}
}
const double t = (v - a.velocity) / span;
const double t2 = t * t;
const double t3 = t2 * t;
const double h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
const double h10 = t3 - 2.0 * t2 + t;
const double h01 = -2.0 * t3 + 3.0 * t2;
const double h11 = t3 - t2;
const double y = h00 * a.value + h10 * span * mA + h01 * b.value + h11 * span * mB;
const SegmentTangents m = segmentTangents(points_.data(), points_.size(), i, d, span);
const double y = hermiteAt(a.value, b.value, span, m.mA, m.mB,
(v - a.velocity) / span);
return clampValue(y, domain_);
}
}
return clampValue(points_.back().value, domain_); // unreachable (v is between the endpoints)
}
std::size_t VelocityCurve::addPoint(double velocity, double value) {
const VelocityPoint p{clampVelocity(velocity), clampValue(value, domain_)};
int VelocityCurve::addPoint(double velocity, double value) {
// At the ceiling the add is REFUSED outright rather than trading a point away — the existing
// contour must come through an over-add bit-identical.
if (points_.size() >= kMaxCurvePoints) return -1;
const VelocityPoint p{clampVelocity(velocity), clampValue(value, domain_), false};
// First index strictly greater, so a duplicate-X point lands immediately after the existing one.
std::size_t i = 0;
while (i < points_.size() && points_[i].velocity <= p.velocity) ++i;
points_.insert(points_.begin() + static_cast<std::ptrdiff_t>(i), p);
return i;
return static_cast<int>(i);
}
bool VelocityCurve::toggleHard(std::size_t index) {
if (index >= points_.size()) return false;
points_[index].hard = !points_[index].hard;
return true;
}
bool VelocityCurve::setHard(std::size_t index, bool hard) {
if (index >= points_.size()) return false;
points_[index].hard = hard;
return true;
}
VelocityPoint VelocityCurve::movePoint(std::size_t index, double velocity, double value) {
@@ -187,7 +171,7 @@ VelocityPoint VelocityCurve::movePoint(std::size_t index, double velocity, doubl
const double hi = points_[index + 1].velocity;
newVel = std::clamp(clampVelocity(velocity), lo, hi);
}
points_[index] = VelocityPoint{newVel, newValue};
points_[index] = VelocityPoint{newVel, newValue, points_[index].hard};
return points_[index];
}
@@ -255,6 +239,7 @@ bool VelocityCurve::equals(const VelocityCurve& other, double eps) const {
for (std::size_t i = 0; i < points_.size(); ++i) {
if (std::fabs(points_[i].velocity - other.points_[i].velocity) > eps) return false;
if (std::fabs(points_[i].value - other.points_[i].value) > eps) return false;
if (points_[i].hard != other.points_[i].hard) return false;
}
return true;
}