S-VIEW-3: add envelope_overlay + envelope_edit pure modules
AHDSR/Trigger params -> polyline forward map (overlay) and node hit-test + clamped/monotonic pixel->param inverse map (edit) for the draggable Sample-view envelope. Engine-free, unit-tested, wired into ctest. No shell changes.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
// envelope_overlay.cpp — see envelope_overlay.h. Pure geometry; no host types.
|
||||
|
||||
#include "envelope_overlay.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
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<long>(frac * static_cast<double>(w) + 0.5);
|
||||
return area.left + static_cast<int>(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<long>((1.0 - level) * static_cast<double>(span) + 0.5);
|
||||
return area.top + static_cast<int>(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<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);
|
||||
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<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));
|
||||
return pts;
|
||||
}
|
||||
|
||||
std::vector<EnvVertex> 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<EnvVertex> 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<EnvVertex> 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
|
||||
Reference in New Issue
Block a user