One taper, one modifier law: extract param_taper, raise the stage ceiling to 10 s, and make the AHDSR schematic axis the taper itself
This commit is contained in:
@@ -4,15 +4,18 @@
|
||||
// RIGHT-ANCHORED release, and the sustain-less AHD laid 1:1 over the waveform's time axis.
|
||||
//
|
||||
// Covers: timeToX / levelToY (linear maps, edge clamps, past-end clamped to right-1, no 32-bit
|
||||
// overflow on huge times, degenerate area/duration); gatePxPerSecond; the AHDSR polyline (node
|
||||
// order, levels, release anchored at the right edge, the sustain plateau reaching the edge at
|
||||
// zero release, 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.
|
||||
// overflow on huge times, degenerate area/duration); gateStageSlotPx; the AHDSR polyline (node
|
||||
// order, levels, the TAPERED stage placement and its legibility at both ends of the range,
|
||||
// release anchored at the right edge, the sustain plateau reaching the edge at zero release,
|
||||
// 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.
|
||||
|
||||
#include "../src/core/instrument/ui/envelope_overlay.h"
|
||||
|
||||
#include "../src/core/instrument/ui/sample_bands.h" // the editor floor the legibility test uses
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
@@ -94,20 +97,19 @@ static void testDegenerateAreaAndDuration() {
|
||||
CHECK(timeToX(Rect{}, 2.0, 1.0) == 0);
|
||||
CHECK(timeToX(wideArea(), 0.0, 1.0) == wideArea().x);
|
||||
CHECK(levelToY(Rect{}, 0.5) == 0);
|
||||
CHECK(gatePxPerSecond(Rect{}) == 0.0);
|
||||
CHECK(gateStageSlotPx(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() {
|
||||
// 967 / 8 px/s, pinned as a literal — restating the formula with the same named constants
|
||||
// would let a change to kGateNodeSepPx or kGateStageMaxSeconds move both sides and pass
|
||||
// silently.
|
||||
CHECK(gatePxPerSecond(wideArea()) == 120.875);
|
||||
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 literal slot width, independent of any sample duration: usable px = canvas width minus the
|
||||
// last column minus 4 node-separation bases, split four ways. A stage then occupies its own
|
||||
// TAPERED fraction of that slot, which is what makes a dragged handle track the cursor at both
|
||||
// ends of the range — a scale regression here is exactly what a relational-only check misses.
|
||||
static void testGateStageSlotPx() {
|
||||
// 967 / 4 px, pinned as a literal — restating the formula with the same named constants
|
||||
// would let a change to kGateNodeSepPx move both sides and pass silently.
|
||||
CHECK(gateStageSlotPx(wideArea()) == 241.75);
|
||||
CHECK(gateStageSlotPx(Rect::ltrb(5, 5, 5, 45)) == 0.0); // zero-width area -> 0
|
||||
CHECK(gateStageSlotPx(Rect::ltrb(0, 0, 10, 10)) > 0.0); // tiny area: usable floors at 1px, > 0
|
||||
}
|
||||
|
||||
// --- the AHDSR schematic ------------------------------------------------------
|
||||
@@ -135,24 +137,52 @@ 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.
|
||||
// The literal per-node x placement, hand-derived from the documented formula (slot = 241.75 px
|
||||
// per testGateStageSlotPx; each timed stage is prefixed by the kGateNodeSepPx=8 base and occupies
|
||||
// slot x timeNormFromSeconds(t) of its own slot; L = ln(1 + 10/0.003) = 8.112028):
|
||||
// attack .25s -> norm ln(84.3333)/L = 0.546677 -> 8 + 132.159 = 140.159 -> px 140
|
||||
// hold .05s -> norm ln(17.6667)/L = 0.354007 -> 140.159 + 8 + 85.581 = 233.740 -> px 234
|
||||
// decay .5s -> norm ln(167.667)/L = 0.631385 -> 233.740 + 8 + 152.637 = 394.377 -> px 394
|
||||
// plateau 1s -> norm ln(334.333)/L = 0.716493 -> 999 - 8 - 173.211 = 817.789 -> px 818
|
||||
// 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);
|
||||
buildEnvelopePolyline(ahdsr(0.25, 0.05, 0.5, 0.5, 1.0), 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::AttackEnd, v) && v.x == a.x + 140);
|
||||
CHECK(findNode(poly, EnvNode::HoldEnd, v) && v.x == a.x + 234);
|
||||
CHECK(findNode(poly, EnvNode::DecayEnd, v) && v.x == a.x + 394);
|
||||
CHECK(findNode(poly, EnvNode::ReleaseStart, v) && v.x == a.x + 818);
|
||||
CHECK(findNode(poly, EnvNode::ReleaseEnd, v) && v.x == a.x + 999);
|
||||
}
|
||||
|
||||
// The legibility the tapered axis exists for, at BOTH ends of the raised range. Linear-in-seconds
|
||||
// put the 3 ms default attack 0.07 px from the origin at a 10 s ceiling — indistinguishable from
|
||||
// zero and impossible to grab. Asserted at the editor's own floor width, not a comfortable one.
|
||||
static void testTaperedAxisKeepsBothEndsOfTheRangeLegible() {
|
||||
const Rect floorArea = Rect::ltrb(0, 0, kEditorMinWidth - 2 * kPad, 100);
|
||||
const std::vector<EnvVertex> poly =
|
||||
buildEnvelopePolyline(ahdsr(0.003, 0.0, 0.0, 1.0, 0.060), overlayOf(floorArea), 4.0);
|
||||
EnvVertex origin, attack;
|
||||
CHECK(findNode(poly, EnvNode::Origin, origin));
|
||||
CHECK(findNode(poly, EnvNode::AttackEnd, attack));
|
||||
// Well clear of the grab radius, so the default attack is a real handle rather than a node
|
||||
// sitting on the origin.
|
||||
CHECK(attack.x - origin.x >= 20);
|
||||
|
||||
// And a maxed stage still lands its end node at its slot's edge: the taper's norm-1 end and
|
||||
// the schematic's canvas edge are the same place, which is the anchor the policy rests on.
|
||||
const std::vector<EnvVertex> maxed = buildEnvelopePolyline(
|
||||
ahdsr(kGateStageMaxSeconds, 0.0, 0.0, 1.0, 0.0), overlayOf(floorArea), 4.0);
|
||||
EnvVertex maxAttack;
|
||||
CHECK(findNode(maxed, EnvNode::AttackEnd, maxAttack));
|
||||
const double slot = gateStageSlotPx(floorArea);
|
||||
CHECK(maxAttack.x == floorArea.x + static_cast<int>(kGateNodeSepPx + slot + 0.5));
|
||||
}
|
||||
|
||||
// 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).
|
||||
@@ -409,10 +439,11 @@ int main() {
|
||||
testTimeToXClampsBothEnds();
|
||||
testLevelToY();
|
||||
testDegenerateAreaAndDuration();
|
||||
testGatePxPerSecond();
|
||||
testGateStageSlotPx();
|
||||
|
||||
testAhdsrNodeOrderAndLevels();
|
||||
testAhdsrSchematicPlacement();
|
||||
testTaperedAxisKeepsBothEndsOfTheRangeLegible();
|
||||
testGateLayoutIndependentOfSampleDuration();
|
||||
testZeroReleasePutsTheSustainPlateauAtTheRightEdge();
|
||||
testReleaseGrowsLeftwardFromTheAnchor();
|
||||
|
||||
Reference in New Issue
Block a user