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:
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user