FA2: bounded-schematic Gate envelope (15% sustain reserve, in-bounds release) + zero-fade-out grabbable via nearest-node hit-test; all nodes clamp in-canvas
This commit is contained in:
+66
-33
@@ -4,16 +4,37 @@
|
||||
// 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):
|
||||
// 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.
|
||||
// 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 sample's time
|
||||
// scale — so A -> (H) -> D -> S-plateau -> R all render INSIDE the canvas and the
|
||||
// release is a visible, draggable segment (it no longer trails past area.right).
|
||||
// * 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.
|
||||
// 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.left, area.right-1], y in [area.top, 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 sustain reserve compresses the timed
|
||||
// region); Trigger's x-axis is still PCM-aligned.
|
||||
// * nodeAtPoint (envelope_edit) now resolves to the NEAREST node within the grab radius,
|
||||
// draw-order tie-break — coincident nodes (zero fade-out at the right edge, zero hold) are
|
||||
// deterministically grabbable.
|
||||
//
|
||||
// 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
|
||||
@@ -44,7 +65,8 @@ enum class EnvMode { Gate, Trigger };
|
||||
//
|
||||
// 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.
|
||||
// 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 {
|
||||
@@ -55,7 +77,8 @@ enum class EnvNode {
|
||||
// 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; 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) —
|
||||
@@ -121,36 +144,46 @@ struct EnvVertex {
|
||||
}
|
||||
};
|
||||
|
||||
// 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 sample's time scale. One constant shared
|
||||
// by the forward map (here) and the inverse map (envelope_edit) so a drag tracks the cursor.
|
||||
inline constexpr double kGateSustainDisplayFraction = 0.15;
|
||||
|
||||
// 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);
|
||||
|
||||
// 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.
|
||||
// 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).
|
||||
// TIME BASE (FA2).
|
||||
// * Gate: a bounded schematic. 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, all
|
||||
// at totalSeconds-over-timed-width scale — plus a FIXED sustain plateau of
|
||||
// (width - timedWidth) px between DecayEnd and ReleaseStart (the schematic note-off). Every
|
||||
// vertex x clamps to area.right-1, so when the stages overrun the visible span the trailing
|
||||
// nodes pile up (still monotonic, still in-bounds, still draggable back left).
|
||||
// * 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).
|
||||
//
|
||||
// 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.
|
||||
// BOUNDS: every vertex is inside the canvas — x in [area.left, area.right-1], y in
|
||||
// [area.top, 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.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.
|
||||
// Map a time (seconds) to a pixel x inside `area`: t=0 -> area.left, t=totalSeconds ->
|
||||
// area.right-1, linear, CLAMPED on both sides (t < 0 pins to area.left; t past totalSeconds pins
|
||||
// to area.right-1 — the in-bounds invariant, FA2). A zero-width area or totalSeconds <= 0 yields
|
||||
// area.left. 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.top, level 0 -> area.bottom-1
|
||||
|
||||
Reference in New Issue
Block a user