Fix envelope-overlay knot/trace disagreement on odd pixel spans

Generalize curveMidLevel/curveFromMidLevel to curveLevelAt/curveFromLevelAt at
arbitrary phi; knotVtx and its drag inverse now read the phi a knot's truncated
x actually implies, not always 0.5.
This commit is contained in:
2026-08-01 21:31:40 -04:00
parent ee8a956fbd
commit a1b42ed1a8
7 changed files with 252 additions and 32 deletions
+94 -1
View File
@@ -10,7 +10,9 @@
// per-segment separation at the tier-0 defaults, overrun compression, every vertex in-bounds);
// splitAhdSeconds (A+H+D never exceeds the span, hold at 0% and 100%); the AHD polyline (1:1 with
// the time axis, origin offset); curve knots (present only on sloped non-zero segments, height
// following the exponent); the degenerate flat baseline.
// following the exponent, and — swept across ODD and EVEN pixel spans, not one fixture's width —
// sitting on the curve its own vertices imply rather than always the segment's exact midpoint);
// the degenerate flat baseline.
#include "../src/core/instrument/ui/envelope_overlay.h"
@@ -422,6 +424,95 @@ static void testKnotHeightTracksTheExponent() {
CHECK(steep.y >= a.y && steep.y <= a.bottom() - 1);
}
// --- the knot sits ON its own curve (the reported defect, stated as the gate) -------------
// The general (non-truncated-phi) reading of a knot's level, computed from the vertices
// `buildEnvelopePolyline` actually returned — x0/x1/knotX are all int pixels a caller can read
// off the polyline, so this is a check ON the output, not a restatement of knotVtx's own
// formula. x0 == x1 has no interior (no knot is ever built there).
static double expectedKnotLevel(int x0, int x1, int knotX, double startLevel, double endLevel,
double exponent) {
const double phi = (x1 != x0)
? static_cast<double>(knotX - x0) / static_cast<double>(x1 - x0)
: 0.5;
return startLevel + (endLevel - startLevel) * reasampler::util::curveMap(phi, exponent);
}
// The reported defect, stated as the gate: at every exponent the knot's centre lies on the
// trace, within 1 px. Swept over a range of canvas widths (down to a few pixels of stage span)
// so the check actually exercises ODD pixel spans, where the segment's true midpoint falls
// between two pixels — testKnotHeightTracksTheExponent above sits at a width whose span happens
// to be even, which is exactly the kind of fixture that missed this defect.
static void testKnotSitsOnItsOwnCurveAcrossOddAndEvenSpans() {
bool sawOdd = false, sawEven = false;
int worstAhdsr = 0, worstAhd = 0;
for (int width = 24; width <= 260; width += 3) {
const Rect a = Rect::ltrb(0, 0, width, 100);
for (double exp : {util::kCurveMin, 0.3, 1.0, 3.0, util::kCurveMax}) {
StageEnvelope e = ahdsr(0.4, 0.0, 0.0, 1.0, 0.0);
e.attackCurve = exp;
EnvVertex origin, attackEnd, knot;
const std::vector<EnvVertex> poly = buildEnvelopePolyline(e, overlayOf(a), 4.0);
if (findNode(poly, EnvNode::Origin, origin) &&
findNode(poly, EnvNode::AttackEnd, attackEnd) &&
findNode(poly, EnvNode::AttackCurve, knot)) {
const int span = attackEnd.x - origin.x;
if (span > 0) {
if (span % 2 == 0) sawEven = true; else sawOdd = true;
const double expected =
expectedKnotLevel(origin.x, attackEnd.x, knot.x, 0.0, 1.0, exp);
const int expectedY = levelToY(a, expected);
worstAhdsr = (std::max)(worstAhdsr, std::abs(knot.y - expectedY));
CHECK(std::abs(knot.y - expectedY) <= 1);
}
}
StageEnvelope f = ahd(0.4, 0.6, 0.5, 0.0, 3.0);
f.attackCurve = exp;
EnvVertex originAhd, attackEndAhd, knotAhd;
const std::vector<EnvVertex> polyAhd = buildEnvelopePolyline(f, overlayOf(a), 4.0);
if (findNode(polyAhd, EnvNode::Origin, originAhd) &&
findNode(polyAhd, EnvNode::AttackEnd, attackEndAhd) &&
findNode(polyAhd, EnvNode::AttackCurve, knotAhd)) {
const int span = attackEndAhd.x - originAhd.x;
if (span > 0) {
if (span % 2 == 0) sawEven = true; else sawOdd = true;
const double expected = expectedKnotLevel(originAhd.x, attackEndAhd.x,
knotAhd.x, 0.0, 1.0, exp);
const int expectedY = levelToY(a, expected);
worstAhd = (std::max)(worstAhd, std::abs(knotAhd.y - expectedY));
CHECK(std::abs(knotAhd.y - expectedY) <= 1);
}
}
}
}
CHECK(sawOdd); // the sweep actually exercised an odd-pixel span...
CHECK(sawEven); // ...and an even one, so this isn't resting on one fixture's luck.
std::printf(" worst knot/curve separation: AHDSR %d px, AHD %d px\n", worstAhdsr, worstAhd);
}
// Exponent 1.0 is still a plain straight line even off the segment's exact midpoint — checked
// at a deliberately ODD span so the linear case isn't only proven at the symmetric one.
static void testNeutralExponentIsAStraightLineOffCentre() {
bool found = false;
for (int width = 24; width <= 200 && !found; ++width) {
const Rect a = Rect::ltrb(0, 0, width, 100);
StageEnvelope e = ahdsr(0.4, 0.0, 0.0, 1.0, 0.0);
e.attackCurve = util::kCurveNeutral;
EnvVertex origin, attackEnd, knot;
const std::vector<EnvVertex> poly = buildEnvelopePolyline(e, overlayOf(a), 4.0);
if (!findNode(poly, EnvNode::Origin, origin)) continue;
if (!findNode(poly, EnvNode::AttackEnd, attackEnd)) continue;
if (!findNode(poly, EnvNode::AttackCurve, knot)) continue;
const int span = attackEnd.x - origin.x;
if (span <= 0 || span % 2 == 0) continue;
found = true;
const double phi = static_cast<double>(knot.x - origin.x) / static_cast<double>(span);
CHECK(std::fabs(knot.level - phi) < 1e-12); // linear: level == phi, exactly
}
CHECK(found); // the sweep must actually land on an odd span
}
// --- degenerate ---------------------------------------------------------------
static void testDegenerateSurfaceYieldsFlatBaseline() {
@@ -458,6 +549,8 @@ int main() {
testKnotsRideOnlySlopedNonZeroSegments();
testKnotHeightTracksTheExponent();
testKnotSitsOnItsOwnCurveAcrossOddAndEvenSpans();
testNeutralExponentIsAStraightLineOffCentre();
testDegenerateSurfaceYieldsFlatBaseline();