FA2: bounded-schematic Gate envelope (15% sustain reserve, in-bounds release) + zero-fade-out grabbable via nearest-node hit-test; all nodes clamp in-canvas

This commit is contained in:
2026-07-27 18:25:03 -04:00
parent e2bd4f4351
commit d5d1902ea4
6 changed files with 392 additions and 121 deletions
+46 -17
View File
@@ -10,13 +10,22 @@ int timeToX(const Rect& area, double totalSeconds, double t) {
const int w = std::max(0, area.width());
if (w <= 0 || totalSeconds <= 0.0) return area.left;
if (t < 0.0) t = 0.0;
// Linear map, NOT clamped on the high side: t past totalSeconds maps past area.right (the Gate
// release tail, drawn after the sample end by design). Round to the nearest pixel.
// Linear map, clamped on BOTH sides (FA2 bounds invariant): t past totalSeconds pins to the
// last in-bounds column area.right-1. Round to the nearest pixel.
const double frac = t / totalSeconds;
const long xi = static_cast<long>(frac * static_cast<double>(w) + 0.5);
long xi = static_cast<long>(frac * static_cast<double>(w) + 0.5);
if (xi > w - 1) xi = w - 1;
return area.left + static_cast<int>(xi);
}
int gateTimedWidth(const Rect& area) {
const int w = std::max(0, area.width());
if (w <= 0) return 0;
const int sustainPx =
static_cast<int>(kGateSustainDisplayFraction * static_cast<double>(w) + 0.5);
return std::max(1, w - sustainPx);
}
int levelToY(const Rect& area, double level) {
const int h = std::max(0, area.height());
if (h <= 0) return area.top;
@@ -46,6 +55,22 @@ EnvVertex vtx(EnvNode node, const Rect& area, double totalSeconds, double t, dou
return v;
}
// One Gate vertex from a pixel offset inside the area (the Gate schematic works in px space —
// timed px + the fixed sustain-plateau reserve — not through the plain timeToX map). Clamps x to
// the last in-bounds column (FA2 bounds invariant).
EnvVertex gateVtx(EnvNode node, const Rect& area, double px, double level) {
const int w = std::max(1, area.width());
long xi = static_cast<long>(px + 0.5);
if (xi < 0) xi = 0;
if (xi > w - 1) xi = w - 1;
EnvVertex v;
v.node = node;
v.x = area.left + static_cast<int>(xi);
v.y = levelToY(area, level);
v.level = level;
return v;
}
std::vector<EnvVertex> gatePolyline(const AmpEnvelope& env, const Rect& area, double totalSeconds) {
// Non-negative segment durations (a stored negative would be an upstream bug; clamp defensively).
const double a = std::max(0.0, env.attackSeconds);
@@ -54,23 +79,27 @@ std::vector<EnvVertex> gatePolyline(const AmpEnvelope& env, const Rect& area, do
const double r = std::max(0.0, env.releaseSeconds);
const double sus = clamp01(env.sustainLevel);
// Cumulative wall-clock times of each breakpoint from t=0.
const double tAttack = a;
const double tHold = tAttack + h;
const double tDecay = tHold + d;
// The sustain plateau runs to the sample end; if the pre-sustain stages already overrun the
// sample, the plateau collapses to zero width (its end clamps up to tDecay).
const double tSustainEnd = std::max(tDecay, totalSeconds);
const double tRelease = tSustainEnd + r; // release trails PAST the sample end, by design
// BOUNDED SCHEMATIC (FA2): A/H/D and R map onto the TIMED region (canvas minus the reserved
// sustain-plateau width) at the sample's time scale; the sustain plateau is the fixed reserve
// between DecayEnd and ReleaseStart. Cumulative px, clamped in gateVtx, stay monotonic.
const int timedW = gateTimedWidth(area);
const int sustainPx = std::max(0, area.width()) - timedW;
const double pxPerSec = static_cast<double>(timedW) / totalSeconds;
const double pxAttack = a * pxPerSec;
const double pxHold = (a + h) * pxPerSec;
const double pxDecay = (a + h + d) * pxPerSec;
const double pxPlateau = pxDecay + static_cast<double>(sustainPx); // schematic note-off
const double pxRelease = pxPlateau + r * pxPerSec;
std::vector<EnvVertex> pts;
pts.reserve(6);
pts.push_back(vtx(EnvNode::Origin, area, totalSeconds, 0.0, 0.0));
pts.push_back(vtx(EnvNode::AttackEnd, area, totalSeconds, tAttack, 1.0));
pts.push_back(vtx(EnvNode::HoldEnd, area, totalSeconds, tHold, 1.0));
pts.push_back(vtx(EnvNode::DecayEnd, area, totalSeconds, tDecay, sus)); // sustain node
pts.push_back(vtx(EnvNode::ReleaseStart, area, totalSeconds, tSustainEnd, sus)); // plateau end
pts.push_back(vtx(EnvNode::ReleaseEnd, area, totalSeconds, tRelease, 0.0));
pts.push_back(gateVtx(EnvNode::Origin, area, 0.0, 0.0));
pts.push_back(gateVtx(EnvNode::AttackEnd, area, pxAttack, 1.0));
pts.push_back(gateVtx(EnvNode::HoldEnd, area, pxHold, 1.0));
pts.push_back(gateVtx(EnvNode::DecayEnd, area, pxDecay, sus)); // sustain node
pts.push_back(gateVtx(EnvNode::ReleaseStart, area, pxPlateau, sus)); // plateau end
pts.push_back(gateVtx(EnvNode::ReleaseEnd, area, pxRelease, 0.0));
return pts;
}