Γ-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:
@@ -11,13 +11,22 @@
|
||||
namespace reasampler::instrument::engine {
|
||||
|
||||
// The playback rates the Preserve DSP is measured over, and therefore the only ones it
|
||||
// accepts. Two independent reasons they are here and not wider:
|
||||
// - the ceiling is what bounds a voice's per-output-frame feed loop (kMaxFeedPerFrame source
|
||||
// frames), which is the RT-safety argument for feeding a variable count at all;
|
||||
// - the splice search can only align a period it can see. The tap's delay drifts at
|
||||
// |rate - shift| per frame, so a wide rate over a deep DOWN-shift splices faster than one
|
||||
// period of the output tone and the correlation stops holding the pitch: measured at rate
|
||||
// 4.0 with -24 st, the observed period came out 539 frames against 785 wanted.
|
||||
// accepts. The ceiling also bounds a voice's per-output-frame feed loop (kMaxFeedPerFrame
|
||||
// source frames) — the RT-safety argument for feeding a variable count at all.
|
||||
//
|
||||
// This range NARROWS the splice-cadence failure onto the source fundamental; it does not
|
||||
// eliminate it. A splice recurs every `window / |rate - shift|` output frames (the tap's
|
||||
// delay drifts across one window at that per-frame rate); the shifted tone's own period is
|
||||
// `sourcePeriod / shift` output frames. Whenever the recurrence interval is shorter than
|
||||
// that period, a splice lands inside a single perceived cycle and the correlation search
|
||||
// has less than one period to align against. Measured at rate 4.0, shift 0.25 (-24 st):
|
||||
// interval 2205/3.75 ~= 588 vs period ~4*P ~= 785 frames (P ~= 196) — matches the originally
|
||||
// observed 539-vs-785 failure. This range's ceiling (2.0, not 4.0) raises the safe floor, it
|
||||
// does not remove it: at rate 2.0, shift 0.25, interval = 2205/1.75 = 1260 still fails for
|
||||
// any source period P > 315 frames (~140 Hz at 44.1k) — inside bass/low-vocal material, and
|
||||
// -24 st is reachable from the Pitch knob alone. (The pre-stretch rate-1.0 engine's floor by
|
||||
// the same inequality is P > 735, ~60 Hz — what this range raises the floor from, not what
|
||||
// it removes.)
|
||||
inline constexpr double kStretchRateMin = 0.5;
|
||||
inline constexpr double kStretchRateMax = 2.0;
|
||||
inline constexpr int kMaxFeedPerFrame = 2; // ceil(kStretchRateMax)
|
||||
|
||||
@@ -327,6 +327,8 @@ void Voice::retune(int note) {
|
||||
// Filter key-tracking follows the pitch: it is a function of the note, so a slide moves it
|
||||
// too. The velocity offset deliberately stays the first note's, matching velocityGain_.
|
||||
if (filterOn_) updateFilterCutoffBase(note);
|
||||
// stretchRate_ (Preserve's duration control) is untouched here too — it is a note-on latch
|
||||
// like velocityGain_, not a per-note property to re-resolve on a legato slide.
|
||||
}
|
||||
|
||||
void Voice::release() {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user