// envelope_overlay.h — staged-envelope -> polyline geometry for the Sample-view overlay. // Engine-free by design (no sample_map/sampler_core dependency); mirror of waveform_view / // param_slider. The shell packs whichever envelope is overlay-active into StageEnvelope and // draws the polyline plus a handle at each node (envelope_edit does the hit-test). #pragma once #include #include #include "core/instrument/ui/editor_geometry.h" // Rect — the shared geometry idiom #include "core/util/curve_law.h" // the ONE per-segment curve law namespace reasampler::instrument::ui { // Which LAYOUT POLICY an envelope takes — see core/instrument/CLAUDE.md's "envelope overlay" // section for why (decided by sustain-stage presence, not by which processor it modulates). // Ahdsr right-anchors its release; Ahd maps 1:1 onto the waveform's own time axis. enum class EnvKind { Ahdsr, Ahd }; // Gate nodes: Origin -> AttackEnd -> HoldEnd -> DecayEnd(sustain) -> ReleaseStart -> ReleaseEnd. // AHD nodes: Origin -> AttackEnd -> HoldEnd -> DecayEnd. // The three *Curve nodes are the round mid-segment knots whose vertical drag sets that // segment's curve exponent. Shared by envelope_overlay (forward/draw) and envelope_edit // (inverse/edit). enum class EnvNode { Origin, // t=0, level 0 — not draggable AttackEnd, // attack ramp top — sets attackSeconds HoldEnd, // hold plateau end — AHDSR: holdSeconds; AHD: holdFraction DecayEnd, // AHDSR: decay settles to sustain (X = decay, Y = sustain); AHD: decay end ReleaseStart, // AHDSR: sustain plateau end — sets releaseSeconds (drags on X, inverted) ReleaseEnd, // AHDSR: the envelope's end point — ANCHORED to the right edge, not draggable AttackCurve, // mid-attack knot — sets attackCurve DecayCurve, // mid-decay knot — sets decayCurve ReleaseCurve, // mid-release knot — sets releaseCurve (AHDSR only) }; // The envelope the overlay draws. One struct for both policies: `kind` selects which fields // are read, so a single pack/unpack pair serves the amp, pitch, and filter envelopes. struct StageEnvelope { EnvKind kind = EnvKind::Ahdsr; // AHDSR: seconds at the schematic param-domain scale, plus a dimensionless sustain level. double attackSeconds = 0.003; double holdSeconds = 0.0; double decaySeconds = 0.0; double sustainLevel = 1.0; double releaseSeconds = 0.060; // AHD: attack/decay seconds plus the Hold FRACTION of the span left after them, laid over // [originSeconds, originSeconds + spanSeconds) of the waveform's own time axis. double holdFraction = 1.0; double originSeconds = 0.0; double spanSeconds = 0.0; // Per-segment curve exponents (release is AHDSR-only). curve_law.h owns the domain. double attackCurve = util::kCurveNeutral; double decayCurve = util::kCurveNeutral; double releaseCurve = util::kCurveNeutral; }; // One polyline vertex: pixel point plus which node it is. `level` is redundant with y, carried // for inspection. `knot` marks the round mid-segment curve handles, which draw differently and // are not part of the traced line. struct EnvVertex { EnvNode node = EnvNode::Origin; int x = 0; int y = 0; double level = 0.0; bool knot = false; bool operator==(const EnvVertex& o) const { return node == o.node && x == o.x && y == o.y && level == o.level && knot == o.knot; } }; // Minimum pixel separation between consecutive AHDSR nodes, so zero-duration stages (tier-0 // defaults) still render as distinct, grabbable handles. Larger than envelope_edit's grab // radius (6) so a click can never tie between neighbours. inline constexpr int kGateNodeSepPx = 8; // The AHDSR schematic's per-stage time domain (seconds) — the four timed stages A/H/D/R each // span at most this. Must match the shell's stage-knob ceiling so a maxed knob lands exactly at // the canvas edge (at which point the sustain plateau has shrunk to nothing). inline constexpr double kGateStageMaxSeconds = 2.0; // Pixels per second of the AHDSR schematic, independent of the sample's actual duration. // Shared by buildEnvelopePolyline and envelope_edit's drag inverse so a dragged handle tracks // the cursor 1:1. double gatePxPerSecond(const Rect& area); // Maps a staged envelope to polyline vertices inside `area` over a sample of `totalSeconds` // duration. y maps level [0,1] across [area.bottom()-1, area.y] (level 1 at the top); the // traced vertices come first in draw order (Origin first), then the curve knots. // // The AHDSR x-axis is a bounded schematic independent of totalSeconds (it does NOT line up with // the waveform under it) with its ReleaseEnd anchored to the right edge; the AHD x-axis is // wall-clock, 1:1 with the waveform. Every vertex is clamped inside the canvas: x in // [area.x, area.right()-1], y in [area.y, area.bottom()-1]. A degenerate area or // totalSeconds <= 0 yields the flat two-point baseline [Origin, end at level 0]. Takes the // waveform overlay (not a lane) — see waveform_view.h's overlay contract. std::vector buildEnvelopePolyline(const StageEnvelope& env, const OverlayArea& area, double totalSeconds); // Maps a time (seconds) to a pixel x inside `area`, linear and clamped at both ends. Shared // with envelope_edit's node hit-test so the drawn handle and its grab region agree. int timeToX(const Rect& area, double totalSeconds, double t); // Maps a level [0,1] to a pixel y inside `area` (level 1 at the top, 0 at the bottom row), // clamped. Shared with envelope_edit's node hit-test. int levelToY(const Rect& area, double level); // The A/H/D split of an AHD's span, in seconds — the pure-UI mirror of the engine's fitAhd, so // the drawn stage boundaries land where the voice actually puts them. Attack takes at most the // span and Decay at most what Attack left, so Hold's fraction of the remainder can never push // the sum past the span; there is no clamp on the sum because none is possible. struct AhdSplit { double attack = 0.0; double hold = 0.0; double decay = 0.0; double total = 0.0; }; AhdSplit splitAhdSeconds(const StageEnvelope& env); } // namespace reasampler::instrument::ui