// envelope_overlay.cpp — see envelope_overlay.h. Pure geometry; no host types. #include "envelope_overlay.h" #include namespace reasampler::vst { 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. const double frac = t / totalSeconds; const long xi = static_cast(frac * static_cast(w) + 0.5); return area.left + static_cast(xi); } int levelToY(const Rect& area, double level) { const int h = std::max(0, area.height()); if (h <= 0) return area.top; if (level < 0.0) level = 0.0; if (level > 1.0) level = 1.0; // Level 1 -> top row, level 0 -> bottom row (bottom-1 under the half-open convention). The // range spans (h-1) pixels so both endpoints land ON a drawable row. const int span = h - 1; const long dy = static_cast((1.0 - level) * static_cast(span) + 0.5); return area.top + static_cast(dy); } namespace { double clamp01(double v) { if (v < 0.0) return 0.0; if (v > 1.0) return 1.0; return v; } EnvVertex vtx(EnvNode node, const Rect& area, double totalSeconds, double t, double level) { EnvVertex v; v.node = node; v.x = timeToX(area, totalSeconds, t); v.y = levelToY(area, level); v.level = level; return v; } std::vector 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); const double h = std::max(0.0, env.holdSeconds); const double d = std::max(0.0, env.decaySeconds); 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 std::vector 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)); return pts; } std::vector triggerPolyline(const AmpEnvelope& env, const Rect& area, double totalSeconds) { // The played span is lengthFraction of the whole sample; fades are fractions OF that span. const double len = clamp01(env.lengthFraction); double fadeIn = clamp01(env.fadeInFraction); double fadeOut = clamp01(env.fadeOutFraction); // Fades cannot overlap: clamp so fadeIn + fadeOut <= 1 (of the played span), mirroring the // engine's TriggerParams clamp. Trim the LATER fade (fade-out) first, matching the engine. if (fadeIn + fadeOut > 1.0) fadeOut = std::max(0.0, 1.0 - fadeIn); const double playSeconds = len * totalSeconds; const double tFadeInEnd = fadeIn * playSeconds; const double tFadeOutStart = playSeconds - fadeOut * playSeconds; // where fade-out begins std::vector pts; pts.reserve(4); pts.push_back(vtx(EnvNode::Origin, area, totalSeconds, 0.0, 0.0)); pts.push_back(vtx(EnvNode::FadeInEnd, area, totalSeconds, tFadeInEnd, 1.0)); pts.push_back(vtx(EnvNode::FadeOutStart, area, totalSeconds, tFadeOutStart, 1.0)); // unity end pts.push_back(vtx(EnvNode::LengthEnd, area, totalSeconds, playSeconds, 0.0)); // playEnd return pts; } } // namespace std::vector buildEnvelopePolyline(const AmpEnvelope& env, const Rect& area, double totalSeconds) { if (area.width() <= 0 || area.height() <= 0 || totalSeconds <= 0.0) { // Degenerate surface: a two-point flat baseline at level 0 so the shell always has a line. return {vtx(EnvNode::Origin, area, 1.0, 0.0, 0.0), vtx(EnvNode::ReleaseEnd, area, 1.0, 1.0, 0.0)}; } return env.mode == EnvMode::Gate ? gatePolyline(env, area, totalSeconds) : triggerPolyline(env, area, totalSeconds); } } // namespace reasampler::vst