Γ-W1-T7: make Preserve's splices pitch-synchronous — the jump is a whole number of the source's own period, detected once at load

30 Hz out-of-band energy 15.45% -> 0.00%; the 29 Hz rate-2.0 detune -133 -> +0 cents.
An unknown period keeps the fixed-window geometry bit for bit. The detector cannot
reach process(): sampler_core does not link it.
This commit is contained in:
2026-08-01 23:19:16 -04:00
parent 163ab11e05
commit 93230208ff
17 changed files with 1085 additions and 137 deletions
+21 -14
View File
@@ -32,20 +32,27 @@ namespace reasampler::instrument::engine {
// 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.)
//
// A SECOND, INDEPENDENT limit binds the same material, and no rate bound touches it. A splice
// relocates the tap by the nominal window refined by a search over +/- window/4, so the
// reachable relocation distances are exactly [0.75, 1.25] * window; a phase-aligned splice
// needs a WHOLE NUMBER of source periods inside that one interval. The interval is 0.5*window
// wide, so any period <= window/2 always has a multiple in it — but above that, coverage
// breaks into disjoint bands (n=1 covers periods [0.75, 1.25]*window, n=2 covers
// [0.375, 0.625]*window) and the gap between them is reachable by nothing. Because both the
// interval and the period scale with the sample rate, the unalignable set is fixed in Hz by
// the window's MILLISECONDS: at 50 ms that is f < 16 Hz and 26.7 Hz < f < 32 Hz. Measured
// (Release, 44.1k and 48k) at 30 Hz: the rendered pitch stays correct, but energy outside the
// fundamental is 3.6% at +2 st / rate 1.0 and 15.5% at rate 2.0, against 0.00% at 34 Hz under
// identical conditions; at 29 Hz / rate 2.0 the tone itself lands 7.4% flat. Unlike the
// cadence inequality above, this one is not about how OFTEN a splice fires — a window of at
// least two source periods removes it outright, and nothing else does.
// 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
// is what the fixed-window fallback still lives under. A splice relocated the tap by the
// nominal window refined by a search over +/- window/4, so the reachable relocation distances
// were exactly [0.75, 1.25] * window; a phase-aligned splice needs a WHOLE NUMBER of source
// periods inside that interval. The interval is 0.5*window wide, so any period <= window/2
// always has a multiple in it — but above that, coverage breaks into disjoint bands (n=1 covers
// periods [0.75, 1.25]*window, n=2 covers [0.375, 0.625]*window) and the gap between them was
// reachable by nothing. Because both the interval and the period scale with the sample rate,
// that unalignable set is fixed in Hz by the window's MILLISECONDS: at 50 ms, f < 16 Hz and
// 26.7 Hz < f < 32 Hz. Measured there (Release, 44.1k and 48k) at 30 Hz: the rendered pitch
// stayed correct, but energy outside the fundamental was 3.6% at +2 st / rate 1.0 and 15.5% at
// rate 2.0, against 0.00% at 34 Hz under identical conditions; at 29 Hz / rate 2.0 the tone
// itself landed 7.4% flat (-133 cents).
//
// The fix is not a wider window: it is a nominal jump that is a whole number of the source's
// own periods, so an aligned landing point exists by construction (pitch_shift.h's
// periodAlignedJump, fed by period_detect at load). The same measurements then read 0.00% and
// 0.00%, and 29 Hz renders at +0.0 cents. 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.
inline constexpr double kStretchRateMin = 0.5;
inline constexpr double kStretchRateMax = 2.0;
inline constexpr int kMaxFeedPerFrame = 2; // ceil(kStretchRateMax)