Γ-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:
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
// The two tone metrics the Preserve tests and the hand-run low-frequency harness share, so
|
||||
// there is one of each rather than a copy per file.
|
||||
//
|
||||
// Zero-crossing counting is deliberately NOT among them: splice debris adds spurious crossings
|
||||
// that make that estimator anti-correlated with severity (a render can read a badly wrong
|
||||
// PERIOD while it is spectrally clean, or vice versa).
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace reasampler::test_support {
|
||||
|
||||
// Percentage (0..100) of the segment [from, from+len)'s spectral energy that falls outside
|
||||
// +/- 6% of `wantPeriod` (frames), evaluated directly on a geometric period grid (no FFT-bin
|
||||
// quantization). 0 = a clean single tone at that period; higher values mean harmonics,
|
||||
// splice-cadence sidebands, or crossfade cancellation debris are present.
|
||||
//
|
||||
// `grid` trades resolution for cost: the gated tests run the default, the hand-run harness
|
||||
// raises it. The value is NOT comparable across grid sizes or segment lengths — a long period
|
||||
// under a short segment leaks part of its own mainlobe outside the +/-6% band, so a reading is
|
||||
// only meaningful against a control measured at the SAME len and grid.
|
||||
inline double energyOutsideFundamentalPercent(const std::vector<double>& v, std::size_t from,
|
||||
std::size_t len, double wantPeriod,
|
||||
int grid = 400) {
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
const int kGrid = grid;
|
||||
const double pLo = 30.0, pHi = 8000.0;
|
||||
std::vector<double> mag(static_cast<std::size_t>(kGrid));
|
||||
std::vector<double> per(static_cast<std::size_t>(kGrid));
|
||||
for (int g = 0; g < kGrid; ++g) {
|
||||
// Geometric grid: constant relative resolution across the swept period range.
|
||||
const double p = pLo * std::pow(pHi / pLo, static_cast<double>(g) / (kGrid - 1));
|
||||
per[static_cast<std::size_t>(g)] = p;
|
||||
double re = 0.0, im = 0.0;
|
||||
const double w = 2.0 * kPi / p;
|
||||
for (std::size_t k = 0; k < len && from + k < v.size(); ++k) {
|
||||
const double hann = 0.5 * (1.0 - std::cos(2.0 * kPi * static_cast<double>(k) /
|
||||
static_cast<double>(len)));
|
||||
const double x = v[from + k] * hann;
|
||||
re += x * std::cos(w * static_cast<double>(k));
|
||||
im += x * std::sin(w * static_cast<double>(k));
|
||||
}
|
||||
mag[static_cast<std::size_t>(g)] = std::sqrt(re * re + im * im);
|
||||
}
|
||||
double eTotal = 0.0, eFund = 0.0;
|
||||
for (int g = 0; g < kGrid; ++g) {
|
||||
const std::size_t i = static_cast<std::size_t>(g);
|
||||
const double e = mag[i] * mag[i];
|
||||
eTotal += e;
|
||||
if (std::fabs(per[i] - wantPeriod) / wantPeriod < 0.06) eFund += e;
|
||||
}
|
||||
return eTotal > 0.0 ? 100.0 * (1.0 - eFund / eTotal) : 0.0;
|
||||
}
|
||||
|
||||
// Period (frames) of the highest normalized-autocorrelation peak over [minLag, maxLag], with a
|
||||
// parabolic refinement so the answer is not quantized to whole frames. Bracket the caller's
|
||||
// range to roughly [0.5, 1.7] x the expected period: a pure tone autocorrelates equally at
|
||||
// EVERY multiple of its period, so an unbounded search reports 2P about half the time.
|
||||
inline double autocorrelationPeriod(const std::vector<double>& v, std::size_t from,
|
||||
std::size_t len, std::int64_t minLag, std::int64_t maxLag) {
|
||||
if (maxLag <= minLag) return 0.0;
|
||||
double e0 = 0.0;
|
||||
for (std::size_t k = 0; k < len && from + k < v.size(); ++k) e0 += v[from + k] * v[from + k];
|
||||
if (e0 <= 0.0) return 0.0;
|
||||
std::vector<double> score(static_cast<std::size_t>(maxLag - minLag + 1), 0.0);
|
||||
double best = -1e18;
|
||||
std::int64_t bestLag = minLag;
|
||||
for (std::int64_t lag = minLag; lag <= maxLag; ++lag) {
|
||||
double s = 0.0, e = 0.0;
|
||||
for (std::size_t k = 0; k < len && from + k + static_cast<std::size_t>(lag) < v.size();
|
||||
++k) {
|
||||
const double b = v[from + k + static_cast<std::size_t>(lag)];
|
||||
s += v[from + k] * b;
|
||||
e += b * b;
|
||||
}
|
||||
const double r = e > 0.0 ? s / std::sqrt(e0 * e) : 0.0;
|
||||
score[static_cast<std::size_t>(lag - minLag)] = r;
|
||||
if (r > best) { best = r; bestLag = lag; }
|
||||
}
|
||||
const std::size_t i = static_cast<std::size_t>(bestLag - minLag);
|
||||
double frac = 0.0;
|
||||
if (i > 0 && i + 1 < score.size()) {
|
||||
const double den = score[i - 1] - 2.0 * score[i] + score[i + 1];
|
||||
if (den < 0.0) frac = 0.5 * (score[i - 1] - score[i + 1]) / den;
|
||||
}
|
||||
return static_cast<double>(bestLag) + frac;
|
||||
}
|
||||
|
||||
} // namespace reasampler::test_support
|
||||
Reference in New Issue
Block a user