Γ-W1-T5 remediation: narrow the rate-bound claim, fix baseline/measurement provenance, correct §2.4 framing

Re-derives the splice-cadence inequality and adds a corner probe that FAILS at
P=500 by design, pending a ruling. Names the baseline commit and harness edit,
fixes measurement methodology, corrects the Trigger-AHD/rate coupling framing.
This commit is contained in:
2026-08-01 20:07:59 -04:00
parent 589a8e078b
commit ae59e9b70d
5 changed files with 196 additions and 49 deletions
+17 -4
View File
@@ -192,9 +192,9 @@ private:
// This frame's amplitude in [0,1] from the active envelope. Spline: the drawn contour read
// at the normalized position (one cached-segment compare per frame). Gate: AHDSR ticks once
// per output frame (envelope time is wall-clock, independent of read rate). Trigger: the AHD
// is evaluated at the source offset (readPos - startFrame) so its stages anchor to source
// frames regardless of pitch engine. Sets amplitudeDone_ on finish so advanceFrame frees
// the voice.
// is evaluated at the source offset (readPos - startFrame) — see the `ratio_ = stretchRate_`
// note below for what that means for Preserve's stage-time/rate coupling. Sets
// amplitudeDone_ on finish so advanceFrame frees the voice.
double tickAmplitude() {
double amp;
// playMode_ is Trigger whenever a spline is genuinely reachable (resolvePlay forces it —
@@ -454,7 +454,10 @@ private:
// 2^((note-root + pitchEnvSemis)/12) — the pitch envelope adds to the shift amount,
// never to the read rate. The feed runs one window ahead of readPos_ (the rings were
// primed with that window at start()), under the same sustain-loop wrap rule,
// reading integer source frames (nothing to interpolate).
// reading integer source frames into the ring — no RATE-DEPENDENT interpolation
// (unlike Varispeed's readPos_ below). The shifter's own read tap still carries a
// splice's sub-sample `frac` (pitch_shift.cpp), so it interpolates on every read,
// splice or no; that constant fractional delay is not a rate coupling.
const bool stereoOut = stereo && haveR && shiftR_.configured();
// The last real source frame is playEnd_-1 for Trigger or frameCount-1 for Gate.
// Once the feed reaches that bound the source is exhausted — feeding the held last
@@ -528,6 +531,16 @@ private:
// Preserve advances the read head at the STRETCH rate — the one duration control.
// Everything downstream of it (the loop wrap, the Trigger span, the spline phase)
// therefore stays a source-frame fact and scales by construction.
//
// Consequence (§2.4 of instrument-control-surface.md is explicit that staged
// envelopes' stage times are wall-clock and do NOT scale with rate): Trigger's amp
// AHD and filter AHD are both evaluated at sourceOffset() = readPos_ - startFrame_
// (tickAmplitude/tickFilterCutoff above), which now advances at stretchRate_ instead
// of always 1.0 — so those two envelopes will scale with a future non-unity Rate.
// This is NEW here: Preserve's ratio_ was pinned at 1.0 before this track, so those
// stage times were exact wall-clock. It is latent (nothing publishes a non-unity
// rate yet) and owned by the track that adds the Rate control, not this one — Gate's
// AHDSR (env_.tick(), per-output-frame) and every spline contour are unaffected.
ratio_ = stretchRate_;
} else {
// VARISPEED: pitch and duration coupled. The read rate carries the repitch; the