// envelope_overlay.h — amp-envelope -> polyline geometry for the Sample-view envelope overlay. // Engine-free by design (no sample_map/sampler_core dependency); mirror of waveform_view / // param_slider. The shell packs the one parameter set's AdsrSeconds/TriggerParams into // AmpEnvelope 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 namespace reasampler::instrument::ui { // Local mirror of sampler_core's PlayMode, kept here so this module stays engine-free. enum class EnvMode { Gate, Trigger }; // Gate nodes: Origin -> AttackEnd -> HoldEnd -> DecayEnd(sustain) -> ReleaseStart -> ReleaseEnd. // Trigger nodes: Origin -> FadeInEnd -> FadeOutStart -> LengthEnd(playEnd). // Shared by envelope_overlay (forward/draw map) and envelope_edit (inverse/edit map). enum class EnvNode { Origin, // t=0, level 0 — not draggable AttackEnd, // Gate: attack ramp top — sets attackSeconds HoldEnd, // Gate: hold plateau end — sets holdSeconds DecayEnd, // Gate: decay settles to sustain — sets decaySeconds (X) and sustainLevel (Y) ReleaseStart, // Gate: sustain plateau end — drawing-only, not draggable ReleaseEnd, // Gate: release tail end — sets releaseSeconds FadeInEnd, // Trigger: fade-in top — sets fadeInFraction FadeOutStart, // Trigger: fade-out start — sets fadeOutFraction LengthEnd, // Trigger: playEnd terminal — sets lengthFraction }; // Amp-envelope params the overlay draws. Trigger's fadeIn/fadeOutFraction are derived from // TriggerParams' frame counts, not a direct field copy — see the trigger_seam gotcha in // core/instrument/CLAUDE.md. struct AmpEnvelope { EnvMode mode = EnvMode::Gate; // Gate (AHDSR): seconds, 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; // Trigger: fractions of the played span. double lengthFraction = 1.0; double fadeInFraction = 0.0; double fadeOutFraction = 0.0; }; // One polyline vertex: pixel point plus which node it is. level is redundant with y, carried for // inspection. struct EnvVertex { EnvNode node = EnvNode::Origin; int x = 0; int y = 0; double level = 0.0; bool operator==(const EnvVertex& o) const { return node == o.node && x == o.x && y == o.y && level == o.level; } }; // Fraction of canvas width reserved for the Gate sustain-plateau display; the remaining width // carries A/H/D/R at the param-domain scale. Shared with envelope_edit. inline constexpr double kGateSustainDisplayFraction = 0.15; // Minimum pixel separation between consecutive Gate 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; // Gate schematic's per-stage time domain (seconds) — the timed region represents four stages // end-to-end at this max each. Must match the shell's stage-slider ceiling so a maxed slider // lands exactly at the canvas edge. inline constexpr double kGateStageMaxSeconds = 2.0; // Pixel width of the Gate timed region (area width minus the sustain reserve), floored at 1 for // a non-empty area; 0 for a zero/negative-width area. int gateTimedWidth(const Rect& area); // Pixels per second of the Gate timed region, 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 an amp 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); vertices // are in draw order, Origin first. // // Gate's x-axis is a bounded schematic independent of totalSeconds (does NOT line up with the // waveform under it); Trigger's x-axis is PCM-aligned wall-clock. 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]. std::vector buildEnvelopePolyline(const AmpEnvelope& env, const Rect& 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); } // namespace reasampler::instrument::ui