Cut core/instrument/engine comment bloat ~33% (comments only, zero code change)
This commit is contained in:
@@ -2,20 +2,19 @@
|
||||
|
||||
#include "core/instrument/engine/velocity_curve.h"
|
||||
|
||||
#include <algorithm> // std::max, std::min, std::abs, std::stable_sort
|
||||
#include <cmath> // std::fabs
|
||||
#include <utility> // std::move
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
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); }
|
||||
|
||||
// Pixel<->box maps (mirror of envelope_edit's timeToX/levelToY). X spans the width for [0,127]; Y
|
||||
// spans (height-1) rows for amp [0,1] with amp 1 at the TOP (y increases downward).
|
||||
// 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;
|
||||
@@ -35,7 +34,6 @@ int velToX(const VelocityCurve::Box& box, double velocity) {
|
||||
int ampToY(const VelocityCurve::Box& box, double amp) {
|
||||
const int h = std::max(0, box.height);
|
||||
if (h <= 1) return box.top;
|
||||
// amp 1 at top (box.top), amp 0 at bottom (box.top + h - 1).
|
||||
const double frac = (clampAmp(amp) - kAmpMin) / (kAmpMax - kAmpMin);
|
||||
return box.top + static_cast<int>((1.0 - frac) * static_cast<double>(h - 1) + 0.5);
|
||||
}
|
||||
@@ -44,19 +42,19 @@ int ampToY(const VelocityCurve::Box& box, double amp) {
|
||||
|
||||
VelocityCurve VelocityCurve::flat() {
|
||||
VelocityCurve c;
|
||||
c.points_ = {{kVelMin, kAmpMax}, {kVelMax, kAmpMax}}; // y = 1 everywhere (R10-F1 Option A)
|
||||
c.points_ = {{kVelMin, kAmpMax}, {kVelMax, kAmpMax}};
|
||||
return c;
|
||||
}
|
||||
|
||||
VelocityCurve VelocityCurve::linear() {
|
||||
VelocityCurve c;
|
||||
c.points_ = {{kVelMin, kAmpMin}, {kVelMax, kAmpMax}}; // y = velocity/127
|
||||
c.points_ = {{kVelMin, kAmpMin}, {kVelMax, kAmpMax}};
|
||||
return c;
|
||||
}
|
||||
|
||||
VelocityCurve VelocityCurve::fromPoints(std::vector<VelocityPoint> pts) {
|
||||
// Box-clamp every point, then stable-sort by velocity (X-order; stable so coincident-X points
|
||||
// keep their wire order). A stable sort keeps the eval well-defined for duplicate-X knots.
|
||||
// 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);
|
||||
@@ -65,18 +63,16 @@ VelocityCurve VelocityCurve::fromPoints(std::vector<VelocityPoint> pts) {
|
||||
[](const VelocityPoint& a, const VelocityPoint& b) {
|
||||
return a.velocity < b.velocity;
|
||||
});
|
||||
// Fewer than 2 usable points -> can't span [0,127] as a function; fall back to the flat default.
|
||||
if (pts.size() < 2) return flat();
|
||||
// Force endpoints present at velocity 0 and 127 (they must exist for eval to be total).
|
||||
if (pts.front().velocity > kVelMin) {
|
||||
pts.insert(pts.begin(), VelocityPoint{kVelMin, pts.front().amp});
|
||||
} else {
|
||||
pts.front().velocity = kVelMin; // snap a near-0 first point exactly onto the endpoint
|
||||
pts.front().velocity = kVelMin;
|
||||
}
|
||||
if (pts.back().velocity < kVelMax) {
|
||||
pts.push_back(VelocityPoint{kVelMax, pts.back().amp});
|
||||
} else {
|
||||
pts.back().velocity = kVelMax; // snap a near-127 last point exactly onto the endpoint
|
||||
pts.back().velocity = kVelMax;
|
||||
}
|
||||
VelocityCurve c;
|
||||
c.points_ = std::move(pts);
|
||||
@@ -85,19 +81,12 @@ VelocityCurve VelocityCurve::fromPoints(std::vector<VelocityPoint> pts) {
|
||||
|
||||
namespace {
|
||||
|
||||
// Fritsch–Carlson monotone-cubic tangent for one interior knot i, given the secant slopes of the
|
||||
// two adjacent segments (dPrev = secant into knot i, dNext = secant out of knot i). Returns the
|
||||
// limited tangent that keeps the cubic Hermite piece monotone and inside the data range.
|
||||
//
|
||||
// The rule: a tangent whose adjacent secants have opposite signs (or either is flat) is a local
|
||||
// extremum — pin the tangent to 0 so the curve does not overshoot past the knot. Otherwise use the
|
||||
// weighted-harmonic-mean tangent (Fritsch–Carlson eq. 4), which for COLLINEAR knots (dPrev==dNext)
|
||||
// reduces to that common secant — so collinear control points reproduce the straight line to within
|
||||
// floating-point rounding (~1e-15), preserving the Option-B / null-response contract for linear().
|
||||
// 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; // sign change or a flat neighbour -> local extremum
|
||||
// Weighted harmonic mean of the two secants (weights = the two segment widths). Collinear case:
|
||||
// dPrev==dNext==d makes this (w1+w2)*d / ((w1+w2)/... ) collapse to d exactly.
|
||||
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);
|
||||
@@ -106,33 +95,23 @@ double fritschCarlsonTangent(double dPrev, double dNext, double spanPrev, double
|
||||
} // namespace
|
||||
|
||||
double VelocityCurve::eval(double velocity) const {
|
||||
if (points_.empty()) return kAmpMax; // degenerate (shouldn't occur) -> flat unity
|
||||
if (points_.size() == 1) return clampAmp(points_[0].amp); // 1-point -> that point's amp
|
||||
if (points_.empty()) return kAmpMax;
|
||||
if (points_.size() == 1) return clampAmp(points_[0].amp);
|
||||
const double v = clampVelocity(velocity);
|
||||
// At or before the first point / at or after the last, read the endpoint amp (the endpoints are
|
||||
// at 0 and 127, so this only fires exactly at the ends for an in-range velocity).
|
||||
if (v <= points_.front().velocity) return clampAmp(points_.front().amp);
|
||||
if (v >= points_.back().velocity) return clampAmp(points_.back().amp);
|
||||
// Find the segment [points_[i], points_[i+1]] containing v (X-ordered, so a linear scan).
|
||||
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): jump straight to the later point's amp — the segment
|
||||
// has zero width so there is no interior to blend.
|
||||
// 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) interpolation on segment [a,b] ---------
|
||||
// Curved (spline) response, not straight lines. The interpolant provably stays within
|
||||
// [a.amp, b.amp] between the two knots (no bulge below 0 / above 1), and for collinear
|
||||
// control points its tangents reduce to the secant slope — so it reproduces the straight
|
||||
// line to within floating-point rounding (~1e-15), preserving linear()'s null-response
|
||||
// contract (y = velocity/127 to ~1e-15; the test tolerance of 1e-12 is appropriate).
|
||||
const double d = (b.amp - a.amp) / span; // secant of THIS segment
|
||||
// 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;
|
||||
|
||||
// Tangent at a: 0 if a is the first knot (endpoint), else the FC-limited tangent using
|
||||
// the previous segment's secant. Same for the tangent at b (0 at the last knot).
|
||||
double mA = d;
|
||||
if (i > 0) {
|
||||
const VelocityPoint& prev = points_[i - 1];
|
||||
@@ -141,7 +120,7 @@ double VelocityCurve::eval(double velocity) const {
|
||||
const double dPrev = (a.amp - prev.amp) / spanPrev;
|
||||
mA = fritschCarlsonTangent(dPrev, d, spanPrev, span);
|
||||
} else {
|
||||
mA = 0.0; // coincident-X predecessor (a step at a) -> flat tangent
|
||||
mA = 0.0;
|
||||
}
|
||||
}
|
||||
double mB = d;
|
||||
@@ -152,13 +131,10 @@ double VelocityCurve::eval(double velocity) const {
|
||||
const double dNext = (next.amp - b.amp) / spanNext;
|
||||
mB = fritschCarlsonTangent(d, dNext, span, spanNext);
|
||||
} else {
|
||||
mB = 0.0; // coincident-X successor (a step at b) -> flat tangent
|
||||
mB = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
// Cubic Hermite basis on the normalized position t across [a,b]. For collinear knots
|
||||
// mA==mB==d, so h00*a + (h10*span)*d + h01*b + (h11*span)*d collapses to the straight
|
||||
// line to within floating-point rounding (~1e-15).
|
||||
const double t = (v - a.velocity) / span;
|
||||
const double t2 = t * t;
|
||||
const double t3 = t2 * t;
|
||||
@@ -175,8 +151,7 @@ double VelocityCurve::eval(double velocity) const {
|
||||
|
||||
std::size_t VelocityCurve::addPoint(double velocity, double amp) {
|
||||
const VelocityPoint p{clampVelocity(velocity), clampAmp(amp)};
|
||||
// Insert keeping X-order: first index whose velocity is STRICTLY greater than the new one, so a
|
||||
// duplicate-X point lands immediately after the existing one (a later move can separate them).
|
||||
// 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);
|
||||
@@ -191,11 +166,10 @@ VelocityPoint VelocityCurve::movePoint(std::size_t index, double velocity, doubl
|
||||
double newAmp = clampAmp(amp);
|
||||
double newVel;
|
||||
if (isFirst) {
|
||||
newVel = kVelMin; // endpoint pinned in X at 0 — only amp moves
|
||||
newVel = kVelMin;
|
||||
} else if (isLast) {
|
||||
newVel = kVelMax; // endpoint pinned in X at 127 — only amp moves
|
||||
newVel = kVelMax;
|
||||
} else {
|
||||
// Interior point: clamp X strictly within its immediate neighbours so it can't cross them.
|
||||
const double lo = points_[index - 1].velocity;
|
||||
const double hi = points_[index + 1].velocity;
|
||||
newVel = std::clamp(clampVelocity(velocity), lo, hi);
|
||||
@@ -216,9 +190,7 @@ VelocityCurve::CurvePixel VelocityCurve::pixelFromPoint(const Box& box, const Ve
|
||||
}
|
||||
|
||||
VelocityPoint VelocityCurve::pointFromPixel(const Box& box, int x, int y) {
|
||||
// The exact inverse of velToX/ampToY (within the one-pixel rounding quantum). Degenerate
|
||||
// dimensions collapse the same way the forward map does: velToX pins to box.left (velocity 0),
|
||||
// ampToY pins to box.top (amp 1).
|
||||
// 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);
|
||||
|
||||
Reference in New Issue
Block a user