Files
reasampler/src/core/instrument/ui/envelope_overlay.cpp
T

214 lines
9.0 KiB
C++

// envelope_overlay.cpp — see envelope_overlay.h. Pure geometry; no host types.
#include "core/instrument/ui/envelope_overlay.h"
#include "core/util/clamp01.h"
#include <algorithm>
namespace reasampler::instrument::ui {
using util::clamp01;
using util::curveMap;
using util::curveMidLevel;
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.x;
if (t < 0.0) t = 0.0;
// Clamp in double space before the int cast — a huge t would overflow a 32-bit long
// (Windows) and wrap to the wrong edge.
double px = (t / totalSeconds) * static_cast<double>(w);
if (px > static_cast<double>(w - 1)) px = static_cast<double>(w - 1);
return area.x + static_cast<int>(px + 0.5);
}
double gatePxPerSecond(const Rect& area) {
const int w = std::max(0, area.width);
if (w <= 0) return 0.0;
// The four timed stages share the canvas minus their four separation bases and the last
// in-bounds column; whatever they leave IS the sustain plateau, which is why a zero release
// puts the plateau's end one separation short of the right edge rather than a fixed
// fraction of the way across.
const double usable = std::max(1.0, static_cast<double>(w - 1 - 4 * kGateNodeSepPx));
return usable / (4.0 * kGateStageMaxSeconds);
}
int levelToY(const Rect& area, double level) {
const int h = std::max(0, area.height);
if (h <= 0) return area.y;
if (level < 0.0) level = 0.0;
if (level > 1.0) level = 1.0;
// Level 1 -> top row, level 0 -> bottom row; spans (h-1) px 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.y + static_cast<int>(dy);
}
AhdSplit splitAhdSeconds(const StageEnvelope& env) {
AhdSplit out;
const double span = std::max(0.0, env.spanSeconds);
double a = std::max(0.0, env.attackSeconds);
if (a > span) a = span;
double d = std::max(0.0, env.decaySeconds);
if (d > span - a) d = span - a;
const double remaining = span - a - d;
out.attack = a;
out.decay = d;
out.hold = remaining * clamp01(env.holdFraction);
out.total = out.attack + out.hold + out.decay;
return out;
}
namespace {
EnvVertex vtx(EnvNode node, const Rect& area, double totalSeconds, double t, double level,
bool knot = false) {
EnvVertex v;
v.node = node;
v.x = timeToX(area, totalSeconds, t);
v.y = levelToY(area, level);
v.level = level;
v.knot = knot;
return v;
}
// The AHDSR schematic works in px space rather than the plain timeToX map; clamps in double
// space before the int cast for the same overflow reason as timeToX.
EnvVertex gateVtx(EnvNode node, const Rect& area, double px, double level, bool knot = false) {
const int w = std::max(1, area.width);
if (px < 0.0) px = 0.0;
if (px > static_cast<double>(w - 1)) px = static_cast<double>(w - 1);
EnvVertex v;
v.node = node;
v.x = area.x + static_cast<int>(px + 0.5);
v.y = levelToY(area, level);
v.level = level;
v.knot = knot;
return v;
}
// The knot for a segment running from `startLevel` to `endLevel`, placed at the segment's
// pixel midpoint, its level read through curve_law.h's own law (the knot/dial pairing's home).
EnvVertex knotVtx(EnvNode node, const Rect& area, int x0, int x1, double startLevel,
double endLevel, double exponent) {
const double u = curveMidLevel(exponent);
const double level = startLevel + (endLevel - startLevel) * u;
EnvVertex v;
v.node = node;
v.x = (x0 + x1) / 2;
v.y = levelToY(area, level);
v.level = level;
v.knot = true;
return v;
}
std::vector<EnvVertex> gatePolyline(const StageEnvelope& env, const Rect& area) {
// Clamp defensively — a stored negative duration would be an upstream bug.
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);
const int W = std::max(1, area.width);
const double sep = static_cast<double>(kGateNodeSepPx);
const double pps = gatePxPerSecond(area);
const double xMax = static_cast<double>(W - 1);
// The release ANCHORS to the right edge: ReleaseEnd is the canvas edge and ReleaseStart —
// the sustain->release join, and the node the user drags — sits a release-length to its
// left. Everything the release does not take is the sustain plateau, so a zero release
// leaves the plateau running to within one separation of the edge.
double xAttack = sep + a * pps;
double xHold = xAttack + sep + h * pps;
double xDecay = xHold + sep + d * pps;
double xPlateau = xMax - sep - r * pps;
const double xRelease = xMax;
// Keep every node separated when the four stages together would overrun the canvas: the
// plateau holds its minimum gap from the edge, then the A/H/D chain compresses from the
// right and re-floors from the left. This only bites at the domain's extremes; gateVtx's
// own clamp wins on a canvas too narrow to hold the gaps at all.
if (xPlateau < xDecay + sep) {
if (xPlateau < 4.0 * sep) xPlateau = 4.0 * sep;
xDecay = std::min(xDecay, xPlateau - sep);
xHold = std::min(xHold, xDecay - sep);
xAttack = std::min(xAttack, xHold - sep);
xAttack = std::max(xAttack, sep);
xHold = std::max(xHold, xAttack + sep);
xDecay = std::max(xDecay, xHold + sep);
xPlateau = std::max(xPlateau, xDecay + sep);
}
std::vector<EnvVertex> pts;
pts.reserve(9);
pts.push_back(gateVtx(EnvNode::Origin, area, 0.0, 0.0));
pts.push_back(gateVtx(EnvNode::AttackEnd, area, xAttack, 1.0));
pts.push_back(gateVtx(EnvNode::HoldEnd, area, xHold, 1.0));
pts.push_back(gateVtx(EnvNode::DecayEnd, area, xDecay, sus)); // sustain node
pts.push_back(gateVtx(EnvNode::ReleaseStart, area, xPlateau, sus)); // plateau end
pts.push_back(gateVtx(EnvNode::ReleaseEnd, area, xRelease, 0.0)); // anchored
// Knots ride only SLOPED stages that actually have a duration — a zero-length stage has no
// interior to place a handle in, and one there would collide with its own endpoints.
if (a > 0.0) {
pts.push_back(knotVtx(EnvNode::AttackCurve, area, pts[0].x, pts[1].x, 0.0, 1.0,
env.attackCurve));
}
if (d > 0.0) {
pts.push_back(knotVtx(EnvNode::DecayCurve, area, pts[2].x, pts[3].x, 1.0, sus,
env.decayCurve));
}
if (r > 0.0) {
pts.push_back(knotVtx(EnvNode::ReleaseCurve, area, pts[4].x, pts[5].x, sus, 0.0,
env.releaseCurve));
}
return pts;
}
std::vector<EnvVertex> ahdPolyline(const StageEnvelope& env, const Rect& area,
double totalSeconds) {
const AhdSplit s = splitAhdSeconds(env);
const double t0 = std::max(0.0, env.originSeconds);
std::vector<EnvVertex> pts;
pts.reserve(6);
pts.push_back(vtx(EnvNode::Origin, area, totalSeconds, t0, 0.0));
pts.push_back(vtx(EnvNode::AttackEnd, area, totalSeconds, t0 + s.attack, 1.0));
pts.push_back(vtx(EnvNode::HoldEnd, area, totalSeconds, t0 + s.attack + s.hold, 1.0));
// DecayEnd is drawn at t0 + total, which coincides with HoldEnd exactly when decay ~ 0 —
// independent of holdFraction (total = attack + hold + decay always). Left at its true
// instant rather than nudged: the 1:1 axis this policy exists to keep honest must hold even
// at a shared instant, including the Trigger default's abrupt (zero-decay) cutoff.
// envelope_edit's nodeAtPoint handles the coincidence instead, by dropping DecayEnd from the
// grabbable set when it also cannot move (holdFraction == 1.0).
pts.push_back(vtx(EnvNode::DecayEnd, area, totalSeconds, t0 + s.total, 0.0));
if (s.attack > 0.0) {
pts.push_back(knotVtx(EnvNode::AttackCurve, area, pts[0].x, pts[1].x, 0.0, 1.0,
env.attackCurve));
}
if (s.decay > 0.0) {
pts.push_back(knotVtx(EnvNode::DecayCurve, area, pts[2].x, pts[3].x, 1.0, 0.0,
env.decayCurve));
}
return pts;
}
} // namespace
std::vector<EnvVertex> buildEnvelopePolyline(const StageEnvelope& env, const OverlayArea& area,
double totalSeconds) {
const Rect& rect = area.rect;
if (rect.width <= 0 || rect.height <= 0 || totalSeconds <= 0.0) {
// Degenerate surface: flat two-point baseline so the shell always has a line.
return {vtx(EnvNode::Origin, rect, 1.0, 0.0, 0.0),
vtx(EnvNode::ReleaseEnd, rect, 1.0, 1.0, 0.0)};
}
return env.kind == EnvKind::Ahdsr ? gatePolyline(env, rect)
: ahdPolyline(env, rect, totalSeconds);
}
} // namespace reasampler::instrument::ui