abace156a5
Zero-crossing counting was anti-correlated with the real defect (splice debris fools it). Now asserts energy outside the fundamental, with an alignable control, matching test_preserve_low_frequency.cpp's approach.
51 lines
2.5 KiB
C++
51 lines
2.5 KiB
C++
#pragma once
|
|
// Out-of-band spectral energy metric: the same period-grid, Hann-windowed direct-evaluation
|
|
// approach as test_preserve_low_frequency.cpp's reportSpectrum. Chosen over zero-crossing
|
|
// counting because splice debris adds spurious crossings that make that estimator
|
|
// anti-correlated with severity (a render can read a badly wrong PERIOD while this metric
|
|
// shows it is mostly clean, or vice versa). Grid/segment sizes are smaller than the hand-run
|
|
// harness's — this one runs inside the gated suite.
|
|
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#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). 0 = a clean single tone at that period; higher values mean
|
|
// harmonics, splice-cadence sidebands, or crossfade cancellation debris are present.
|
|
inline double energyOutsideFundamentalPercent(const std::vector<double>& v, std::size_t from,
|
|
std::size_t len, double wantPeriod) {
|
|
constexpr double kPi = 3.14159265358979323846;
|
|
constexpr int kGrid = 400;
|
|
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;
|
|
}
|
|
|
|
} // namespace reasampler::test_support
|