FA2: param-domain Gate schematic + 8px node min-sep (all nodes grabbable at defaults); cross-mode drag guard; double-clamp px overflow fix
This commit is contained in:
@@ -5,11 +5,14 @@
|
||||
// fade/%-length shape at the waveform time base.
|
||||
//
|
||||
// Covers: timeToX / levelToY (linear maps, edge clamps, past-end CLAMPED to right-1 — the FA2
|
||||
// bounds invariant, degenerate area/duration); gateTimedWidth; buildEnvelopePolyline Gate (node
|
||||
// order, levels, timed-region placement, fixed sustain-plateau reserve, release visible
|
||||
// in-bounds, overrun pile-up clamped at the right edge, every vertex in-bounds);
|
||||
// buildEnvelopePolyline Trigger (fade-in/unity/fade-out at fractions of the played span, overlap
|
||||
// clamp, full-length/zero-fade-out nodes in-bounds at right-1); degenerate flat baseline.
|
||||
// bounds invariant, no 32-bit overflow on huge times, degenerate area/duration); gateTimedWidth
|
||||
// + gatePxPerSecond; buildEnvelopePolyline Gate (node order, levels, PARAM-DOMAIN timed-region
|
||||
// placement independent of sample duration, per-segment kGateNodeSepPx separation — every node
|
||||
// distinct even at the tier-0 zero-hold/zero-decay defaults, fixed sustain-plateau reserve,
|
||||
// release visible in-bounds, overrun compressed from the right preserving the minimum gaps,
|
||||
// every vertex in-bounds); buildEnvelopePolyline Trigger (fade-in/unity/fade-out at fractions of
|
||||
// the played span, overlap clamp, full-length/zero-fade-out nodes in-bounds at right-1);
|
||||
// degenerate flat baseline.
|
||||
|
||||
#include "../src/vst/envelope_overlay.h"
|
||||
|
||||
@@ -53,6 +56,9 @@ static void testTimeToXPastEndClamps() {
|
||||
const Rect a = wideArea();
|
||||
CHECK(timeToX(a, 2.0, 3.0) == a.right - 1);
|
||||
CHECK(timeToX(a, 2.0, 1000.0) == a.right - 1);
|
||||
// A HUGE t must clamp in double space, not overflow the integer cast (32-bit long on
|
||||
// Windows would wrap to LONG_MIN and pin to the WRONG edge).
|
||||
CHECK(timeToX(a, 2.0, 1e15) == a.right - 1);
|
||||
}
|
||||
|
||||
static void testGateTimedWidth() {
|
||||
@@ -63,6 +69,15 @@ static void testGateTimedWidth() {
|
||||
CHECK(gateTimedWidth(Rect{0, 0, 1, 10}) == 1);
|
||||
}
|
||||
|
||||
static void testGatePxPerSecond() {
|
||||
// PARAM-DOMAIN scale: (timedW - 1 - 4*sep) px spread over 4 x kGateStageMaxSeconds. For the
|
||||
// 1000px canvas: (850 - 1 - 32) / 8.0s = 817/8 px/s. Independent of any sample duration.
|
||||
const double expected = 817.0 / (4.0 * kGateStageMaxSeconds);
|
||||
CHECK(gatePxPerSecond(wideArea()) == expected);
|
||||
CHECK(gatePxPerSecond(Rect{5, 5, 5, 45}) == 0.0); // zero-width area -> 0
|
||||
CHECK(gatePxPerSecond(Rect{0, 0, 10, 10}) > 0.0); // tiny area: usable floors at 1px, > 0
|
||||
}
|
||||
|
||||
static void testTimeToXDegenerate() {
|
||||
const Rect a = wideArea();
|
||||
CHECK(timeToX(a, 0.0, 1.0) == a.left); // no duration -> left
|
||||
@@ -118,26 +133,55 @@ static void testGateNodeOrderAndLevels() {
|
||||
}
|
||||
|
||||
static void testGateSchematicPlacement() {
|
||||
// FA2 bounded schematic: timed region = 850px (150px reserved plateau), total 2.0s =>
|
||||
// 425 px/s in the timed region. attack .2 -> x@85, hold end .3 -> x@round(127.5)=128, decay
|
||||
// end .6 -> x@255. Plateau is the FIXED 150px reserve -> ReleaseStart x@405. Release .4 ->
|
||||
// 170px ramp -> ReleaseEnd x@575, well inside the canvas.
|
||||
// FA2 bounded schematic at the PARAM-DOMAIN scale: timed region = 850px (150px reserved
|
||||
// plateau), pps = (850-1-32)/8s = 102.125 px/s, each segment prefixed by the 8px separation
|
||||
// base. attack .2 -> x@round(8+20.425)=28; hold .1 -> x@round(28.425+8+10.2125)=47; decay
|
||||
// .3 -> x@round(46.6375+8+30.6375)=85; plateau is the FIXED 150px reserve -> ReleaseStart
|
||||
// x@235; release .4 -> x@round(235.275+8+40.85)=284, well inside the canvas.
|
||||
AmpEnvelope env;
|
||||
env.mode = EnvMode::Gate;
|
||||
env.attackSeconds = 0.2;
|
||||
env.holdSeconds = 0.1; // hold end at 0.3s
|
||||
env.decaySeconds = 0.3; // decay end at 0.6s
|
||||
env.holdSeconds = 0.1;
|
||||
env.decaySeconds = 0.3;
|
||||
env.sustainLevel = 0.5;
|
||||
env.releaseSeconds = 0.4;
|
||||
const Rect a = wideArea();
|
||||
const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, a, 2.0);
|
||||
|
||||
EnvVertex v;
|
||||
CHECK(findNode(poly, EnvNode::AttackEnd, v) && v.x == a.left + 85);
|
||||
CHECK(findNode(poly, EnvNode::HoldEnd, v) && v.x == a.left + 128);
|
||||
CHECK(findNode(poly, EnvNode::DecayEnd, v) && v.x == a.left + 255);
|
||||
CHECK(findNode(poly, EnvNode::ReleaseStart, v) && v.x == a.left + 405);
|
||||
CHECK(findNode(poly, EnvNode::ReleaseEnd, v) && v.x == a.left + 575);
|
||||
CHECK(findNode(poly, EnvNode::AttackEnd, v) && v.x == a.left + 28);
|
||||
CHECK(findNode(poly, EnvNode::HoldEnd, v) && v.x == a.left + 47);
|
||||
CHECK(findNode(poly, EnvNode::DecayEnd, v) && v.x == a.left + 85);
|
||||
CHECK(findNode(poly, EnvNode::ReleaseStart, v) && v.x == a.left + 235);
|
||||
CHECK(findNode(poly, EnvNode::ReleaseEnd, v) && v.x == a.left + 284);
|
||||
}
|
||||
|
||||
static void testGateLayoutIndependentOfSampleDuration() {
|
||||
// The Gate schematic is scaled by the PARAM domain, NOT the capture length: the same params
|
||||
// produce the SAME polyline over a 0.3s and a 10s sample (pre-fix, a 60ms release on a 10s
|
||||
// capture collapsed to ~5px while 2s stages on a 0.3s capture pinned to the right edge).
|
||||
AmpEnvelope env;
|
||||
env.mode = EnvMode::Gate;
|
||||
env.attackSeconds = 0.2;
|
||||
env.holdSeconds = 0.1;
|
||||
env.decaySeconds = 0.3;
|
||||
env.sustainLevel = 0.5;
|
||||
env.releaseSeconds = 0.06;
|
||||
const Rect a = wideArea();
|
||||
CHECK(buildEnvelopePolyline(env, a, 0.3) == buildEnvelopePolyline(env, a, 10.0));
|
||||
}
|
||||
|
||||
static void testGateMinSeparationAtDefaults() {
|
||||
// THE FA2 headline: at the tier-0 Gate defaults (attack 3ms, hold 0, decay 0, sustain 1.0,
|
||||
// release 60ms) every consecutive node pair is at least kGateNodeSepPx apart — no node ever
|
||||
// renders on top of its neighbour, so each is individually grabbable.
|
||||
const AmpEnvelope env; // struct defaults ARE the tier-0 Gate defaults
|
||||
const Rect a = wideArea();
|
||||
const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, a, 2.0);
|
||||
CHECK(poly.size() == 6);
|
||||
for (size_t i = 1; i < poly.size(); ++i) {
|
||||
CHECK(poly[i].x - poly[i - 1].x >= kGateNodeSepPx);
|
||||
}
|
||||
}
|
||||
|
||||
static void testGateSustainPlateauFixedWidth() {
|
||||
@@ -183,28 +227,32 @@ static void testGateReleaseVisibleInBounds() {
|
||||
CHECK(rel.level == 0.0);
|
||||
}
|
||||
|
||||
static void testGateOverrunClampsToCanvas() {
|
||||
// A/H/D/R overrun the visible span (sum 3.5s > 2.0s total): the trailing nodes pile up at
|
||||
// the last in-bounds column — monotonic, in-bounds, still individually draggable back left.
|
||||
// NOTHING maps past area.right (the pre-FA2 release tail is gone).
|
||||
static void testGateOverrunCompressesFromRight() {
|
||||
// Stages BEYOND the schematic domain (4.0s each > kGateStageMaxSeconds): the layout
|
||||
// compresses from the right preserving the minimum gaps — ReleaseEnd pins to the last
|
||||
// in-bounds column, but the trailing nodes stay strictly increasing and individually
|
||||
// separated (>= kGateNodeSepPx), NOT piled on one pixel. NOTHING maps past area.right.
|
||||
AmpEnvelope env;
|
||||
env.mode = EnvMode::Gate;
|
||||
env.attackSeconds = 1.0;
|
||||
env.holdSeconds = 1.0;
|
||||
env.decaySeconds = 1.0; // decay end at 3.0s -> already past the timed span
|
||||
env.attackSeconds = 4.0;
|
||||
env.holdSeconds = 4.0;
|
||||
env.decaySeconds = 4.0;
|
||||
env.sustainLevel = 0.7;
|
||||
env.releaseSeconds = 0.5;
|
||||
env.releaseSeconds = 4.0;
|
||||
const Rect a = wideArea();
|
||||
const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, a, 2.0);
|
||||
CHECK(poly.size() == 6);
|
||||
|
||||
EnvVertex decay, plateauEnd, rel;
|
||||
CHECK(findNode(poly, EnvNode::DecayEnd, decay));
|
||||
EnvVertex plateauEnd, rel;
|
||||
CHECK(findNode(poly, EnvNode::ReleaseStart, plateauEnd));
|
||||
CHECK(findNode(poly, EnvNode::ReleaseEnd, rel));
|
||||
CHECK(decay.x == a.right - 1); // pinned to the last in-bounds column
|
||||
CHECK(plateauEnd.x == a.right - 1);
|
||||
CHECK(rel.x == a.right - 1);
|
||||
CHECK(rel.x == a.right - 1); // pinned to the last in-bounds column
|
||||
CHECK(plateauEnd.level == 0.7); // still at sustain
|
||||
for (size_t i = 1; i < poly.size(); ++i) {
|
||||
CHECK(poly[i].x > poly[i - 1].x); // strictly monotonic
|
||||
CHECK(poly[i].x - poly[i - 1].x >= kGateNodeSepPx - 1); // min gaps survive compression
|
||||
CHECK(poly[i].x >= a.left && poly[i].x < a.right); // in-bounds
|
||||
}
|
||||
}
|
||||
|
||||
static void testGateAllVerticesInBounds() {
|
||||
@@ -223,8 +271,13 @@ static void testGateAllVerticesInBounds() {
|
||||
AmpEnvelope trig = base;
|
||||
trig.mode = EnvMode::Trigger;
|
||||
trig.lengthFraction = 1.0; trig.fadeInFraction = 0.0; trig.fadeOutFraction = 0.0;
|
||||
// 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).
|
||||
AmpEnvelope huge = base;
|
||||
huge.mode = EnvMode::Gate;
|
||||
huge.releaseSeconds = 1e12;
|
||||
|
||||
for (const AmpEnvelope& env : {base, big, zero, trig}) {
|
||||
for (const AmpEnvelope& env : {base, big, zero, trig, huge}) {
|
||||
for (const EnvVertex& v : buildEnvelopePolyline(env, a, 2.0)) {
|
||||
CHECK(v.x >= a.left && v.x < a.right);
|
||||
CHECK(v.y >= a.top && v.y < a.bottom);
|
||||
@@ -316,14 +369,17 @@ int main() {
|
||||
testTimeToXPastEndClamps();
|
||||
testTimeToXDegenerate();
|
||||
testGateTimedWidth();
|
||||
testGatePxPerSecond();
|
||||
testLevelToYEndpoints();
|
||||
testLevelToYClamps();
|
||||
|
||||
testGateNodeOrderAndLevels();
|
||||
testGateSchematicPlacement();
|
||||
testGateLayoutIndependentOfSampleDuration();
|
||||
testGateMinSeparationAtDefaults();
|
||||
testGateSustainPlateauFixedWidth();
|
||||
testGateReleaseVisibleInBounds();
|
||||
testGateOverrunClampsToCanvas();
|
||||
testGateOverrunCompressesFromRight();
|
||||
testGateAllVerticesInBounds();
|
||||
|
||||
testTriggerShape();
|
||||
|
||||
Reference in New Issue
Block a user