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