Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion tests/bare_metal_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.*";
Expand Down
118 changes: 118 additions & 0 deletions tests/test_asrc_quality_16k.cpp
Original file line number Diff line number Diff line change
@@ -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 <cmath>
#include <cstdio>
#include <numbers>
#include <vector>

#include <gtest/gtest.h>

#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<float>(kAmp *
std::sin(2.0 * std::numbers::pi * nuIn * static_cast<double>(i)));
};
std::vector<float> 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
Loading