From 5fb6867cd8396e3e031aa74f561e0383e4153da2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 17:13:58 +0000 Subject: [PATCH] Add measured 16 kHz quality suite (AsrcQuality16k) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New tests/test_asrc_quality_16k.cpp: 16 kHz -> 16 kHz, +200 ppm, sample-granular transfer, last 1 s analyzed with fitSineTracked — the test_asrc_quality.cpp methodology with rate-scaled configuration, applied test-side only (no header changes): - FilterSpec band edges scaled by 16/48 (balanced-at-16k: passband ~6666.7 Hz, stopband ~9333.3 Hz, L=256, T=48, 120 dB). - ServoConfig bandwidths scaled by 16/48 as well: they are absolute Hz, and with the defaults the 3.2 Hz slip beat (vs 9.6 Hz at 48 k) loses (16/48)^3 ~ 28.6 dB of 3-pole smoother rejection, leaving the measurement servo-FM-limited ~32 dB below the 48 k figures. Scaling keeps the loop identical in normalized terms. - 120 s run: the same sample count and loop time constants as the 48 k test's 40 s, since the quiet loop is now ~0.017 Hz. Measured (host, Release), thresholds pinned ~4 dB under: tone measured threshold 48 k counterpart (same f/fs) 333 Hz 136.6 dB 132 dB 997 Hz: 135.0 dB 2 kHz 121.9 dB 117 dB 6 kHz: 120.0 dB 4 kHz 114.3 dB 110 dB 12 kHz: 112.8 dB 6.5 kHz 106.5 dB 102 dB 19.5 kHz: 105.8 dB Emulated-target exclusion: the hexagon job's ctest -E 'AsrcQuality|...' already matches AsrcQuality16k (unanchored regex), but the bare-metal gtest filter "-AsrcQuality.*" did not ('.' is literal in gtest filters), so it is widened to "-AsrcQuality*". README: short 16 kHz note in Measured performance (band-edge/servo scaling, measured numbers, group delay in ms triples at the same taps). https://claude.ai/code/session_01HuAFfoeD5a5Xe5aGNA16M9 --- README.md | 11 +++ tests/CMakeLists.txt | 1 + tests/bare_metal_main.cpp | 4 +- tests/test_asrc_quality_16k.cpp | 118 ++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 tests/test_asrc_quality_16k.cpp diff --git a/README.md b/README.md index b24046f..ce3a432 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,17 @@ adjacent phase-table rows (≈ −12 dB per doubling of `L`, +12 dB per octave o signal frequency). Servo lock from a cold start takes ~1 s; a 0 → 300 ppm drift ramp at 10 ppm/s is tracked without unlocking. +The same structure holds at other deployment rates, e.g. 16 kHz for +reference-microphone processing — but `FilterSpec` band edges and +`ServoConfig` bandwidths are absolute Hz (the defaults assume ~48 kHz), so +scale both with the rate (balanced at 16 kHz: passband ≈ 6.67 kHz, stopband +≈ 9.33 kHz, servo bandwidths × 16/48). Measured that way +(`tests/test_asrc_quality_16k.cpp`), 16 kHz matches the 48 kHz +normalized-frequency structure: 136.6 dB at 333 Hz and 106.5 dB at 6.5 kHz, +within ~1 dB of the 48 kHz tones at the same f/fs. Interpolation noise +depends only on f/fs; group delay at the same tap count stays ~24 input +samples and therefore triples in milliseconds (1.5 ms vs 0.5 ms). + ## Platform support CI builds and tests every push on: diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1fc21ec..999e5f2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -35,6 +35,7 @@ add_executable(srt_tests test_servo.cpp test_asrc_lock.cpp test_asrc_quality.cpp + test_asrc_quality_16k.cpp test_fade.cpp test_latency.cpp test_multichannel.cpp) diff --git a/tests/bare_metal_main.cpp b/tests/bare_metal_main.cpp index abf1086..fd909db 100644 --- a/tests/bare_metal_main.cpp +++ b/tests/bare_metal_main.cpp @@ -13,7 +13,9 @@ int main() { // MultiChannelShort.* stays in: it is the only on-target coverage of the // N-channel deinterleave and the wide-MAC dotRow paths at N > 2. - ::testing::GTEST_FLAG(filter) = "-AsrcQuality.*:AsrcLock.*:Servo.*:Kaiser.*MeetsSpec:" + // "AsrcQuality*" (no dot) excludes every quality suite: in gtest filters + // '.' is a literal, so "AsrcQuality.*" would not cover AsrcQuality16k. + ::testing::GTEST_FLAG(filter) = "-AsrcQuality*:AsrcLock.*:Servo.*:Kaiser.*MeetsSpec:" "FixedPoint.AsrcQuality*:" "FixedPoint.FullScaleSineDoesNotWrapQ15:" "MultiChannel.*"; diff --git a/tests/test_asrc_quality_16k.cpp b/tests/test_asrc_quality_16k.cpp new file mode 100644 index 0000000..8a2fdb4 --- /dev/null +++ b/tests/test_asrc_quality_16k.cpp @@ -0,0 +1,118 @@ +// 16 kHz -> 16 kHz quality coverage (a real deployment rate, e.g. +// reference-microphone processing). Same methodology as +// test_asrc_quality.cpp, with two rate-specific twists, both applied +// test-side (the header presets and defaults are deliberately untouched): +// +// 1. FilterSpec band edges are absolute Hz and the presets assume ~48 kHz, +// so passbandHz/stopbandHz are scaled by 16/48. +// 2. ServoConfig bandwidths are absolute Hz too. The slip-sawtooth beat +// sits at ppm * fs = 3.2 Hz instead of 9.6 Hz, so with default servo +// settings the 3-pole quiet smoother rejects it (16/48)^3 ~ 28.6 dB +// less and the measurement becomes servo-FM-limited: measured ~32 dB +// below the 48 kHz figures at every tone, falling 6 dB/octave of +// signal frequency (the small-index FM sideband signature). Scaling +// the servo bandwidths by 16/48 keeps the loop identical in +// normalized (per-sample) terms and restores the 48 kHz structure. +#include +#include +#include +#include + +#include + +#include "srt/asrc.hpp" +#include "support/sine_analysis.hpp" +#include "support/two_clock_sim.hpp" + +namespace { + +constexpr double kFs = 16000.0; +constexpr double kEps = 200e-6; +constexpr double kAmp = 0.5; + +// balanced() with band edges scaled to 16 kHz: identical L/T (so the same +// normalized-frequency response and the same group delay in samples — which +// is 3x the milliseconds at one third the rate). +srt::FilterSpec balancedAt16k() { + return {.numPhases = 256, + .tapsPerPhase = 48, + .passbandHz = 20000.0 * 16.0 / 48.0, // ~6666.7 Hz + .stopbandHz = 28000.0 * 16.0 / 48.0, // ~9333.3 Hz + .stopbandAttenDb = 120.0}; +} + +// Resamples a sine across a +200 ppm clock offset (sample-synchronous +// transfer) and measures the residual after removing the fitted fundamental +// from the last second of output. Mirrors measureSnrDb in +// test_asrc_quality.cpp at fs = 16 kHz. +double measureSnrDb16k(const srt::FilterSpec& spec, double freqHz) { + srt::Config cfg; + cfg.sampleRateHz = kFs; + cfg.channels = 1; + cfg.filter = spec; + // Servo scaled with the rate (see file comment): same normalized loop + // as the 48 kHz defaults. + constexpr double r = 16.0 / 48.0; + cfg.servo.acquireBandwidthHz *= r; + cfg.servo.trackBandwidthHz *= r; + cfg.servo.quietBandwidthHz *= r; + cfg.servo.acquireSmootherHz *= r; + cfg.servo.trackSmootherHz *= r; + cfg.servo.quietSmootherHz *= r; + srt::AsyncSampleRateConverter asrc(cfg); + srt_test::TwoClockSim sim{.asrc = asrc, + .fsIn = kFs * (1.0 + kEps), + .fsOut = kFs, + .channels = 1, + .chunkIn = 1, + .chunkOut = 1}; + const double nuIn = freqHz / kFs; + sim.gen = [&](std::uint64_t i) { + return static_cast(kAmp * + std::sin(2.0 * std::numbers::pi * nuIn * static_cast(i))); + }; + std::vector tail; + tail.reserve(16000); + // Long run: the locked loop must fully forget the acquisition transient + // before the measurement window. The quiet loop is scaled to ~0.017 Hz, + // so the 48 kHz test's 40 s becomes 120 s here — the identical number + // of samples and of loop time constants (a 40 s run still sits ~15 dB + // above the settled residual at every tone). + const double total = 120.0; + sim.run(total, [&](const float* x, std::size_t frames, double t) { + if (t >= total - 1.0) + tail.insert(tail.end(), x, x + frames); + }); + EXPECT_EQ(asrc.status().underruns, 0u); + EXPECT_EQ(asrc.status().state, srt::State::Locked); + const double nuOutExpected = nuIn * (1.0 + kEps); + const auto fit = srt_test::fitSineTracked(tail, nuOutExpected); + EXPECT_NEAR(fit.amplitude, kAmp, 0.01); + // The tracked frequency must still match the true clock ratio closely. + EXPECT_NEAR(fit.freqNorm / nuOutExpected, 1.0, 2e-6); + const double snr = srt_test::snrDb(fit); + std::printf("[ measured ] %5.0f Hz, %zu phases: SNR %.1f dB\n", freqHz, spec.numPhases, snr); + return snr; +} + +// Thresholds sit ~4 dB under measured performance, the convention of +// test_asrc_quality.cpp. Measured (balanced-at-16k, +200 ppm): +// 333 Hz: 136.6 dB, 2 kHz: 121.9 dB, 4 kHz: 114.3 dB, 6.5 kHz: 106.5 dB. +// The interpolation residual depends on the normalized frequency f/fs and +// the tones sit at the same f/fs as the 48 kHz suite's 997 Hz/6 k/12 k/ +// 19.5 k, which measure 135.0/120.0/112.8/105.8 dB on the same host — +// matching within ~1 dB, as expected. +TEST(AsrcQuality16k, Balanced333Hz) { + EXPECT_GT(measureSnrDb16k(balancedAt16k(), 333.0), 132.0); +} +TEST(AsrcQuality16k, Balanced2kHz) { + EXPECT_GT(measureSnrDb16k(balancedAt16k(), 2000.0), 117.0); +} +TEST(AsrcQuality16k, Balanced4kHz) { + EXPECT_GT(measureSnrDb16k(balancedAt16k(), 4000.0), 110.0); +} +TEST(AsrcQuality16k, Balanced6_5kHz) { + EXPECT_GT(measureSnrDb16k(balancedAt16k(), 6500.0), 102.0); +} + +} // namespace