instrument: one VELOCITY deck for all three velocity curves, bipolar and off by default for pitch and filter
Payload v12 appends the new velocity->pitch curve and folds the retired filter velAmount into its now-bipolar curve, so pre-v12 projects reopen sounding identical. Preview button takes a drawn play triangle.
This commit is contained in:
@@ -11,19 +11,19 @@ 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); }
|
||||
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 amp [0,1] with amp 1 at the TOP
|
||||
// (pixel y increases downward, so this axis is inverted relative to amp).
|
||||
// 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 ampPerPixel(const VelocityCurve::Box& box) {
|
||||
double valuePerPixel(const VelocityCurve::Box& box, CurveDomain d) {
|
||||
const int h = std::max(0, box.height);
|
||||
if (h <= 1) return 0.0;
|
||||
return (kAmpMax - kAmpMin) / static_cast<double>(h - 1);
|
||||
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);
|
||||
@@ -31,10 +31,11 @@ int velToX(const VelocityCurve::Box& box, double velocity) {
|
||||
const double frac = (clampVelocity(velocity) - kVelMin) / (kVelMax - kVelMin);
|
||||
return box.left + static_cast<int>(frac * static_cast<double>(w) + 0.5);
|
||||
}
|
||||
int ampToY(const VelocityCurve::Box& box, double amp) {
|
||||
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 frac = (clampAmp(amp) - kAmpMin) / (kAmpMax - kAmpMin);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -42,39 +43,49 @@ int ampToY(const VelocityCurve::Box& box, double amp) {
|
||||
|
||||
VelocityCurve VelocityCurve::flat() {
|
||||
VelocityCurve c;
|
||||
c.points_ = {{kVelMin, kAmpMax}, {kVelMax, kAmpMax}};
|
||||
c.points_ = {{kVelMin, kCurveYMax}, {kVelMax, kCurveYMax}};
|
||||
return c;
|
||||
}
|
||||
|
||||
VelocityCurve VelocityCurve::linear() {
|
||||
VelocityCurve c;
|
||||
c.points_ = {{kVelMin, kAmpMin}, {kVelMax, kAmpMax}};
|
||||
c.points_ = {{kVelMin, 0.0}, {kVelMax, kCurveYMax}};
|
||||
return c;
|
||||
}
|
||||
|
||||
VelocityCurve VelocityCurve::fromPoints(std::vector<VelocityPoint> pts) {
|
||||
VelocityCurve VelocityCurve::zero() {
|
||||
VelocityCurve c;
|
||||
c.domain_ = CurveDomain::Bipolar;
|
||||
c.points_ = {{kVelMin, 0.0}, {kVelMax, 0.0}};
|
||||
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).
|
||||
for (VelocityPoint& p : pts) {
|
||||
p.velocity = clampVelocity(p.velocity);
|
||||
p.amp = clampAmp(p.amp);
|
||||
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 flat();
|
||||
if (pts.size() < 2) {
|
||||
return domain == CurveDomain::Bipolar ? zero() : flat();
|
||||
}
|
||||
if (pts.front().velocity > kVelMin) {
|
||||
pts.insert(pts.begin(), VelocityPoint{kVelMin, pts.front().amp});
|
||||
pts.insert(pts.begin(), VelocityPoint{kVelMin, pts.front().value});
|
||||
} else {
|
||||
pts.front().velocity = kVelMin;
|
||||
}
|
||||
if (pts.back().velocity < kVelMax) {
|
||||
pts.push_back(VelocityPoint{kVelMax, pts.back().amp});
|
||||
pts.push_back(VelocityPoint{kVelMax, pts.back().value});
|
||||
} else {
|
||||
pts.back().velocity = kVelMax;
|
||||
}
|
||||
VelocityCurve c;
|
||||
c.domain_ = domain;
|
||||
c.points_ = std::move(pts);
|
||||
return c;
|
||||
}
|
||||
@@ -84,7 +95,8 @@ 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.
|
||||
// makes the spline reproduce a straight line for linear()-style input. Homogeneous of degree 1
|
||||
// in the secants, which is what makes eval homogeneous in y (see the header).
|
||||
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;
|
||||
@@ -95,29 +107,29 @@ double fritschCarlsonTangent(double dPrev, double dNext, double spanPrev, double
|
||||
} // namespace
|
||||
|
||||
double VelocityCurve::eval(double velocity) const {
|
||||
if (points_.empty()) return kAmpMax;
|
||||
if (points_.size() == 1) return clampAmp(points_[0].amp);
|
||||
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 clampAmp(points_.front().amp);
|
||||
if (v >= points_.back().velocity) return clampAmp(points_.back().amp);
|
||||
if (v <= points_.front().velocity) return clampValue(points_.front().value, domain_);
|
||||
if (v >= points_.back().velocity) return clampValue(points_.back().value, domain_);
|
||||
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);
|
||||
if (span <= 0.0) return clampValue(b.value, domain_);
|
||||
|
||||
// Monotone cubic Hermite (Fritsch-Carlson): provably stays within [a.amp, b.amp]
|
||||
// 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.amp - a.amp) / span;
|
||||
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.amp - prev.amp) / spanPrev;
|
||||
const double dPrev = (a.value - prev.value) / spanPrev;
|
||||
mA = fritschCarlsonTangent(dPrev, d, spanPrev, span);
|
||||
} else {
|
||||
mA = 0.0;
|
||||
@@ -128,7 +140,7 @@ double VelocityCurve::eval(double velocity) const {
|
||||
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;
|
||||
const double dNext = (next.value - b.value) / spanNext;
|
||||
mB = fritschCarlsonTangent(d, dNext, span, spanNext);
|
||||
} else {
|
||||
mB = 0.0;
|
||||
@@ -142,15 +154,15 @@ double VelocityCurve::eval(double velocity) const {
|
||||
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);
|
||||
const double y = h00 * a.value + h10 * span * mA + h01 * b.value + h11 * span * mB;
|
||||
return clampValue(y, domain_);
|
||||
}
|
||||
}
|
||||
return clampAmp(points_.back().amp); // unreachable (v is between the endpoints)
|
||||
return clampValue(points_.back().value, domain_); // unreachable (v is between the endpoints)
|
||||
}
|
||||
|
||||
std::size_t VelocityCurve::addPoint(double velocity, double amp) {
|
||||
const VelocityPoint p{clampVelocity(velocity), clampAmp(amp)};
|
||||
std::size_t VelocityCurve::addPoint(double velocity, double value) {
|
||||
const VelocityPoint p{clampVelocity(velocity), clampValue(value, domain_)};
|
||||
// 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;
|
||||
@@ -158,12 +170,12 @@ std::size_t VelocityCurve::addPoint(double velocity, double amp) {
|
||||
return i;
|
||||
}
|
||||
|
||||
VelocityPoint VelocityCurve::movePoint(std::size_t index, double velocity, double amp) {
|
||||
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 newAmp = clampAmp(amp);
|
||||
double newValue = clampValue(value, domain_);
|
||||
double newVel;
|
||||
if (isFirst) {
|
||||
newVel = kVelMin;
|
||||
@@ -174,7 +186,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, newAmp};
|
||||
points_[index] = VelocityPoint{newVel, newValue};
|
||||
return points_[index];
|
||||
}
|
||||
|
||||
@@ -185,12 +197,13 @@ bool VelocityCurve::deletePoint(std::size_t index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
VelocityCurve::CurvePixel VelocityCurve::pixelFromPoint(const Box& box, const VelocityPoint& p) {
|
||||
return CurvePixel{velToX(box, p.velocity), ampToY(box, p.amp)};
|
||||
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) {
|
||||
// Exact inverse of velToX/ampToY (within one pixel); degenerate dims collapse the same way.
|
||||
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);
|
||||
@@ -198,17 +211,19 @@ VelocityPoint VelocityCurve::pointFromPixel(const Box& box, int x, int y) {
|
||||
? kVelMin
|
||||
: clampVelocity(kVelMin + static_cast<double>(x - box.left) / static_cast<double>(w) *
|
||||
(kVelMax - kVelMin));
|
||||
p.amp = (h <= 1)
|
||||
? kAmpMax
|
||||
: clampAmp(kAmpMax - static_cast<double>(y - box.top) / static_cast<double>(h - 1) *
|
||||
(kAmpMax - kAmpMin));
|
||||
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 = ampToY(box, points_[i].amp);
|
||||
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);
|
||||
}
|
||||
@@ -221,22 +236,24 @@ VelocityCurve VelocityCurve::resolvePointDrag(const VelocityCurve& grabCurve, st
|
||||
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 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 amp increases upward, so a downward drag (positive dy) LOWERS amp.
|
||||
const double newAmp = grab.amp - static_cast<double>(dyPixels) * ampPerPx;
|
||||
out.movePoint(index, newVel, newAmp); // applies box + neighbour-X + endpoint-pin clamps
|
||||
// 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].amp - other.points_[i].amp) > eps) return false;
|
||||
if (std::fabs(points_[i].value - other.points_[i].value) > eps) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user