Fix cross-rate Preserve splices, re-stamp on host-rate change, name the rate refusal
The splice crossfade now counts output frames and stops before the ring's far end (this also fixes a lap at matched rate). The search strides by the rate ratio. The editor takes its time base from its own decode. Same-rate output is pinned to hashes from before the change.
This commit is contained in:
+80
-18
@@ -748,6 +748,65 @@ static void testStretchAndShiftComposeSafely() {
|
||||
}
|
||||
}
|
||||
|
||||
// A down-splice's crossfade walks the OUTGOING tap away from the writer at (feedRate - shift)
|
||||
// source frames per output frame, toward the ring's far end — a quarter window past the
|
||||
// trigger. The live fade must end before that walk laps the ring, both at a matched rate (a fast
|
||||
// feed under a down-shift) and at a high source ratio, where the fade is counted in output
|
||||
// frames while the tap moves in source frames. The first case per ratio is also checked for
|
||||
// its nominal length: window/4 source frames' worth of output time.
|
||||
static void testDownSpliceFadeNeverLapsTheRingsFarEnd() {
|
||||
const std::int64_t w = 2205;
|
||||
const double ringLen = 2.0 * static_cast<double>(w);
|
||||
const double dHigh = ringLen - static_cast<double>(w / 4);
|
||||
struct Case { double feed, shift, sourceRatio; std::int64_t nominalFade; };
|
||||
const Case cases[] = {
|
||||
{1.0, 0.5, 1.0, w / 4}, // -12 st, matched: room to spare, the full fade
|
||||
{2.0, 1.0, 1.0, -1}, // Rate 200 %, no shift: recedes one frame per frame
|
||||
{2.0, 0.25, 1.0, -1}, // Rate 200 %, -24 st
|
||||
{4.0, 2.0, 4.0, 138}, // -12 st at a 4x source ratio: round(551 / 4)
|
||||
{4.0, 1.0, 4.0, -1}, // -24 st at a 4x source ratio
|
||||
};
|
||||
std::vector<AudioSample> src(400000);
|
||||
for (std::size_t i = 0; i < src.size(); ++i) {
|
||||
src[i] = static_cast<AudioSample>(std::sin(2.0 * kPi * static_cast<double>(i) / 196.37));
|
||||
}
|
||||
for (const Case& c : cases) {
|
||||
PitchShifter ps;
|
||||
ps.configure(w);
|
||||
ps.prime(src.data(), w);
|
||||
ps.setShiftRatio(c.shift);
|
||||
ps.setFeedRate(c.feed);
|
||||
ps.setSourceRateRatio(c.sourceRatio);
|
||||
std::size_t pos = static_cast<std::size_t>(w);
|
||||
double debt = 0.0;
|
||||
SpliceEvent first;
|
||||
for (int i = 0; i < 40000 && !first.fired; ++i) {
|
||||
debt += c.feed;
|
||||
const int due = static_cast<int>(debt);
|
||||
debt -= static_cast<double>(due);
|
||||
for (int k = 0; k + 1 < due; ++k) ps.writeFrame(src[pos++]);
|
||||
if (due > 0) ps.process(src[pos++]);
|
||||
else ps.processNoInput();
|
||||
first = ps.lastSplice();
|
||||
}
|
||||
CHECK(first.fired);
|
||||
CHECK(first.jump < 0); // a DOWN-splice: the tap jumped toward the writer
|
||||
const double recede = c.feed - c.shift;
|
||||
// Worst case: the trigger overshot dHigh by one frame's drift, and the integer feed runs
|
||||
// up to one frame ahead of its average. One frame short of the ring is the interpolator's
|
||||
// read-ahead.
|
||||
const double worstDelay =
|
||||
dHigh + recede + recede * static_cast<double>(first.fadeLen) + 1.0;
|
||||
if (!(worstDelay < ringLen)) {
|
||||
std::printf(" feed %.2f shift %.2f ratio %.1f: fade %lld reaches delay %.1f of %.0f\n",
|
||||
c.feed, c.shift, c.sourceRatio, static_cast<long long>(first.fadeLen),
|
||||
worstDelay, ringLen);
|
||||
}
|
||||
CHECK(worstDelay < ringLen);
|
||||
if (c.nominalFade > 0) CHECK(first.fadeLen == c.nominalFade);
|
||||
}
|
||||
}
|
||||
|
||||
// The [0.5, 2.0] rate bound (time_stretch.h) narrows the splice-cadence failure onto the
|
||||
// source fundamental rather than eliminating it. At rate 2.0, shift 0.25 (-24 st) — both
|
||||
// inside the shipped range — the header's own derivation puts the safe-source floor at a
|
||||
@@ -757,18 +816,18 @@ static void testStretchAndShiftComposeSafely() {
|
||||
// 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 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/shift/source, this file's own metric parameters) at 7.08% / 13.90% / 20.92% for
|
||||
// P=500/600/700, against 0.04% 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.
|
||||
// They are 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 EXCESS over that floor once measured 0.23% / 0.51% / 0.30% (control 0.10%); that
|
||||
// residue was the down-shift crossfade (recede 1.75 frames/frame) lapping the ring's far end,
|
||||
// and splice()'s far-end fade cap took it to 0.00%. 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
|
||||
@@ -782,8 +841,8 @@ static void testStretchCadenceCornerArtifactEnergyAtRate2ShiftQuarter() {
|
||||
const std::size_t from = 20000, len = 32768;
|
||||
|
||||
// Below the safe floor (P > 315 frames): per the header's CORRECTED note, this raw reading
|
||||
// is mostly the metric's own mainlobe-leakage floor, not cadence damage (the real excess is
|
||||
// ~0.2-0.5%, asserted in testCadenceCornerIsUnmovedByAPitchSynchronousSplice). The bounds
|
||||
// is the metric's own mainlobe-leakage floor, not cadence damage (the excess is asserted in
|
||||
// testCadenceCornerIsUnmovedByAPitchSynchronousSplice). The bounds
|
||||
// below are a RAW-READING STABILITY TRIPWIRE, not a discriminator — they catch a large swing
|
||||
// in the raw measurement; the ceiling (30%) is a generous margin above the highest measured
|
||||
// value, there to catch a much worse regression rather than to chase today's exact number.
|
||||
@@ -1001,13 +1060,15 @@ static void testTwentyNineHertzAtRateTwoKeepsItsPitch() {
|
||||
|
||||
// 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%.
|
||||
// metric's own floor, PSOLA moves it by nothing: 0.00% excess either way (it was 0.23/0.51/0.30%
|
||||
// both ways until the far-end fade cap removed the lapped crossfade behind it).
|
||||
//
|
||||
// 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.
|
||||
// test's reading (see its comment) shrank the corner from a 7-21% headline to no measurable
|
||||
// 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;
|
||||
@@ -1045,7 +1106,7 @@ static void testCadenceCornerIsUnmovedByAPitchSynchronousSplice() {
|
||||
//
|
||||
// MEASURED (Debug, this machine): metric floor 55.86% (P=1470's want-period of 5880 fr under a
|
||||
// 32768-frame segment leaks a lot of mainlobe, same effect as the P=500-700 rows, just larger),
|
||||
// fixed-window excess 18.52%, pitch-synchronous excess 0.00%. The faster cadence does NOT
|
||||
// fixed-window excess 18.61%, pitch-synchronous excess 0.00%. The faster cadence does NOT
|
||||
// regress this corner — every splice at n=1 lands exactly one source period away, so despite
|
||||
// firing 1.5x as often each one is phase-perfect rather than merely aligned-on-average, and the
|
||||
// corner clears rather than worsens. Recorded as read, not tuned: if a future change moves
|
||||
@@ -1073,7 +1134,7 @@ static void testCadenceCollapseBandAtThirtyHertzUnderPitchSynchronousSplice() {
|
||||
"excess %.2f%% -> %.2f%%\n", period, want, floor, pctOff - floor, pctOn - floor);
|
||||
CHECK(pctOn - floor < 1.0); // measured 0.00%: phase-perfect at n=1, not merely aligned
|
||||
// Vacuity guard (shape of testThirtyHertzSplicesAlignOnceTheSourcePeriodIsKnown's :961): the
|
||||
// FIXED-WINDOW arm is asserted too (measured 18.52% excess), so a setSourcePeriod that
|
||||
// FIXED-WINDOW arm is asserted too (measured 18.61% excess), so a setSourcePeriod that
|
||||
// silently did nothing would render both arms identically and pass the bound above by luck.
|
||||
CHECK(pctOff - floor > pctOn - floor + 1.0);
|
||||
}
|
||||
@@ -1099,6 +1160,7 @@ int main() {
|
||||
testStereoLinkedLagSharedSchedule();
|
||||
testStretchMovesDurationNotPitch();
|
||||
testStretchAndShiftComposeSafely();
|
||||
testDownSpliceFadeNeverLapsTheRingsFarEnd();
|
||||
testStretchCadenceCornerArtifactEnergyAtRate2ShiftQuarter();
|
||||
testPeriodAlignedJumpSnapsToWholePeriodsWithinTheReachableBound();
|
||||
testAnUnknownPeriodIsBitIdenticalToTheFixedWindowGeometry();
|
||||
|
||||
Reference in New Issue
Block a user