Γ-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:
+228
-8
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user