Γ-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
+228 -8
View File
@@ -30,11 +30,17 @@
// 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.
// 10. pitch-synchronous splices — the nominal jump snapped to a whole number of source
// periods: the jump law itself, the bit-identical unknown-period fallback, 30 Hz and
// 29 Hz (the two symptoms of the unalignable gap), and the cadence corner, which this
// leaves where it found it.
#include "../src/core/instrument/engine/pitch_shift.h"
#include "energy_outside_fundamental.h"
#include "tone_metrics.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <vector>
@@ -595,14 +601,17 @@ static void testStereoLinkedLagSharedSchedule() {
// source frame due on an output frame go through writeFrame (no output), the last through
// process(); an output frame with none due takes processNoInput(). Returns the output plus,
// via `consumed`, how much source it ate.
// `sourcePeriod` > 0 puts the shifter on the pitch-synchronous jump the loader would have
// given it; 0 (the default) is the fixed-window fallback every pre-PSOLA call here exercises.
static std::vector<double> runStretch(const std::vector<AudioSample>& src, std::int64_t w,
double feedRate, double shift, std::size_t outFrames,
std::size_t* consumed) {
std::size_t* consumed, double sourcePeriod = 0.0) {
PitchShifter ps;
ps.configure(w);
ps.prime(src.data(), w);
ps.setShiftRatio(shift);
ps.setFeedRate(feedRate);
ps.setSourcePeriod(sourcePeriod);
std::size_t pos = static_cast<std::size_t>(w);
double debt = 0.0;
std::vector<double> out(outFrames);
@@ -746,12 +755,23 @@ static void testStretchAndShiftComposeSafely() {
// observable here: an investigation (test_preserve_low_frequency.cpp) found the P=500
// render's FUNDAMENTAL within 0.03% of target by autocorrelation and spectral peak alike,
// while the zero-crossing estimator read 23% flat — splice debris adds spurious crossings
// the count cannot tell from a real detune. Energy outside the fundamental tracks the actual
// damage instead: measured here (same rate/shift/source, this file's own metric parameters)
// at 7.31% / 14.41% / 21.22% for P=500/600/700, against 0.10% on an alignable control (P=200,
// below the safe floor) at the same rate and shift — so that is what this asserts: a known,
// characterised property of the range, not a pass/fail on a period estimate. A failure on
// either bound below is a finding — report it, don't retune the thresholds to hide it.
// the count cannot tell from a real detune. Energy outside the fundamental is measured here
// (same rate/shift/source, this file's own metric parameters) at 7.31% / 14.41% / 21.22% for
// P=500/600/700, against 0.10% on an alignable control (P=200, below the safe floor) at the
// same rate and shift. Those readings are stable and are what the bounds below hold.
//
// **CORRECTED — what those three readings MEAN.** They were once read as the corner's damage.
// They are almost entirely the metric's own floor: an ideal tone at the same want-period,
// measured identically, reads 7.08% / 13.90% / 20.92% (idealToneFloorPercent, below), because
// a long period under a 32768-frame segment leaks part of its own mainlobe outside the +/-6%
// band. The corner's real EXCESS over that floor is 0.23% / 0.51% / 0.30% — small, real, and
// nothing like the headline numbers. The alignable control's 0.10% is genuinely near-zero only
// because its want-period is short enough to have almost no floor. The bounds below are kept
// as a stable regression tripwire on the raw readings; read the excess, not the reading.
//
// This measures the FIXED-WINDOW path — no source period is set, which is what a capture with
// no single period (percussive, polyphonic, noise) gets. What the same corner does once the
// splice is pitch-synchronous is the test immediately after this one.
static void testStretchCadenceCornerArtifactEnergyAtRate2ShiftQuarter() {
using reasampler::test_support::energyOutsideFundamentalPercent;
const std::int64_t w = 2205;
@@ -804,6 +824,201 @@ static void testStretchCadenceCornerArtifactEnergyAtRate2ShiftQuarter() {
}
}
// --- 10. Pitch-synchronous splices: the nominal jump is a whole number of SOURCE periods. ---
// The out-of-band metric's OWN floor at a given period: a mathematically perfect tone, measured
// with exactly the parameters a render is. A long period under a fixed segment leaks part of
// its own mainlobe outside the +/-6% band, and that leakage grows steeply with the period — so
// a raw reading at period 2800 is not comparable with one at period 800, and neither is
// comparable with zero. The EXCESS over this floor is the honest "how much of this render is
// not the tone" number.
static double idealToneFloorPercent(double wantPeriod, std::size_t from, std::size_t len) {
std::vector<double> v(from + len + 2);
for (std::size_t i = 0; i < v.size(); ++i) {
v[i] = std::sin(2.0 * kPi * static_cast<double>(i) / wantPeriod);
}
return reasampler::test_support::energyOutsideFundamentalPercent(v, from, len, wantPeriod);
}
// A splice can only phase-align on a landing point a whole number of source periods away, and
// the search only reaches [0.75, 1.25] windows. periodAlignedJump is what puts an aligned point
// at the CENTRE of that interval instead of hoping one falls inside it.
static void testPeriodAlignedJumpSnapsToWholePeriodsWithinTheReachableBound() {
const std::int64_t w = 2205; // the product window at 44.1k
const std::int64_t maxJump = 2756; // 1.25 * w, the shifter's own jumpMax_
// Unknown period, and a period too long for even ONE whole one to fit: the fixed window,
// unchanged. Both are the documented fallback, and both must be EXACTLY today's geometry.
CHECK(periodAlignedJump(w, maxJump, 0.0) == w);
CHECK(periodAlignedJump(w, maxJump, -5.0) == w);
CHECK(periodAlignedJump(w, maxJump, 3000.0) == w);
// 30 Hz at 44.1k (P = 1470): two periods overshoot the bound, so it takes ONE — which is
// the case the whole track exists for. The pre-PSOLA geometry could reach neither 1470 nor
// 2940 from a 2205 nominal, since the search only spans [1654, 2756].
CHECK(periodAlignedJump(w, maxJump, 1470.0) == 1470);
CHECK(2 * 1470 > maxJump); // the witness that one period is forced, not merely chosen
// 220 Hz (P = 200.4545): eleven periods land within a frame of the window itself, so the
// splice cadence is essentially untouched while every landing is aligned.
CHECK(periodAlignedJump(w, maxJump, 44100.0 / 220.0) == 2205);
// A period just under the bound is taken whole; the result is never over the bound, at any
// period in the band. Sweeping is what proves the shrink loop terminates correctly rather
// than one hand-picked value doing so.
for (double p = 20.0; p < 3200.0; p += 0.37) {
const std::int64_t j = periodAlignedJump(w, maxJump, p);
CHECK(j >= 1);
if (p > static_cast<double>(maxJump)) {
CHECK(j == w); // out of reach -> fallback
} else {
CHECK(j <= maxJump);
// Aligned: the jump is a whole number of periods, to within the rounding to frames.
const double n = static_cast<double>(j) / p;
CHECK(std::fabs(n - std::floor(n + 0.5)) * p < 0.51);
}
}
}
// setSourcePeriod(0) and never calling it are the same state, not merely similar ones — the
// cheap half of the fallback claim. The EXPENSIVE half, that the fallback still matches the
// engine as it shipped, is testPreserveUnityRateIsBitIdenticalToTheShippedRead in
// test_sampler_core.cpp: it hashes four rendered streams (including transposed ones that
// really splice) against a baseline captured from commit 0a7778b, and it passes unmodified.
static void testAnUnknownPeriodIsBitIdenticalToTheFixedWindowGeometry() {
const std::int64_t w = 2205;
const std::vector<AudioSample> src = sine(400000, 400000.0 / 196.37);
const std::vector<double> never = runStretch(src, w, 1.0, 1.5, 40000, nullptr);
const std::vector<double> zeroed = runStretch(src, w, 1.0, 1.5, 40000, nullptr, 0.0);
bool same = true;
for (std::size_t i = 0; i < never.size(); ++i) if (never[i] != zeroed[i]) same = false;
CHECK(same);
}
// THE case this track exists for. 30 Hz sits in the only unalignable gap above 16 Hz at the
// product's 50 ms window: its nearest whole multiple misses the reachable interval by 184
// frames (45 degrees of phase), and the investigation measured the resulting sidebands at
// 3.57% out-of-band at +2 st / rate 1.0 and 15.45% at rate 2.0, against 0.00% on alignable
// controls. Here the same two conditions run with and without the source period, against a
// 34 Hz control that was alignable all along.
//
// The absolute numbers are NOT the harness's: a 1470-frame period under a 32768-frame segment
// leaks part of its own mainlobe outside the +/-6% band, so every reading here carries the same
// floor. That is exactly why the control is measured at the same length — the assertion is that
// 30 Hz reaches the control's floor, not that it reaches zero.
static void testThirtyHertzSplicesAlignOnceTheSourcePeriodIsKnown() {
using reasampler::test_support::energyOutsideFundamentalPercent;
const std::int64_t w = 2205;
const std::size_t srcLen = 400000, outFrames = 60000, from = 20000, len = 32768;
struct Row { const char* label; double freq; double rate; double semis; };
const Row rows[] = {
{"30 Hz +2 st, rate 1.0", 30.0, 1.0, 2.0},
{"30 Hz rate 2.0", 30.0, 2.0, 0.0},
{"34 Hz +2 st, rate 1.0", 34.0, 1.0, 2.0}, // control: alignable without a period
{"34 Hz rate 2.0", 34.0, 2.0, 0.0},
};
double controlWorst = 0.0, subjectWorst = 0.0;
for (const Row& r : rows) {
const double period = 44100.0 / r.freq;
std::vector<AudioSample> src(srcLen);
for (std::size_t i = 0; i < srcLen; ++i) {
src[i] = static_cast<AudioSample>(
std::sin(2.0 * kPi * static_cast<double>(i) / period));
}
const double shift = std::pow(2.0, r.semis / 12.0);
const double want = period / shift;
const std::vector<double> off = runStretch(src, w, r.rate, shift, outFrames, nullptr);
const std::vector<double> on =
runStretch(src, w, r.rate, shift, outFrames, nullptr, period);
for (double v : on) CHECK(std::isfinite(v));
const double floor = idealToneFloorPercent(want, from, len);
const double pctOff = energyOutsideFundamentalPercent(off, from, len, want) - floor;
const double pctOn = energyOutsideFundamentalPercent(on, from, len, want) - floor;
std::printf(" [30 Hz] %-24s (want %6.1f fr, metric floor %.2f%%) excess energy: "
"fixed window %6.2f%% -> pitch-synchronous %6.2f%%\n", r.label, want, floor,
pctOff, pctOn);
if (r.freq == 34.0) controlWorst = std::max(controlWorst, pctOn);
else subjectWorst = std::max(subjectWorst, pctOn);
}
// 30 Hz stops being a special case: with the period known its excess over the metric's own
// floor is no worse than the alignable neighbour's, measured identically. Against the
// control rather than against a fixed number, so the assertion cannot be satisfied by a
// change that merely raised the floor everywhere.
std::printf(" [30 Hz] worst subject excess %.2f%% vs worst control excess %.2f%%\n",
subjectWorst, controlWorst);
CHECK(subjectWorst < 0.10);
CHECK(subjectWorst <= controlWorst + 0.05); // 0.05 absorbs the floor subtraction's sign noise
}
// The sharpest single symptom of the geometry: at 29 Hz the nearest multiple misses the
// reachable interval ONE-SIDED rather than straddling, so the per-splice phase steps stop
// cancelling and accumulate into a real detune — the investigation measured -133 cents at
// rate 2.0 with NO transposition at all. Rate moves duration; it must not move pitch.
static void testTwentyNineHertzAtRateTwoKeepsItsPitch() {
using reasampler::test_support::autocorrelationPeriod;
const std::int64_t w = 2205;
const double period = 44100.0 / 29.0; // 1520.7 frames
const std::size_t srcLen = 400000;
std::vector<AudioSample> src(srcLen);
for (std::size_t i = 0; i < srcLen; ++i) {
src[i] = static_cast<AudioSample>(std::sin(2.0 * kPi * static_cast<double>(i) / period));
}
auto centsOf = [&](double sourcePeriod) {
const std::vector<double> out =
runStretch(src, w, /*rate=*/2.0, /*shift=*/1.0, 60000, nullptr, sourcePeriod);
const double got = autocorrelationPeriod(out, 20000, 20000,
static_cast<std::int64_t>(period * 0.5),
static_cast<std::int64_t>(period * 1.7));
return 1200.0 * std::log2(got / period);
};
const double centsOff = centsOf(0.0);
const double centsOn = centsOf(period);
std::printf(" [29 Hz] rate 2.0, no transposition: fixed window %+.1f cents -> "
"pitch-synchronous %+.1f cents\n", centsOff, centsOn);
CHECK(std::fabs(centsOn) < 10.0);
// The fixed-window reading is asserted too, and that is what makes the pair non-vacuous: a
// setSourcePeriod that silently did nothing would render both identically and fail here.
CHECK(std::fabs(centsOff) > 50.0);
}
// The cadence corner (rate 2.0, -24 st, source periods above the 315-frame safe floor) is the
// OTHER mechanism — a splice landing inside a single perceived cycle. Measured against the
// metric's own floor, PSOLA moves it by nothing: 0.23/0.51/0.30% excess becomes 0.24/0.49/0.30%.
//
// That is not a shortfall, it is what the corner turned out to be. Correcting the previous
// test's reading (see its comment) shrank the corner from a 7-21% headline to a sub-1% excess,
// which leaves PSOLA nothing to recover there — a pitch-synchronous jump makes each splice
// land in phase, and these splices already did; what it cannot do is make them less frequent.
// So this asserts NO REGRESSION, not an improvement, and says so rather than claiming one.
static void testCadenceCornerIsUnmovedByAPitchSynchronousSplice() {
using reasampler::test_support::energyOutsideFundamentalPercent;
const std::int64_t w = 2205;
const double shift = std::pow(2.0, -24.0 / 12.0);
const std::size_t outFrames = 60000, from = 20000, len = 32768;
for (double period : {500.0, 600.0, 700.0}) {
const std::size_t srcLen = 400000;
std::vector<AudioSample> src(srcLen);
for (std::size_t i = 0; i < srcLen; ++i) {
src[i] = static_cast<AudioSample>(
std::sin(2.0 * kPi * static_cast<double>(i) / period));
}
const std::vector<double> off = runStretch(src, w, 2.0, shift, outFrames, nullptr);
const std::vector<double> on =
runStretch(src, w, 2.0, shift, outFrames, nullptr, period);
for (double v : on) CHECK(std::isfinite(v));
const double want = period / shift;
const double floor = idealToneFloorPercent(want, from, len);
const double pctOff = energyOutsideFundamentalPercent(off, from, len, want);
const double pctOn = energyOutsideFundamentalPercent(on, from, len, want);
std::printf(" [cadence corner, PSOLA] period %.0f (want %.0f, metric floor %.2f%%): "
"excess %.2f%% -> %.2f%%\n", period, want, floor, pctOff - floor,
pctOn - floor);
CHECK(pctOn - floor < 1.0); // the corner's real excess, PSOLA or not
CHECK(pctOn < pctOff + 0.05); // and PSOLA costs it nothing
}
}
// The two new entry points on a shifter that was never configured (a Varispeed voice's) —
// neither may touch the empty ring.
static void testStretchEntryPointsOnPassThrough() {
@@ -826,6 +1041,11 @@ int main() {
testStretchMovesDurationNotPitch();
testStretchAndShiftComposeSafely();
testStretchCadenceCornerArtifactEnergyAtRate2ShiftQuarter();
testPeriodAlignedJumpSnapsToWholePeriodsWithinTheReachableBound();
testAnUnknownPeriodIsBitIdenticalToTheFixedWindowGeometry();
testThirtyHertzSplicesAlignOnceTheSourcePeriodIsKnown();
testTwentyNineHertzAtRateTwoKeepsItsPitch();
testCadenceCornerIsUnmovedByAPitchSynchronousSplice();
testStretchEntryPointsOnPassThrough();
if (g_fail == 0) {