instrument: fix AHD node-tracking/tie-break/live-latch defects and close staged-envelope-curve test gaps
This commit is contained in:
@@ -97,6 +97,18 @@ static void testDegenerateAreaAndDuration() {
|
||||
CHECK(gatePxPerSecond(Rect{}) == 0.0);
|
||||
}
|
||||
|
||||
// The literal PARAM-DOMAIN scale, independent of any sample duration: usable px = canvas width
|
||||
// minus the last column minus 4 node-separation bases, spread over 4 x kGateStageMaxSeconds.
|
||||
// This is what makes a dragged handle track the cursor 1:1 (envelope_edit's own inverse reads
|
||||
// this same function) — a scale regression here is exactly what a relational-only check misses.
|
||||
static void testGatePxPerSecond() {
|
||||
const double expected =
|
||||
(1000.0 - 1.0 - 4.0 * kGateNodeSepPx) / (4.0 * kGateStageMaxSeconds); // 967/8 px/s
|
||||
CHECK(gatePxPerSecond(wideArea()) == expected);
|
||||
CHECK(gatePxPerSecond(Rect::ltrb(5, 5, 5, 45)) == 0.0); // zero-width area -> 0
|
||||
CHECK(gatePxPerSecond(Rect::ltrb(0, 0, 10, 10)) > 0.0); // tiny area: usable floors at 1px, > 0
|
||||
}
|
||||
|
||||
// --- the AHDSR schematic ------------------------------------------------------
|
||||
|
||||
static void testAhdsrNodeOrderAndLevels() {
|
||||
@@ -122,6 +134,33 @@ static void testAhdsrNodeOrderAndLevels() {
|
||||
CHECK(v.x == a.right() - 1); // ANCHORED, whatever the release is
|
||||
}
|
||||
|
||||
// The literal per-node x placement, hand-derived from the documented formula (pps = 120.875
|
||||
// px/s per testGatePxPerSecond; each timed stage is prefixed by the kGateNodeSepPx=8 base):
|
||||
// attack .2s -> raw 8+24.175=32.175 -> px 32; hold .1s -> raw 32.175+8+12.0875=52.2625 -> px 52;
|
||||
// decay .3s -> raw 52.2625+8+36.2625=96.525 -> px 97; plateau -> raw 999-8-48.35=942.65 -> px
|
||||
// 943; release end pinned at the last column, 999. A literal regression pin — no relational or
|
||||
// bounds-only check catches a formula-shape change the way an exact pixel count does.
|
||||
static void testAhdsrSchematicPlacement() {
|
||||
const Rect a = wideArea();
|
||||
const std::vector<EnvVertex> poly =
|
||||
buildEnvelopePolyline(ahdsr(0.2, 0.1, 0.3, 0.5, 0.4), overlayOf(a), 4.0);
|
||||
EnvVertex v;
|
||||
CHECK(findNode(poly, EnvNode::AttackEnd, v) && v.x == a.x + 32);
|
||||
CHECK(findNode(poly, EnvNode::HoldEnd, v) && v.x == a.x + 52);
|
||||
CHECK(findNode(poly, EnvNode::DecayEnd, v) && v.x == a.x + 97);
|
||||
CHECK(findNode(poly, EnvNode::ReleaseStart, v) && v.x == a.x + 943);
|
||||
CHECK(findNode(poly, EnvNode::ReleaseEnd, v) && v.x == a.x + 999);
|
||||
}
|
||||
|
||||
// The AHDSR schematic is scaled by the PARAM domain, NOT the capture length: the same params
|
||||
// produce the SAME polyline whether totalSeconds is 0.3 or 10 (gatePolyline doesn't even take
|
||||
// totalSeconds — only the sustain-less AHD's x-axis is wall-clock/PCM-aligned).
|
||||
static void testGateLayoutIndependentOfSampleDuration() {
|
||||
const Rect a = wideArea();
|
||||
const StageEnvelope e = ahdsr(0.2, 0.1, 0.3, 0.5, 0.06);
|
||||
CHECK(buildEnvelopePolyline(e, overlayOf(a), 0.3) == buildEnvelopePolyline(e, overlayOf(a), 10.0));
|
||||
}
|
||||
|
||||
// The layout failure this policy exists to fix: at zero release the sustain plateau must run to
|
||||
// (near) the right edge instead of the figure bunching left.
|
||||
static void testZeroReleasePutsTheSustainPlateauAtTheRightEdge() {
|
||||
@@ -176,12 +215,29 @@ static void testMaxedStagesCompressWithoutOverrunning() {
|
||||
for (std::size_t i = 1; i < 6; ++i) {
|
||||
CHECK(poly[i].x >= poly[i - 1].x);
|
||||
CHECK(poly[i].x <= a.right() - 1);
|
||||
// The compression exists to preserve MINIMUM gaps under overrun, not merely
|
||||
// non-strict monotonicity — a compression that let two nodes collapse onto one
|
||||
// pixel would still pass a `>=` check but defeat the whole point of kGateNodeSepPx.
|
||||
CHECK(poly[i].x - poly[i - 1].x >= kGateNodeSepPx - 1);
|
||||
}
|
||||
EnvVertex end;
|
||||
CHECK(findNode(poly, EnvNode::ReleaseEnd, end));
|
||||
CHECK(end.x == a.right() - 1);
|
||||
}
|
||||
|
||||
// Absurd stage values must clamp in double space, not overflow the integer cast (32-bit long
|
||||
// on Windows would wrap negative and land on the WRONG edge) — a regression named for the bug
|
||||
// it once was. gateVtx's own double-space clamp is what this exercises.
|
||||
static void testAbsurdReleaseValueStaysInBounds() {
|
||||
const Rect a = wideArea();
|
||||
StageEnvelope huge = ahdsr(0.1, 0.1, 0.1, 0.5, 0.1);
|
||||
huge.releaseSeconds = 1e12;
|
||||
for (const EnvVertex& v : buildEnvelopePolyline(huge, overlayOf(a), 4.0)) {
|
||||
CHECK(v.x >= a.x && v.x < a.right());
|
||||
CHECK(v.y >= a.y && v.y < a.bottom());
|
||||
}
|
||||
}
|
||||
|
||||
// --- the AHD split ------------------------------------------------------------
|
||||
|
||||
// The combined-time bound, asserted structurally across the full domains: no (attack, decay,
|
||||
@@ -322,12 +378,16 @@ int main() {
|
||||
testTimeToXClampsBothEnds();
|
||||
testLevelToY();
|
||||
testDegenerateAreaAndDuration();
|
||||
testGatePxPerSecond();
|
||||
|
||||
testAhdsrNodeOrderAndLevels();
|
||||
testAhdsrSchematicPlacement();
|
||||
testGateLayoutIndependentOfSampleDuration();
|
||||
testZeroReleasePutsTheSustainPlateauAtTheRightEdge();
|
||||
testReleaseGrowsLeftwardFromTheAnchor();
|
||||
testTierZeroDefaultsKeepEveryNodeDistinct();
|
||||
testMaxedStagesCompressWithoutOverrunning();
|
||||
testAbsurdReleaseValueStaysInBounds();
|
||||
|
||||
testAhdSplitNeverExceedsTheSpan();
|
||||
testHoldFractionEndpoints();
|
||||
|
||||
Reference in New Issue
Block a user