test(pitch_shift): tighten purity f0 to 1/196 so revert-of-fadeLen_ fails; add hop guard; clamp fadeLen_ in double before int64 cast

This commit is contained in:
2026-07-28 06:59:04 -04:00
parent 436a685984
commit 3d0406ef64
2 changed files with 21 additions and 11 deletions
+15 -8
View File
@@ -183,14 +183,21 @@ static void testRtDisciplineAndPassthrough() {
// --- 5. Spectral purity: a repitched pure sine stays a SINGLE shifted tone. ---
static void testRepitchSpectralPurity() {
// Frequencies are in cycles/sample (rate-free). The source tone is chosen ADVERSARIALLY:
// f0 * (window/2) = 5.5125 cycles, i.e. a fractional part of ~0.51 — content half a window
// apart in the ring is near ANTI-PHASE. The old dual-tap design (taps hard-locked w/2
// apart) cancelled almost completely at every crossfade midpoint for such tones — the DAW
// "severe beating / multiple partials from a pure sine" bug. A correct shifter keeps the
// output a single sinusoid at ratio*f0 with a steady amplitude.
// Frequencies are in cycles/sample (rate-free). The source tone is chosen ADVERSARIALLY
// on TWO axes simultaneously:
// (a) f0*(w/2) = (2205/2)/196 = 1102/196 ≈ 5.622 cycles (frac ≈ 0.622) — content half a
// window apart in the ring is near ANTI-PHASE. The old dual-tap design cancelled
// almost completely at every crossfade midpoint for such tones — the DAW "severe
// beating / multiple partials from a pure sine" bug.
// (b) ringLen_*f0 = 4410/196 = 22.5 EXACTLY — at ratio 4 the write head advances 4 taps
// per output frame, so each splice-period the outgoing tap crosses the writer at the
// HALF-period point of the source waveform (sign flip), producing a visible null when
// gNew == gOld if fadeLen_ is not clamped to headroom. With f0=0.005 this product
// is 22.05 (frac ≈ 0.05), near a zero-crossing — the artifact is near-benign, so the
// +24 st purity case would pass even with the clamping reverted. f0=1/196 forces the
// half-integer alignment that makes the pre-fix artifact catastrophic.
const std::int64_t w = 2205; // ~50 ms @ 44.1k (the product window)
const double f0 = 0.005; // source: period 200 samples
const double f0 = 1.0 / 196.0; // source: period 196 samples; see adversarial note above
const double ratios[] = {std::pow(2.0, 2.0 / 12.0), // +2 semitones (the DAW report: D from C)
std::pow(2.0, -3.0 / 12.0), // -3 semitones (down-shift path)
2.0, // octave up (nominal-fade boundary)
@@ -248,7 +255,7 @@ static void testRepitchSpectralPurity() {
// to ~0.78 of max on the CLEAN signal alone). Smallest phase-clean choice: exactly one
// output period per window (50..400 frames here), hop of half a window.
const std::size_t win = static_cast<std::size_t>(std::lround(1.0 / f1));
const std::size_t hop = win / 2;
const std::size_t hop = std::max<std::size_t>(1, win / 2);
double minRms = 1e9, maxRms = 0.0;
for (std::size_t s0 = from; s0 + win <= n; s0 += hop) {
double e = 0.0;