// Standalone tests for reasampler::PitchShifter — the S16 Preserve-engine DSP core. No VST3, // no REAPER, no vendor, no test framework. The compile-time proof it does NOT drag the WDL // chain is the CMake target linking only pitch_shift (+ peaks). // // Covers (PLAN.md S16 / CONTEXT.md §Pitch engine modes — Preserve): // 1. duration invariance — N inputs yield N outputs at every shift ratio (the load-bearing // Preserve property: a transposed render is the SAME frame length as the un-transposed one). // 2. unity pass-through fidelity — ratio 1.0 reproduces the input closely (a shifter at unity // must not mangle the signal). // 3. transpose direction — an octave-up shift raises the observed pitch (period shortens), an // octave-down lowers it (period lengthens), measured on a synthesized sine. // 4. RT discipline surrogate — after configure()+warm() (the off-thread setup), a long // process() run never resizes the ring (checked via window() constancy) and never returns // NaN/inf; pass-through (unconfigured) returns input verbatim. // 5. spectral purity + onset integrity (GA / GA2 regressions) — a PRIMED repitched PURE // SINE must come out as a SINGLE tone at the shifted frequency FROM THE VERY FIRST // MILLISECOND: no zero-gaps anywhere (the GA2 DAW report: silence-warmed rings made // every early splice jump into zeros — burst/gap/burst stutter in the first few ms), // and a per-block least-squares residual floor that catches harmonics, splice-cadence // sideband combs, and crossfade cancellation alike. Ratios cover the FULL playable // range the DAW report exercised: +2/-3 st, +/-1 octave, +24 st, +48 st (C8 from C4, // ratio 16) and -36 st (C1 from C4, ratio 1/8). // 6. unity + latency contract — asserted bit-exactly: a warm()ed shifter at ratio 1.0 IS a // clean window delay; a prime()d one has ZERO added latency (out[i] == src[i] to the // bit) — the GA2 immediate-onset claim. // 8. stereo linked lag (Q-W0 T1-01) — a follower channel driven via processLinked() mirrors // the master's splice decision (jump/lag/frac/fadeLen AND firing frame) exactly, on // decorrelated stereo content where an independent per-channel search provably diverges. #include "../src/vst/pitch_shift.h" #include #include #include using namespace reasampler; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) static bool approx(double a, double b, double tol) { return std::fabs(a - b) <= tol; } constexpr double kPi = 3.14159265358979323846; // A sine of `cycles` periods over `frames` frames. static std::vector sine(std::size_t frames, double cycles) { std::vector s(frames); for (std::size_t i = 0; i < frames; ++i) { s[i] = static_cast(std::sin(2.0 * kPi * cycles * static_cast(i) / static_cast(frames))); } return s; } // Average spacing between positive-going zero crossings (the observed period). static double observedPeriod(const std::vector& out, std::size_t from) { std::vector up; for (std::size_t i = from + 1; i < out.size(); ++i) { if (out[i - 1] <= 0.0f && out[i] > 0.0f) up.push_back(i); } if (up.size() < 2) return 0.0; double sum = 0.0; for (std::size_t i = 1; i < up.size(); ++i) sum += static_cast(up[i] - up[i - 1]); return sum / static_cast(up.size() - 1); } // --- 1. Duration invariance across shift ratios. --- static void testDurationInvariance() { // The core Preserve property: whatever the shift ratio, one input frame yields one output // frame. So a shifter fed N frames produces exactly N frames — a transposed render is the // same length as an un-transposed one (unlike Varispeed, where an octave up halves length). const std::size_t n = 4000; const std::vector in = sine(n, 40.0); const double ratios[] = {0.5, 1.0, 2.0, std::pow(2.0, 7.0 / 12.0)}; for (double r : ratios) { PitchShifter ps; ps.configure(2205); // ~50 ms @ 44.1k ps.warm(); ps.setShiftRatio(r); std::size_t produced = 0; for (std::size_t i = 0; i < n; ++i) { const AudioSample o = ps.process(in[i]); (void)o; ++produced; // exactly one output per input, unconditionally. } CHECK(produced == n); // duration held at every ratio. } } // --- 2. Unity pass-through fidelity. --- static void testUnityRoughlyReproduces() { // At ratio 1.0 the shifter should reproduce the input's PITCH faithfully (the OLA taps run // in lockstep with the writer). Amplitude/phase warble is allowed (basic OLA), but the // observed period must match the source period within a small tolerance past the warm-up. const std::size_t n = 8000; const double cycles = 40.0; const double nativePeriod = static_cast(n) / cycles; // 200 const std::vector in = sine(n, cycles); PitchShifter ps; ps.configure(2205); ps.warm(); ps.setShiftRatio(1.0); std::vector out(n); for (std::size_t i = 0; i < n; ++i) out[i] = ps.process(in[i]); // Measure past the initial half-window latency region. const double p = observedPeriod(out, 3000); CHECK(p > 0.0); CHECK(approx(p, nativePeriod, nativePeriod * 0.10)); // within 10% of source period } // --- 3. Transpose direction: up shortens the period, down lengthens it. --- static void testTransposeDirection() { const std::size_t n = 12000; const double cycles = 60.0; const double nativePeriod = static_cast(n) / cycles; // 200 const std::vector in = sine(n, cycles); // Octave up: output period ~ half the source period (higher pitch). { PitchShifter ps; ps.configure(2205); ps.warm(); ps.setShiftRatio(2.0); std::vector out(n); for (std::size_t i = 0; i < n; ++i) out[i] = ps.process(in[i]); const double p = observedPeriod(out, 4000); CHECK(p > 0.0); CHECK(approx(p, nativePeriod / 2.0, nativePeriod * 0.15)); // period halves } // Octave down: output period ~ double the source period (lower pitch). { PitchShifter ps; ps.configure(2205); ps.warm(); ps.setShiftRatio(0.5); std::vector out(n); for (std::size_t i = 0; i < n; ++i) out[i] = ps.process(in[i]); const double p = observedPeriod(out, 4000); CHECK(p > 0.0); CHECK(approx(p, nativePeriod * 2.0, nativePeriod * 0.30)); // period doubles } } // --- 4. RT discipline surrogate + pass-through. --- static void testRtDisciplineAndPassthrough() { // Unconfigured shifter passes input through verbatim (a Varispeed voice never allocates one). { PitchShifter ps; CHECK(!ps.configured()); CHECK(ps.process(0.37f) == 0.37f); // exact pass-through CHECK(ps.process(-0.9f) == -0.9f); } // Configured: the window is fixed at configure() and never changes across a long run (no // per-frame Resize), and no output is NaN/inf (numerically well-behaved OLA). { PitchShifter ps; ps.configure(1024); ps.warm(); const std::int64_t w = ps.window(); CHECK(w == 1024); ps.setShiftRatio(std::pow(2.0, 5.0 / 12.0)); const std::vector in = sine(20000, 100.0); for (std::size_t i = 0; i < in.size(); ++i) { const AudioSample o = ps.process(in[i]); CHECK(std::isfinite(o)); } CHECK(ps.window() == w); // window unchanged -> ring never resized mid-run } // A non-positive shift ratio is ignored (keeps the last valid ratio) — never stalls/reverses. { PitchShifter ps; ps.configure(512); ps.warm(); ps.setShiftRatio(1.0); ps.setShiftRatio(-2.0); // ignored ps.setShiftRatio(0.0); // ignored for (int i = 0; i < 2000; ++i) CHECK(std::isfinite(ps.process(0.5f))); } // Degenerate window (<= 1) stays pass-through even after configure. { PitchShifter ps; ps.configure(1); CHECK(!ps.configured()); CHECK(ps.process(0.25f) == 0.25f); } } // --- 5. Spectral purity + onset integrity: a PRIMED repitched pure sine is a SINGLE shifted // tone from the very first millisecond. --- static void testRepitchSpectralPurityAndOnset() { // Frequencies are in cycles/sample (rate-free). The source tone is chosen ADVERSARIALLY // on TWO axes simultaneously: // (a) f0*(w/2) ≈ (2205/2)/196.37 ≈ 5.609 cycles (frac ≈ 0.609) — 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.37 ≈ 22.46 — near the half-integer alignment that makes the // pre-fix fade-headroom artifact visible at ratio 4 (+24 st). Additionally, period // 196.37 is NON-INTEGER, so the correlation peak is NOT on the integer lag grid; the // sub-sample parabolic refinement is LOAD-BEARING to stay at the -84 dB floor — the // old integer f0=1/196 put the optimum on the grid and the parabola contributed // nothing, making the -30 dB floor reachable without it. // // The shifter is driven exactly as the Voice drives it since GA2: prime() with the first // window of the source, then stream the CONTINUATION — so the measurements start at // output frame 0 and the onset regime (early splices near the primed boundary, the DAW // "zero-sample gaps in the first few ms" report) is inside the assertions, not skipped. const std::int64_t w = 2205; // ~50 ms @ 44.1k (the product window) const double f0 = 1.0 / 196.37; // NON-INTEGER period: the sub-sample correlation peak // is NOT on the integer grid, so the parabolic // refinement MUST contribute to achieve a clean // aligned splice — reverting it now FAILS this test. // (Old integer 1/196 put the optimum on the grid, // letting the parabola contribute nothing; the −30 dB // residual floor was then reachable without it.) const double ratios[] = {std::pow(2.0, 2.0 / 12.0), // +2 semitones (D from C) std::pow(2.0, -3.0 / 12.0), // -3 semitones (down-shift path) 2.0, // octave up (the GA2 report: C5) std::pow(2.0, 24.0 / 12.0), // +24 st: ratio 4 — the ratio-scaled- // fade target (unscaled fade would // read stale data at ~75% gain) std::pow(2.0, 48.0 / 12.0), // +48 st: ratio 16 — C8 from C4 (the // GA2 "awful at C8" report; fast // splice cadence, short fades) std::pow(2.0, -12.0 / 12.0), // octave down (full down-shift path) std::pow(2.0, -36.0 / 12.0)}; // -36 st: ratio 1/8 — C1 from C4 // (the GA2 down-shift report) for (double r : ratios) { PitchShifter ps; ps.configure(w); const std::size_t n = 120000; std::vector src(n + static_cast(w)); for (std::size_t i = 0; i < src.size(); ++i) { src[i] = static_cast( std::sin(2.0 * kPi * f0 * static_cast(i))); } ps.prime(src.data(), w); // the Voice's note-on path: real content, not warm zeros ps.setShiftRatio(r); std::vector out(n); for (std::size_t i = 0; i < n; ++i) { out[i] = static_cast(ps.process(src[i + static_cast(w)])); } // (a) ONSET/GAP integrity over the ENTIRE run, frame 0 included: no near-zero run // longer than 32 frames (~0.7 ms). A unit-amplitude shifted sine dwells below 1e-3 // for well under one frame per zero crossing even at the lowest ratio here, while the // pre-fix onset gaps were hundreds to thousands of frames of literal silence. std::size_t worstGap = 0, run = 0; for (std::size_t i = 0; i < n; ++i) { if (std::fabs(out[i]) < 1e-3) { ++run; if (run > worstGap) worstGap = run; } else { run = 0; } } CHECK(worstGap < 32); // (b) PER-BLOCK least-squares fit of a*sin + b*cos at the SHIFTED frequency, from the // FIRST block. Fitting phase per block deliberately tolerates the slow (pitch-true, // inaudible) SOLA phase wander across seconds while catching everything audible: // harmonics ("square-ish"), splice-cadence sideband combs (the spectrogram alias // lines), crossfade cancellation, and onset gaps all land in the residual or collapse // the in-block fit amplitude. Solve the exact 2x2 normal equations per block. const double f1 = r * f0; const std::size_t block = 4096; for (std::size_t b0 = 0; b0 + block <= n; b0 += block) { double sss = 0.0, scc = 0.0, ssc = 0.0, sys = 0.0, syc = 0.0; for (std::size_t i = b0; i < b0 + block; ++i) { const double ph = 2.0 * kPi * f1 * static_cast(i); const double s = std::sin(ph), c = std::cos(ph); sss += s * s; scc += c * c; ssc += s * c; sys += out[i] * s; syc += out[i] * c; } const double det = sss * scc - ssc * ssc; CHECK(det > 0.0); const double a = (sys * scc - syc * ssc) / det; const double b = (syc * sss - sys * ssc) / det; double residSq = 0.0, fitSq = 0.0; for (std::size_t i = b0; i < b0 + block; ++i) { const double ph = 2.0 * kPi * f1 * static_cast(i); const double fit = a * std::sin(ph) + b * std::cos(ph); residSq += (out[i] - fit) * (out[i] - fit); fitSq += fit * fit; } const double fitRms = std::sqrt(fitSq / static_cast(block)); const double residRms = std::sqrt(residSq / static_cast(block)); // The shifted tone is there at full amplitude (unit sine RMS ~0.707) in EVERY // block — a gapped or beating block collapses this... CHECK(fitRms > 0.6); CHECK(fitRms < 0.8); // ...and it is the ONLY thing there: residual at least 84 dB under the tone. // With the non-integer f0=1/196.37, the sub-sample parabolic refinement is // LOAD-BEARING: reverting it raises the floor to ~-50 dB (ratio 1/8), failing here. // With integer f0=1/196 the optimum was on the integer grid and the parabola // contributed nothing — the old floor of -30 dB was reachable without it. // Engine steady-state measures -84 dB and better across all 7 tested ratios; // moderate ratios (+/-2 st, octaves) sit at -88 dB typical. CHECK(residRms < 0.000063 * fitRms); // -84 dB floor } } } // --- 6. Unity + latency contract: warm = bit-exact window delay; primed = bit-exact ZERO // latency. --- static void testUnityBitExactAndLatency() { // A configured shifter at ratio 1.0 parks the tap mid-band (no splice ever fires) at an // integral delay (no interpolation error). After warm() that delay is exactly one window // of declared silence, so out[i] == in[i - w] to the bit. After prime() with the first // window of source the tap sits ON src[0] — out[i] == src[i] to the bit from the very // first frame: the GA2 zero-structural-latency (immediate onset) claim. const std::int64_t w = 2205; // the product window const std::size_t n = 6000; const std::vector in = sine(n + static_cast(w), 37.0); // warm(): a clean, bit-exact one-window delay of the streamed input. { PitchShifter ps; ps.configure(w); ps.warm(); ps.setShiftRatio(1.0); std::vector out(n); for (std::size_t i = 0; i < n; ++i) out[i] = ps.process(in[i]); std::size_t badSilence = 0, badDelay = 0; for (std::size_t i = 0; i < static_cast(w); ++i) { if (out[i] != 0.0f) ++badSilence; // pre-latency region: declared silence, exact } for (std::size_t i = static_cast(w); i < n; ++i) { if (out[i] != in[i - static_cast(w)]) ++badDelay; // bit-exact delay } CHECK(badSilence == 0); CHECK(badDelay == 0); } // prime(): zero added latency — the output IS the source from frame 0, bit-exact. { PitchShifter ps; ps.configure(w); ps.prime(in.data(), w); ps.setShiftRatio(1.0); std::size_t badZeroLat = 0; for (std::size_t i = 0; i < n; ++i) { if (ps.process(in[i + static_cast(w)]) != in[i]) ++badZeroLat; } CHECK(badZeroLat == 0); } } // --- 7. Tail wind-down (GA3): freezeTail() at source exhaustion keeps the output a // continuous, full-amplitude tone at the shifted frequency — the splice machinery // recycles the ring's frozen ALL-REAL tail instead of chopping against held-DC // padding (the DAW "ring modulation" troughs growing toward the note end). --- static void testFreezeTailContinuousTone() { const std::int64_t w = 2205; const double f0 = 1.0 / 196.37; // non-integer period (the test-5 adversarial tone) const std::size_t stream = 20000; // frames fed before exhaustion (several splice cycles) const double ratios[] = {std::pow(2.0, 7.0 / 12.0), // +7 st (the DAW report regime) 2.0, // octave up std::pow(2.0, 24.0 / 12.0), // +24 st: fast frozen drain std::pow(2.0, -5.0 / 12.0), // -5 st (down-shift tail) 1.0}; // unity: frozen delay drains at 1 — // splices NOW fire even at unity for (double r : ratios) { PitchShifter ps; ps.configure(w); std::vector src(stream + static_cast(w)); for (std::size_t i = 0; i < src.size(); ++i) { src[i] = static_cast( std::sin(2.0 * kPi * f0 * static_cast(i))); } ps.prime(src.data(), w); ps.setShiftRatio(r); for (std::size_t i = 0; i < stream; ++i) { (void)ps.process(src[i + static_cast(w)]); } // Source exhausted: freeze (idempotent) and keep producing for one full window — the // longest a Voice runs frozen (its own note end lands within a window of exhaustion). CHECK(!ps.tailFrozen()); ps.freezeTail(); ps.freezeTail(); // double-freeze harmless CHECK(ps.tailFrozen()); const std::size_t tail = static_cast(w); std::vector out(tail); for (std::size_t i = 0; i < tail; ++i) { out[i] = static_cast(ps.process(0.0f)); // input ignored while frozen CHECK(std::isfinite(out[i])); } // (a) No dead stretches: a unit-amplitude tone dwells below 0.05 only a few frames // per zero crossing; the pre-GA3 DC chop ran hundreds. std::size_t worstGap = 0, run = 0; for (std::size_t i = 0; i < tail; ++i) { if (std::fabs(out[i]) < 0.05) { ++run; if (run > worstGap) worstGap = run; } else { run = 0; } } CHECK(worstGap < 24); // (b) Full amplitude throughout: every 256-frame block spans > a half period at all // tested ratios, so a continuous tone peaks near 1.0 in each. for (std::size_t b = 0; b + 256 <= tail; b += 256) { double peak = 0.0; for (std::size_t i = b; i < b + 256; ++i) { if (std::fabs(out[i]) > peak) peak = std::fabs(out[i]); } CHECK(peak > 0.5); CHECK(peak < 1.1); // aligned complementary fades: no cancellation, no bulge } } // Freeze landing MID-CROSSFADE: at ratio 2 from a fresh prime the tap drains from delay // w at 1/frame, splices at w/4 (frame 3w/4), then fades for w/4 frames — so frame // 3w/4 + w/8 is deterministically mid-fade. The frozen writer makes the outgoing tap // close at the FULL ratio; the transition caps the live fade so it completes before // reading lapped content — output must stay finite, gap-free, and bounded. { PitchShifter ps; ps.configure(w); std::vector src(4 * static_cast(w)); for (std::size_t i = 0; i < src.size(); ++i) { src[i] = static_cast( std::sin(2.0 * kPi * f0 * static_cast(i))); } ps.prime(src.data(), w); ps.setShiftRatio(2.0); const std::size_t preFreeze = static_cast(3 * w / 4 + w / 8); double lastPre = 0.0; for (std::size_t i = 0; i < preFreeze; ++i) { lastPre = static_cast(ps.process(src[i + static_cast(w)])); } ps.freezeTail(); std::size_t worstGap = 0, run = 0; for (std::size_t i = 0; i < static_cast(w); ++i) { const double o = static_cast(ps.process(0.0f)); CHECK(std::isfinite(o)); CHECK(std::fabs(o) < 1.1); if (std::fabs(o) < 0.05) { ++run; if (run > worstGap) worstGap = run; } else { run = 0; } } CHECK(worstGap < 24); // reset()/prime() clear the freeze: the shifter is fully reusable for the next // note-on, and a primed unity run is STILL bit-exact zero-latency (no stale state). ps.reset(); CHECK(!ps.tailFrozen()); ps.prime(src.data(), w); ps.setShiftRatio(1.0); std::size_t badZeroLat = 0; for (std::size_t i = 0; i < 2000; ++i) { if (ps.process(src[i + static_cast(w)]) != src[i]) ++badZeroLat; } CHECK(badZeroLat == 0); } // STEP-DETECTOR: freeze-transition continuity using a ramp source where tapA and tapB // read values that differ by a predictable constant (≈ A * w / N), making the crossfade // gain step directly visible in the output. With a ramp, the per-frame natural change is // A/(N) * ratio ≈ 0.0002 per frame; the un-fixed gain step is ~0.247 * (w/N) ≈ 0.062 — // roughly 300x the natural rate. A threshold of 0.02 clearly separates fixed from unfixed. // // The ramp also defeats correlation-alignment (all lags score equally on a linear ramp), // so the splice jump of one window guarantees tapA - tapB = −A*w/N regardless of lag. { PitchShifter ps; ps.configure(w); // Ramp from 0.0 to 1.0 over 4*w frames (same buffer size as the mid-crossfade case). const std::size_t rampLen = 4 * static_cast(w); std::vector ramp(rampLen); for (std::size_t i = 0; i < rampLen; ++i) { ramp[i] = static_cast(static_cast(i) / static_cast(rampLen - 1)); } ps.prime(ramp.data(), w); ps.setShiftRatio(2.0); // Drive to the deterministic mid-fade freeze point: same preFreeze offset as above. const std::size_t preFreeze = static_cast(3 * w / 4 + w / 8); double lastPre = 0.0; for (std::size_t i = 0; i < preFreeze; ++i) { lastPre = static_cast( ps.process(ramp[i + static_cast(w)])); } ps.freezeTail(); // First frozen frame — if gNew steps at the freeze boundary the output jumps by // ~deltaGain * (tapA - tapB) ≈ 0.247 * 0.25 = 0.062. const double firstFrozen = static_cast(ps.process(0.0f)); CHECK(std::isfinite(firstFrozen)); // Natural per-frame ramp advance at ratio 2 ≈ 2/(4*w - 1) ≈ 0.0002; the un-fixed // step is ~0.062. Threshold 0.02 is 100x the natural rate but well below the step. const double transitionStep = std::fabs(firstFrozen - lastPre); CHECK(transitionStep < 0.02); } } // --- 8. Stereo linked lag (Q-W0 T1-01): a follower channel driven via processLinked() // applies EXACTLY the master's splice decision — same firing frame, same jump, same // lag, same sub-sample frac, same fade length — so a stereo pair shares ONE splice // schedule (no inter-channel offset re-drawn per splice: the pre-fix image-wander / // mono-sum-combing mechanism). The divergence witness: an INDEPENDENT shifter fed the // follower's content picks a different lag on the same schedule, proving the mirror // assertion is not vacuous (the two channels' contents genuinely disagree on the best // alignment). --- static void testStereoLinkedLagSharedSchedule() { const std::int64_t w = 2205; // the product window const std::size_t n = 40000; // ~17 splice cycles at ratio 2 // Decorrelated "stereo" content: two different non-integer-period tones, so each // channel's own correlation optimum lands on a different lag. const double fL = 1.0 / 196.37; const double fR = 1.0 / 123.13; std::vector srcL(n + static_cast(w)); std::vector srcR(n + static_cast(w)); for (std::size_t i = 0; i < srcL.size(); ++i) { srcL[i] = static_cast(std::sin(2.0 * kPi * fL * static_cast(i))); srcR[i] = static_cast(std::sin(2.0 * kPi * fR * static_cast(i))); } PitchShifter master, follower, independent; master.configure(w); follower.configure(w); independent.configure(w); master.prime(srcL.data(), w); follower.prime(srcR.data(), w); // linked: R content, master's decisions independent.prime(srcR.data(), w); // control: R content, OWN search (pre-fix behavior) master.setShiftRatio(2.0); follower.setShiftRatio(2.0); independent.setShiftRatio(2.0); int spliceCount = 0; bool followerDiverged = false; bool independentDiverged = false; for (std::size_t i = 0; i < n; ++i) { const std::size_t si = i + static_cast(w); (void)master.process(srcL[si]); const SpliceEvent& em = master.lastSplice(); const AudioSample oR = follower.processLinked(srcR[si], em); CHECK(std::isfinite(oR)); // The follower mirrors the master's decision EXACTLY, every frame (fired == false // frames included — a follower must never splice on its own). const SpliceEvent& ef = follower.lastSplice(); if (ef.fired != em.fired || ef.jump != em.jump || ef.lag != em.lag || ef.frac != em.frac || ef.fadeLen != em.fadeLen) { followerDiverged = true; } if (em.fired) ++spliceCount; // The control: same content as the follower, own search. Its decision differing // from the master's proves the mirror assertion above is load-bearing. (void)independent.process(srcR[si]); const SpliceEvent& ei = independent.lastSplice(); if (ei.fired != em.fired || ei.lag != em.lag || ei.frac != em.frac) { independentDiverged = true; } } CHECK(spliceCount >= 3); // the run actually exercised several splices CHECK(!followerDiverged); // linked lag: one decision, one schedule, both channels CHECK(independentDiverged); // non-tautology witness: unlinked channels DO disagree } int main() { testDurationInvariance(); testUnityRoughlyReproduces(); testTransposeDirection(); testRtDisciplineAndPassthrough(); testRepitchSpectralPurityAndOnset(); testUnityBitExactAndLatency(); testFreezeTailContinuousTone(); testStereoLinkedLagSharedSchedule(); if (g_fail == 0) { std::printf("all pitch_shift tests passed\n"); return 0; } std::printf("%d pitch_shift check(s) failed\n", g_fail); return 1; }