// envelope_overlay.h — PURE amp-envelope → polyline geometry for the S-VIEW-3 Sample-view // envelope overlay. NO VST3, NO REAPER, NO SWELL/LICE types at the boundary. The mirror of // waveform_view / param_slider: the params→pixel polyline math lives here, unit-tested outside // the DAW, while the editor shell (reasampler_editor.cpp) traces the polyline in an accent hue // and draws the node handles (via envelope_edit's hit-test). // // WHAT IT DRAWS. The amp envelope over the Sample view's hero waveform at accurate wall-clock // time (Simpler / Phase-Plant grammar): // * Gate -> the AHDSR shape: attack ramp 0->1, hold plateau at 1, decay 1->sustain, // sustain plateau, release sustain->0. // * Trigger -> the fade/%-length shape: fade-in 0->1, unity plateau, fade-out 1->0 anchored // to playEnd (= lengthFraction of the post-start span). // The horizontal axis is wall-clock TIME across the waveform rect; the vertical axis is LEVEL // (0 at rect bottom, 1 at rect top). The overlay shares the waveform's time base so the drawn // shape lines up with the PCM under it: the same [0, frameCount] span waveform_view maps, so the // envelope's own duration is placed at the SAME frames the voice plays it over. // // DELIBERATELY ENGINE-FREE (house pattern — param_slider does the same). It does NOT depend on // sample_map / sampler_core (which would drag bank_book / wav_trim in). The shell reads the // zone's AdsrSeconds / TriggerParams and packs them into the small AmpEnvelope view struct here. // AHDSR times are wall-clock SECONDS (rate-free, matching the stored domain — Daniel's no- // hardcoded-rate ruling); Trigger fades are FRACTIONS of the play span. The one rate-bound input // is the total sample duration in seconds, which the shell resolves once from the live rate and // the frame count and passes in — this module never sees a sample rate. // // It reuses editor_geometry's Rect + contains(), the one shared geometry idiom. #pragma once #include #include #include "editor_geometry.h" // Rect — the shared geometry idiom namespace reasampler::vst { // The play mode the overlay draws — a LOCAL mirror of sampler_core's PlayMode kept here so the // geometry module stays engine-free (the shell maps the zone's PlayMode to this). Same two cases. enum class EnvMode { Gate, Trigger }; // Which breakpoint a polyline vertex / node is. The shell draws a draggable handle at each of // these; envelope_edit hit-tests against them. Kept in one enum shared by overlay + edit so the // forward map (draw) and inverse map (edit) name the same nodes. // // Gate nodes: Origin -> AttackEnd -> HoldEnd -> DecayEnd(=sustain corner) -> ReleaseStart // -> ReleaseEnd. The sustain node is DecayEnd (its Y is the sustain level); // ReleaseStart is a drawing-only plateau-end vertex. // Trigger nodes: Origin -> FadeInEnd -> FadeOutStart -> LengthEnd(playEnd, level 0). The fade-out // ramp is the FadeOutStart->LengthEnd segment; LengthEnd is the playEnd terminal. enum class EnvNode { Origin, // t=0, level 0 (both modes) — not draggable (fixed anchor) AttackEnd, // Gate: top of the attack ramp (level 1) — X sets attackSeconds HoldEnd, // Gate: end of the hold plateau (level 1) — X sets holdSeconds DecayEnd, // Gate: decay settles to sustain — the SUSTAIN node (X sets decaySeconds, // Y sets sustainLevel) ReleaseStart, // Gate: end of the sustain plateau / start of the release (sustain level) — // a DRAWING vertex only, not a draggable handle (release is edited at // ReleaseEnd; this vertex tracks its X = sample end, Y = sustain level) ReleaseEnd, // Gate: end of the release tail (level 0) — X sets releaseSeconds FadeInEnd, // Trigger: top of the fade-in (level 1) — X sets fadeInFraction FadeOutStart, // Trigger: end of the unity plateau / start of the fade-out (level 1) — // X sets fadeOutFraction LengthEnd, // Trigger: the playEnd terminal / %-length (level 0) — X sets lengthFraction }; // The amp-envelope params the overlay draws — the small view struct the shell packs from the // zone's stored AdsrSeconds / TriggerParams. Engine-free by design (no sampler_core include). // // Gate fields (SECONDS, wall-clock): attack / hold / decay / release; sustain is a LEVEL 0..1. // Trigger fields (FRACTIONS of play): fadeIn / fadeOut as a fraction of the played span; // lengthFraction is the played span as a fraction of the // post-start sample length (matching TriggerParams). // Unused fields for the active mode are ignored. struct AmpEnvelope { EnvMode mode = EnvMode::Gate; // Gate (AHDSR), seconds + 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 play span (fadeIn/fadeOut) and of the post-start length. double lengthFraction = 1.0; // (0,1] of the post-start span that plays double fadeInFraction = 0.0; // 0->1 ramp as a fraction of the played span double fadeOutFraction = 0.0; // 1->0 ramp as a fraction of the played span }; // One polyline vertex: a pixel point plus which node it is. The shell draws a line through the // points in order (the amp curve) and a draggable handle at each vertex whose node is not Origin. // Level is carried alongside (0..1) for callers that want to label/inspect; it is redundant with y. struct EnvVertex { EnvNode node = EnvNode::Origin; int x = 0; // pixel x inside the overlay rect int y = 0; // pixel y inside the overlay rect (top = level 1, bottom = level 0) double level = 0.0; // 0..1, the vertex's amplitude (redundant with y; for inspection) bool operator==(const EnvVertex& o) const { return node == o.node && x == o.x && y == o.y && level == o.level; } }; // Map an amp envelope to its polyline vertices inside `area`, over a sample of `totalSeconds` // wall-clock duration. `area` is the waveform rect (left/top inclusive, right/bottom exclusive); // x maps time 0..totalSeconds across [area.left, area.right], y maps level 0..1 across // [area.bottom-1 .. area.top] (level 1 at the TOP). The polyline reads left-to-right in draw // order, Origin first. // // TIME BASE. The envelope's own segment durations are placed on the SAME time axis the waveform // occupies, so the curve lines up with the PCM: // * Gate: attack/hold/decay run from t=0; the sustain plateau runs to the note-off. Since the // overlay has no held note-off to draw against, the sustain plateau is drawn to the END of // the sample (totalSeconds) and the release tail is drawn AFTER that boundary — i.e. the // release is appended past the sample end (the standard "release after key-up at end of // view" convention). When attack+hold+decay already exceed totalSeconds the plateau collapses // to zero width (nodes clamp to the sample end) and release still trails past it. // * Trigger: the played span is lengthFraction * totalSeconds; fade-in/out are fractions OF // that played span. Nodes past the played span never appear (LengthEnd/FadeOutEnd sit at the // played span's right edge). // // A time beyond totalSeconds (the Gate release tail) maps past area.right — the shell clips at // paint time (the same way waveform_view lets a frame past the count pin the marker). A degenerate // area (zero width/height) or totalSeconds <= 0 yields the two-point flat baseline [Origin, end at // level 0] so the shell always has a drawable line. Pure — same inputs, same polyline. std::vector buildEnvelopePolyline(const AmpEnvelope& env, const Rect& area, double totalSeconds); // Map a time (seconds) to a pixel x inside `area`: t=0 -> area.left, t=totalSeconds -> area.right, // linear. t is NOT clamped on the high side (a Gate release past the sample end maps past // area.right, by design — see buildEnvelopePolyline); t < 0 pins to area.left. A zero-width area // or totalSeconds <= 0 yields area.left. Pure — the shared time->x map both the polyline and the // node hit-test (envelope_edit) use, so the drawn handle and its grab region agree. int timeToX(const Rect& area, double totalSeconds, double t); // Map a level (0..1) to a pixel y inside `area`: level 1 -> area.top, level 0 -> area.bottom-1 // (so the full-amplitude line sits at the top edge and silence at the bottom pixel row). level is // clamped to [0,1]. A zero-height area yields area.top. Pure — the shared level->y map the polyline // and the node hit-test share. int levelToY(const Rect& area, double level); } // namespace reasampler::vst