docs(envelope): add Trigger frames<->fraction seam note + zero-delta guard on fade nodes

This commit is contained in:
2026-07-27 13:34:43 -04:00
parent 12a7d072ac
commit 9a2b752fe9
2 changed files with 33 additions and 3 deletions
+10
View File
@@ -93,7 +93,16 @@ AmpEnvelope resolveNodeDrag(const AmpEnvelope& grabEnv, EnvNode node, const Rect
// --- Trigger: fades + length are FRACTIONS. X pixels convert to a fraction of the PLAYED
// span (fades) or the whole sample (length). Monotonic: fadeIn + fadeOut <= 1 so the
// two fade nodes never cross (each clamps against the other), and length in [0, max].
//
// TRIGGER SEAM — CONVERSION REQUIRED ON BOTH PATHS (Wave 2 shell author, read this):
// fadeInFraction/fadeOutFraction in AmpEnvelope are fractions of the played span.
// TriggerParams (sampler_core.h) stores the corresponding values as SOURCE FRAMES
// (fadeInFrames/fadeOutFrames, int64_t). The shell owes a converter on BOTH directions:
// pack (draw): fadeInFrames/fadeOutFrames -> fraction (needs frameCount + rate)
// unpack (commit): fraction -> fadeInFrames/fadeOutFrames (same inputs)
// See the TRIGGER SEAM note on AmpEnvelope in envelope_overlay.h for the formula.
case EnvNode::FadeInEnd: {
if (dxPixels == 0) break; // zero-motion grab: no param change, no division
const double playSeconds = std::max(0.0, grabEnv.lengthFraction) * totalSeconds;
const double dFrac = playSeconds > 0.0 ? dSec / playSeconds : 0.0;
const double hi = std::min(bounds.maxFadeInFraction,
@@ -102,6 +111,7 @@ AmpEnvelope resolveNodeDrag(const AmpEnvelope& grabEnv, EnvNode node, const Rect
break;
}
case EnvNode::FadeOutStart: {
if (dxPixels == 0) break; // zero-motion grab: no param change, no division
// FadeOutStart sits at (1 - fadeOut) of the played span; dragging it LEFT (negative dx)
// lengthens the fade-out. So the fade-out fraction moves OPPOSITE the pixel delta.
const double playSeconds = std::max(0.0, grabEnv.lengthFraction) * totalSeconds;
+23 -3
View File
@@ -67,9 +67,26 @@ enum class EnvNode {
// 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;
@@ -82,9 +99,12 @@ struct AmpEnvelope {
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
// 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