// velocity_curve.cpp — see velocity_curve.h. Pure eval + editing/clamp/inverse map; no host types. #include "core/instrument/engine/velocity_curve.h" #include #include #include namespace reasampler::instrument::engine { namespace { double clampVelocity(double v) { return std::clamp(v, kVelMin, kVelMax); } double clampAmp(double a) { return std::clamp(a, kAmpMin, kAmpMax); } // X spans the width for [0,127]; Y spans (height-1) rows for amp [0,1] with amp 1 at the TOP // (pixel y increases downward, so this axis is inverted relative to amp). double velPerPixel(const VelocityCurve::Box& box) { const int w = std::max(0, box.width); if (w <= 0) return 0.0; return (kVelMax - kVelMin) / static_cast(w); } double ampPerPixel(const VelocityCurve::Box& box) { const int h = std::max(0, box.height); if (h <= 1) return 0.0; return (kAmpMax - kAmpMin) / static_cast(h - 1); } int velToX(const VelocityCurve::Box& box, double velocity) { const int w = std::max(0, box.width); if (w <= 0) return box.left; const double frac = (clampVelocity(velocity) - kVelMin) / (kVelMax - kVelMin); return box.left + static_cast(frac * static_cast(w) + 0.5); } int ampToY(const VelocityCurve::Box& box, double amp) { const int h = std::max(0, box.height); if (h <= 1) return box.top; const double frac = (clampAmp(amp) - kAmpMin) / (kAmpMax - kAmpMin); return box.top + static_cast((1.0 - frac) * static_cast(h - 1) + 0.5); } } // namespace VelocityCurve VelocityCurve::flat() { VelocityCurve c; c.points_ = {{kVelMin, kAmpMax}, {kVelMax, kAmpMax}}; return c; } VelocityCurve VelocityCurve::linear() { VelocityCurve c; c.points_ = {{kVelMin, kAmpMin}, {kVelMax, kAmpMax}}; return c; } VelocityCurve VelocityCurve::fromPoints(std::vector pts) { // Stable sort so coincident-X points keep their wire order (eval stays well-defined for // duplicate-X knots). for (VelocityPoint& p : pts) { p.velocity = clampVelocity(p.velocity); p.amp = clampAmp(p.amp); } std::stable_sort(pts.begin(), pts.end(), [](const VelocityPoint& a, const VelocityPoint& b) { return a.velocity < b.velocity; }); if (pts.size() < 2) return flat(); if (pts.front().velocity > kVelMin) { pts.insert(pts.begin(), VelocityPoint{kVelMin, pts.front().amp}); } else { pts.front().velocity = kVelMin; } if (pts.back().velocity < kVelMax) { pts.push_back(VelocityPoint{kVelMax, pts.back().amp}); } else { pts.back().velocity = kVelMax; } VelocityCurve c; 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 to ~1e-15 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 kAmpMax; if (points_.size() == 1) return clampAmp(points_[0].amp); const double v = clampVelocity(velocity); if (v <= points_.front().velocity) return clampAmp(points_.front().amp); if (v >= points_.back().velocity) return clampAmp(points_.back().amp); for (std::size_t i = 0; i + 1 < points_.size(); ++i) { const VelocityPoint& a = points_[i]; const VelocityPoint& b = points_[i + 1]; if (v >= a.velocity && v <= b.velocity) { const double span = b.velocity - a.velocity; // Coincident-X neighbours (a step): zero-width segment, no interior to blend. if (span <= 0.0) return clampAmp(b.amp); // Monotone cubic Hermite (Fritsch-Carlson): provably stays within [a.amp, b.amp] // between the two knots (no overshoot), reproducing a straight line for collinear input. const double d = (b.amp - a.amp) / 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.amp - prev.amp) / 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.amp - b.amp) / 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.amp + h10 * span * mA + h01 * b.amp + h11 * span * mB; return clampAmp(y); } } return clampAmp(points_.back().amp); // unreachable (v is between the endpoints) } std::size_t VelocityCurve::addPoint(double velocity, double amp) { const VelocityPoint p{clampVelocity(velocity), clampAmp(amp)}; // 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(i), p); return i; } VelocityPoint VelocityCurve::movePoint(std::size_t index, double velocity, double amp) { if (index >= points_.size()) return VelocityPoint{}; // no-op (out of range) const bool isFirst = (index == 0); const bool isLast = (index + 1 == points_.size()); double newAmp = clampAmp(amp); double newVel; if (isFirst) { newVel = kVelMin; } else if (isLast) { newVel = kVelMax; } else { const double lo = points_[index - 1].velocity; const double hi = points_[index + 1].velocity; newVel = std::clamp(clampVelocity(velocity), lo, hi); } points_[index] = VelocityPoint{newVel, newAmp}; return points_[index]; } bool VelocityCurve::deletePoint(std::size_t index) { if (index >= points_.size()) return false; if (index == 0 || index + 1 == points_.size()) return false; // endpoints are not deletable points_.erase(points_.begin() + static_cast(index)); return true; } VelocityCurve::CurvePixel VelocityCurve::pixelFromPoint(const Box& box, const VelocityPoint& p) { return CurvePixel{velToX(box, p.velocity), ampToY(box, p.amp)}; } VelocityPoint VelocityCurve::pointFromPixel(const Box& box, int x, int y) { // Exact inverse of velToX/ampToY (within one pixel); degenerate dims collapse the same way. VelocityPoint p; const int w = std::max(0, box.width); const int h = std::max(0, box.height); p.velocity = (w <= 0) ? kVelMin : clampVelocity(kVelMin + static_cast(x - box.left) / static_cast(w) * (kVelMax - kVelMin)); p.amp = (h <= 1) ? kAmpMax : clampAmp(kAmpMax - static_cast(y - box.top) / static_cast(h - 1) * (kAmpMax - kAmpMin)); return p; } int VelocityCurve::pointAtPixel(const Box& box, int x, int y) const { for (std::size_t i = 0; i < points_.size(); ++i) { const int px = velToX(box, points_[i].velocity); const int py = ampToY(box, points_[i].amp); if (std::abs(x - px) <= kCurveNodeGrabRadius && std::abs(y - py) <= kCurveNodeGrabRadius) { return static_cast(i); } } return -1; } VelocityCurve VelocityCurve::resolvePointDrag(const VelocityCurve& grabCurve, std::size_t index, const Box& box, int dxPixels, int dyPixels) { VelocityCurve out = grabCurve; if (index >= out.points_.size()) return out; // out of range -> no motion const double velPerPx = velPerPixel(box); const double ampPerPx = ampPerPixel(box); if (velPerPx <= 0.0 || ampPerPx <= 0.0) return out; // degenerate box -> no motion const VelocityPoint& grab = grabCurve.points_[index]; const double newVel = grab.velocity + static_cast(dxPixels) * velPerPx; // Y increases downward but amp increases upward, so a downward drag (positive dy) LOWERS amp. const double newAmp = grab.amp - static_cast(dyPixels) * ampPerPx; out.movePoint(index, newVel, newAmp); // applies box + neighbour-X + endpoint-pin clamps return out; } bool VelocityCurve::equals(const VelocityCurve& other, double eps) const { if (points_.size() != other.points_.size()) return false; 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].amp - other.points_[i].amp) > eps) return false; } return true; } } // namespace reasampler::instrument::engine