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,142 @@
|
||||
// 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 <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#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<EnvVertex> 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
|
||||
Reference in New Issue
Block a user