fix(q-w0): six audit fix-nows — linked-lag stereo SOLA, playable-span prime bound, declick dead-state, provenance cursor hardening, rate-derived gain ramp + fade ceiling
This commit is contained in:
@@ -23,6 +23,9 @@
|
||||
// 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"
|
||||
|
||||
@@ -491,6 +494,68 @@ static void testFreezeTailContinuousTone() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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<AudioSample> srcL(n + static_cast<std::size_t>(w));
|
||||
std::vector<AudioSample> srcR(n + static_cast<std::size_t>(w));
|
||||
for (std::size_t i = 0; i < srcL.size(); ++i) {
|
||||
srcL[i] = static_cast<AudioSample>(std::sin(2.0 * kPi * fL * static_cast<double>(i)));
|
||||
srcR[i] = static_cast<AudioSample>(std::sin(2.0 * kPi * fR * static_cast<double>(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<std::size_t>(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();
|
||||
@@ -499,6 +564,7 @@ int main() {
|
||||
testRepitchSpectralPurityAndOnset();
|
||||
testUnityBitExactAndLatency();
|
||||
testFreezeTailContinuousTone();
|
||||
testStereoLinkedLagSharedSchedule();
|
||||
|
||||
if (g_fail == 0) {
|
||||
std::printf("all pitch_shift tests passed\n");
|
||||
|
||||
Reference in New Issue
Block a user