Close Γ-W1-T7 re-review: pitch-sync cadence math, floor-model regression check, evidence-count fix, one-home comments

New cadence-collapse-band test at P=1470 shows PSOLA eliminates the corner rather than regressing it (18.52% -> 0.00%).
This commit is contained in:
2026-08-02 05:34:43 -04:00
parent ef59265e7a
commit f1168e16eb
9 changed files with 249 additions and 40 deletions
+37 -17
View File
@@ -15,22 +15,42 @@ namespace reasampler::instrument::engine {
// 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 produces
// measurable splice debris 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. pitch_shift_tests
// (testStretchCadenceCornerArtifactEnergyAtRate2ShiftQuarter) asserts this corner directly at
// P=500/600/700: energy outside the fundamental runs 7-21% there against ~0% on an aligned
// control at the same rate/shift — zero-crossing period is NOT what it checks, since splice
// debris fools that estimator into reading the wrong period on a render whose fundamental is
// actually fine. (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.)
// eliminate it. A splice recurs every `pitch_shift.h`'s spliceJump() / |rate - shift| output
// frames (the tap's delay drifts across one nominal jump 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), fixed-window jump (2205): 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 produces measurable splice debris 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. pitch_shift_tests (testStretchCadenceCornerArtifactEnergyAtRate2ShiftQuarter)
// asserts this corner directly at P=500/600/700: energy outside the fundamental runs 7-21% there
// against ~0% on an aligned control at the same rate/shift — zero-crossing period is NOT what it
// checks, since splice debris fools that estimator into reading the wrong period on a render
// whose fundamental is provably correct. (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.)
//
// The above derives the floor with jump == window(), which is only the FIXED-WINDOW half of
// the story. Once a source period is known, spliceJump() is periodAlignedJump's answer instead
// (pitch_shift.h), and that answer can land NARROWER than window() — as low as ~0.63*window for
// some periods — which SHRINKS the interval and moves the failure threshold EARLIER, not later.
// There is no single closed-form floor for this case (the jump is itself a function of P), so
// read it at the concrete corner instead: at P=1470 (30 Hz at 44.1k) the same rate 2.0/shift
// 0.25 corner's jump narrows from window (2205) to 1470, and its interval from 1260 to
// 1470/1.75 = 840. Independently, at the plain (no time-stretch) rate 1.0 case, solving this
// same inequality for shift at P=1470 puts the failure threshold at shift = P/(jump+P): 0.4
// (-16 st) at the fixed-window jump (2205), 0.5 (-12 st) at the pitch-synchronous jump (1470) —
// the geometry fix that lets 30 Hz align AT ALL moves this unrelated cadence inequality's own
// trip point from roughly -16 st to roughly -12 st for the same source. Do NOT read this as a
// proven regression: the inequality above was calibrated for RANDOM-PHASE (unaligned) splices,
// and a pitch-synchronous splice is waveform-aligned by construction, which the inequality does
// not model — whether the shorter interval still produces audible debris once every splice
// lands in phase is what pitch_shift_tests' own P=1470 cadence-collapse-band measurement
// answers, not this derivation. Do not narrow kStretchRateMin/kStretchRateMax in response to
// this: sub-50 Hz sine material is first-class product material, not an edge case, and a
// narrower range does not fix a floor it does not reach.
//
// A SECOND, INDEPENDENT limit bound the same material, and no rate bound touched it. It is now
// CLOSED for any source whose period is detected, but the geometry is worth keeping because it
@@ -56,7 +76,7 @@ namespace reasampler::instrument::engine {
// pitch_shift_tests' testThirtyHertzSplicesAlignOnceTheSourcePeriodIsKnown, a different
// quantity from the raw percentages here. What survives: a period longer than the reachable
// jump (~1.25 windows, so below ~16 Hz at 50 ms) still cannot align, and a source with no
// single period falls back to this fixed-window geometry by design.
// single period falls back to it by design (periodAlignedJump, pitch_shift.h).
inline constexpr double kStretchRateMin = 0.5;
inline constexpr double kStretchRateMax = 2.0;
inline constexpr int kMaxFeedPerFrame = 2; // ceil(kStretchRateMax)