248 lines
10 KiB
C++
248 lines
10 KiB
C++
// velocity_curve.cpp — see velocity_curve.h. Pure eval + editing/clamp/inverse map; no host types.
|
|
|
|
#include "core/instrument/engine/velocity_curve.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <utility>
|
|
|
|
namespace reasampler::instrument::engine {
|
|
|
|
namespace {
|
|
|
|
double clampVelocity(double v) { return std::clamp(v, kVelMin, kVelMax); }
|
|
double clampValue(double a, CurveDomain d) { return std::clamp(a, curveYMin(d), kCurveYMax); }
|
|
|
|
// X spans the width for [0,127]; Y spans (height-1) rows for the domain's range with its max at
|
|
// the TOP (pixel y increases downward, so this axis is inverted relative to the value).
|
|
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<double>(w);
|
|
}
|
|
double valuePerPixel(const VelocityCurve::Box& box, CurveDomain d) {
|
|
const int h = std::max(0, box.height);
|
|
if (h <= 1) return 0.0;
|
|
return (kCurveYMax - curveYMin(d)) / static_cast<double>(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<int>(frac * static_cast<double>(w) + 0.5);
|
|
}
|
|
int valueToY(const VelocityCurve::Box& box, double value, CurveDomain d) {
|
|
const int h = std::max(0, box.height);
|
|
if (h <= 1) return box.top;
|
|
const double lo = curveYMin(d);
|
|
const double frac = (clampValue(value, d) - lo) / (kCurveYMax - lo);
|
|
return box.top + static_cast<int>((1.0 - frac) * static_cast<double>(h - 1) + 0.5);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
VelocityCurve VelocityCurve::flat() {
|
|
VelocityCurve c;
|
|
const double n = curveNeutral(CurveDomain::Unipolar);
|
|
c.points_ = {{kVelMin, n}, {kVelMax, n}};
|
|
return c;
|
|
}
|
|
|
|
VelocityCurve VelocityCurve::linear() {
|
|
VelocityCurve c;
|
|
c.points_ = {{kVelMin, 0.0}, {kVelMax, kCurveYMax}};
|
|
return c;
|
|
}
|
|
|
|
VelocityCurve VelocityCurve::zero() {
|
|
VelocityCurve c;
|
|
c.domain_ = CurveDomain::Bipolar;
|
|
const double n = curveNeutral(CurveDomain::Bipolar);
|
|
c.points_ = {{kVelMin, n}, {kVelMax, n}};
|
|
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);
|
|
}
|
|
std::stable_sort(pts.begin(), pts.end(),
|
|
[](const VelocityPoint& a, const VelocityPoint& b) {
|
|
return a.velocity < b.velocity;
|
|
});
|
|
if (pts.size() < 2) {
|
|
return domain == CurveDomain::Bipolar ? zero() : flat();
|
|
}
|
|
if (pts.front().velocity > kVelMin) {
|
|
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.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;
|
|
}
|
|
|
|
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];
|
|
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 clampValue(b.value, domain_);
|
|
const double d = (b.value - a.value) / span;
|
|
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)
|
|
}
|
|
|
|
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 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) {
|
|
if (index >= points_.size()) return VelocityPoint{}; // no-op (out of range)
|
|
const bool isFirst = (index == 0);
|
|
const bool isLast = (index + 1 == points_.size());
|
|
|
|
double newValue = clampValue(value, domain_);
|
|
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, newValue, points_[index].hard};
|
|
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<std::ptrdiff_t>(index));
|
|
return true;
|
|
}
|
|
|
|
VelocityCurve::CurvePixel VelocityCurve::pixelFromPoint(const Box& box,
|
|
const VelocityPoint& p) const {
|
|
return CurvePixel{velToX(box, p.velocity), valueToY(box, p.value, domain_)};
|
|
}
|
|
|
|
VelocityPoint VelocityCurve::pointFromPixel(const Box& box, int x, int y) const {
|
|
// Exact inverse of velToX/valueToY (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<double>(x - box.left) / static_cast<double>(w) *
|
|
(kVelMax - kVelMin));
|
|
const double lo = curveYMin(domain_);
|
|
p.value = (h <= 1)
|
|
? kCurveYMax
|
|
: clampValue(kCurveYMax - static_cast<double>(y - box.top) / static_cast<double>(h - 1) *
|
|
(kCurveYMax - lo),
|
|
domain_);
|
|
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 = valueToY(box, points_[i].value, domain_);
|
|
if (std::abs(x - px) <= kCurveNodeGrabRadius && std::abs(y - py) <= kCurveNodeGrabRadius) {
|
|
return static_cast<int>(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 valPerPx = valuePerPixel(box, grabCurve.domain_);
|
|
if (velPerPx <= 0.0 || valPerPx <= 0.0) return out; // degenerate box -> no motion
|
|
|
|
const VelocityPoint& grab = grabCurve.points_[index];
|
|
const double newVel = grab.velocity + static_cast<double>(dxPixels) * velPerPx;
|
|
// Y increases downward but the value increases upward, so a downward drag (positive dy)
|
|
// LOWERS the value.
|
|
const double newValue = grab.value - static_cast<double>(dyPixels) * valPerPx;
|
|
out.movePoint(index, newVel, newValue); // applies box + neighbour-X + endpoint-pin clamps
|
|
return out;
|
|
}
|
|
|
|
bool VelocityCurve::equals(const VelocityCurve& other, double eps) const {
|
|
if (domain_ != other.domain_) return false;
|
|
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].value - other.points_[i].value) > eps) return false;
|
|
if (points_[i].hard != other.points_[i].hard) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace reasampler::instrument::engine
|