f3be4d8cce
capture.h realtime seam split to capture_realtime_shell.h; GetProjExtState grow-loop rehomed to core/wire/ext_state_read; stale persist.cpp/bank_panel.cpp comment refs fixed; CLAUDE.md persist/bank_book/actions bullets updated. Command-id suffixes, display phrases, and undo labels byte-identical.
230 lines
15 KiB
C++
230 lines
15 KiB
C++
// 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 (Simpler / Phase-Plant
|
|
// grammar):
|
|
// * Gate -> the AHDSR shape: attack ramp 0->1, hold plateau at 1, decay 1->sustain,
|
|
// sustain plateau, release sustain->0. Since there is no held note-off to draw
|
|
// against, Gate is a BOUNDED SCHEMATIC (FA2): a fixed fraction of the canvas
|
|
// width (kGateSustainDisplayFraction) is RESERVED for the sustain plateau, and
|
|
// the remaining "timed" width carries A/H/D AND the release at the PARAM-DOMAIN
|
|
// scale — the timed width represents 4 x kGateStageMaxSeconds (the four stage
|
|
// sliders end-to-end at their maxima), NOT the sample's duration, so the layout
|
|
// is identical for a 0.3s and a 10s capture. Each segment additionally gets a
|
|
// kGateNodeSepPx pixel base, so consecutive nodes NEVER coincide: every Gate
|
|
// node is individually grabbable at ANY param values, including the tier-0
|
|
// defaults (hold 0 / decay 0). A -> (H) -> D -> S-plateau -> R all render INSIDE
|
|
// the canvas and the release is a visible, draggable segment.
|
|
// * Trigger -> the fade/%-length shape: fade-in 0->1, unity plateau, fade-out 1->0 anchored
|
|
// to playEnd (= lengthFraction of the post-start span). Trigger keeps the
|
|
// waveform's exact time base so the shape lines up with the PCM under it.
|
|
// The horizontal axis is TIME (Gate: schematic, see above; Trigger: wall-clock across the rect);
|
|
// the vertical axis is LEVEL (0 at rect bottom, 1 at rect top).
|
|
//
|
|
// BOUNDS INVARIANT (FA2). EVERY vertex of EVERY polyline is clamped inside the canvas:
|
|
// x in [area.x, area.right()-1], y in [area.y, area.bottom()-1] (half-open rect convention).
|
|
// No node and no drawn segment ever exceeds the canvas — paint-time clipping of handles is no
|
|
// longer needed (and never fires) in the shell.
|
|
//
|
|
// FA2 CONTRACT CHANGE — WAVE B SHELL AUTHOR, READ THIS:
|
|
// * The EnvNode enum is UNCHANGED (same node set, same draggable set — Origin + ReleaseStart
|
|
// remain the only non-draggable anchors).
|
|
// * ALL vertices are now in-bounds (see above). The shell's previous "skip handle when
|
|
// v.x >= waveArea.right()" clip is dead code: ReleaseEnd (Gate) and FadeOutStart/LengthEnd
|
|
// (Trigger, at full length / zero fade-out) now land at area.right()-1 and MUST get handles.
|
|
// * Gate's x-axis is SCHEMATIC, not PCM-aligned: the timed region is scaled to the param
|
|
// domain (4 x kGateStageMaxSeconds), the sustain reserve is a fixed width, and every
|
|
// segment carries a kGateNodeSepPx pixel base. The Gate curve does NOT line up with the
|
|
// waveform under it — do not label it as if it did. Trigger's x-axis IS still PCM-aligned.
|
|
// * Gate nodes never coincide (min-separation, above), so every Gate handle is individually
|
|
// grabbable in every state. nodeAtPoint (envelope_edit) resolves to the NEAREST node within
|
|
// the grab radius with a draw-order tie-break; the tie-break only matters for the one
|
|
// remaining coincidence, Trigger's zero-fade-out (FadeOutStart overlays LengthEnd at the
|
|
// right edge and wins the tie, so the fade can be dragged open from zero).
|
|
//
|
|
// 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_codec 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 "core/instrument/ui/editor_geometry.h" // Rect — the shared geometry idiom
|
|
|
|
namespace reasampler::instrument::ui {
|
|
|
|
// 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 (the schematic note-off);
|
|
// release is edited by dragging ReleaseEnd.
|
|
// 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 sits a fixed sustain-plateau width right of
|
|
// DecayEnd — the schematic note-off — 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.
|
|
// These map 1-to-1 with the stored AdsrSeconds fields — no conversion required.
|
|
//
|
|
// 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).
|
|
//
|
|
// TRIGGER SEAM — CONVERSION REQUIRED ON BOTH PATHS (Wave 2 shell author, read this):
|
|
// TriggerParams (sampler_core.h) stores Trigger fades as SOURCE FRAMES:
|
|
// fadeInFrames (int64_t) — 0->1 ramp length in source frames
|
|
// fadeOutFrames (int64_t) — 1->0 ramp length in source frames
|
|
// AmpEnvelope stores them as FRACTIONS of the played span:
|
|
// fadeInFraction = fadeInFrames / playLengthFrames
|
|
// fadeOutFraction = fadeOutFrames / playLengthFrames
|
|
// where playLengthFrames = round(lengthFraction * (frameCount - startFrame)).
|
|
// This is a NON-TRIVIAL derived view — NOT a direct field copy. The shell owes a
|
|
// converter on BOTH directions:
|
|
// PACK (draw): frames -> fraction (TriggerParams -> AmpEnvelope, needs frameCount + rate)
|
|
// UNPACK (commit): fraction -> frames (AmpEnvelope -> TriggerParams, same inputs)
|
|
// lengthFraction maps 1-to-1 with TriggerParams::lengthFraction and needs no conversion.
|
|
//
|
|
// 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.
|
|
// NOTE: fadeInFraction/fadeOutFraction are DERIVED from TriggerParams::fadeInFrames/
|
|
// fadeOutFrames — see the TRIGGER SEAM note above. A converter is owed on both the
|
|
// pack (draw) and unpack (commit) paths; these fields are NOT a direct TriggerParams copy.
|
|
double lengthFraction = 1.0; // (0,1] of the post-start span that plays (1-to-1 with TriggerParams)
|
|
double fadeInFraction = 0.0; // 0->1 ramp as a fraction of the played span (DERIVED — see above)
|
|
double fadeOutFraction = 0.0; // 1->0 ramp as a fraction of the played span (DERIVED — see above)
|
|
};
|
|
|
|
// 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;
|
|
}
|
|
};
|
|
|
|
// The fraction of the canvas width RESERVED for the Gate sustain-plateau display (FA2). The
|
|
// plateau is a fixed-width schematic region between DecayEnd and ReleaseStart; the remaining
|
|
// width is the "timed" region A/H/D/R map onto at the schematic param-domain scale. One
|
|
// constant shared by the forward map (here) and the inverse map (envelope_edit).
|
|
inline constexpr double kGateSustainDisplayFraction = 0.15;
|
|
|
|
// The minimum pixel separation between consecutive Gate polyline nodes: every Gate segment gets
|
|
// this many px as a base, PLUS its time-proportional extent, so zero-duration stages (tier-0
|
|
// defaults: hold 0, decay 0) still render as distinct, individually grabbable handles. Chosen
|
|
// larger than envelope_edit's kNodeGrabRadius (6) so a click dead-on a node can never tie with
|
|
// its neighbour. Shared by the forward map and the drag inverse.
|
|
inline constexpr int kGateNodeSepPx = 8;
|
|
|
|
// The Gate schematic's per-stage time domain (seconds): the timed region represents the four
|
|
// stages end-to-end at this maximum each (4 x this total). MIRRORS the shell's stage-slider
|
|
// ceiling (kEnvTimeMaxSeconds in reasampler_editor.cpp) — keep the two equal so a stage at its
|
|
// slider max lands exactly at the canvas edge. Drag safety does NOT depend on this constant
|
|
// (param clamps are caller-supplied in envelope_edit); only layout does.
|
|
inline constexpr double kGateStageMaxSeconds = 2.0;
|
|
|
|
// The pixel width of the Gate timed region: area.width minus the sustain-plateau reserve,
|
|
// floored at 1 px so the px<->seconds scale never degenerates for a non-empty area. Returns 0
|
|
// for a zero/negative-width area. Shared by gatePolyline and envelope_edit's gate drag scale.
|
|
int gateTimedWidth(const Rect& area);
|
|
|
|
// Pixels per second of the Gate timed region under the PARAM-DOMAIN scale: the timed width,
|
|
// minus the four per-segment kGateNodeSepPx bases and the last in-bounds column, spread over
|
|
// 4 x kGateStageMaxSeconds. Independent of the sample's duration. Returns 0 for a
|
|
// zero/negative-width area; otherwise > 0 (the usable width floors at 1 px). The ONE px<->sec
|
|
// scale shared by the forward map (gatePolyline) and the drag inverse (envelope_edit), so a
|
|
// dragged handle tracks the cursor 1:1.
|
|
double gatePxPerSecond(const Rect& area);
|
|
|
|
// 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);
|
|
// y maps level 0..1 across [area.bottom()-1 .. area.y] (level 1 at the TOP). The polyline reads
|
|
// left-to-right in draw order, Origin first.
|
|
//
|
|
// TIME BASE (FA2).
|
|
// * Gate: a bounded schematic, INDEPENDENT of totalSeconds. The canvas splits into a TIMED
|
|
// region of gateTimedWidth(area) px — where attack/hold/decay run from t=0 and the release
|
|
// ramp runs after the plateau, at the gatePxPerSecond(area) PARAM-DOMAIN scale, each segment
|
|
// carrying a kGateNodeSepPx base so consecutive nodes never coincide — plus a FIXED sustain
|
|
// plateau of (width - timedWidth) px between DecayEnd and ReleaseStart (the schematic
|
|
// note-off). Stages beyond the schematic domain (a stored stage > kGateStageMaxSeconds)
|
|
// compress from the RIGHT preserving the minimum gaps, so trailing nodes stay individually
|
|
// separated instead of piling on the last column; only a canvas too narrow to hold the
|
|
// minimum gaps at all sacrifices separation (in-bounds wins).
|
|
// * Trigger: the waveform's exact time base (PCM-aligned). The played span is
|
|
// lengthFraction * totalSeconds; fade-in/out are fractions OF that played span. Nodes past
|
|
// the played span never appear (FadeOutStart/LengthEnd sit at the played span's right edge).
|
|
//
|
|
// BOUNDS: every vertex is inside the canvas — x in [area.x, area.right()-1], y in
|
|
// [area.y, area.bottom()-1]. Nothing maps past area.right() (the pre-FA2 release tail is gone). 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.x, t=totalSeconds ->
|
|
// area.right()-1, linear, CLAMPED on both sides (t < 0 pins to area.x; t past totalSeconds pins
|
|
// to area.right()-1 — the in-bounds invariant, FA2). A zero-width area or totalSeconds <= 0 yields
|
|
// area.x. Pure — the shared time->x map the Trigger 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.y, 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.y. Pure — the shared level->y map the polyline
|
|
// and the node hit-test share.
|
|
int levelToY(const Rect& area, double level);
|
|
|
|
} // namespace reasampler::instrument::ui
|