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
4 changes: 2 additions & 2 deletions .github/workflows/style.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ on: [push, pull_request]

jobs:
drift:
uses: tap/taphouse/.github/workflows/drift-check.yml@v3
uses: tap/taphouse/.github/workflows/drift-check.yml@v4
with:
ref: v3
ref: v4

clang-tidy:
runs-on: ubuntu-latest
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ target_link_libraries(app PRIVATE SampleRateTap::SampleRateTap)
```cpp
#include <srt/srt.h>

srt::config cfg;
tap::samplerate::config cfg;
cfg.sample_rate_hz = 48000.0;
cfg.channels = 2;
srt::async_sample_rate_converter asrc(cfg); // allocates + designs filter; may throw
tap::samplerate::async_sample_rate_converter asrc(cfg); // allocates + designs filter; may throw

// Input-device thread (input clock):
asrc.push(input_interleaved, frames); // noexcept, lock-free
Expand All @@ -46,7 +46,7 @@ asrc.push(input_interleaved, frames); // noexcept, lock-free
asrc.pull(output_interleaved, frames); // noexcept, lock-free; silence
// until filled/locked

srt::converter_status st = asrc.status(); // any thread: state, ppm, fill,
tap::samplerate::converter_status st = asrc.status(); // any thread: state, ppm, fill,
// underruns/overruns/resyncs
```

Expand Down Expand Up @@ -233,7 +233,7 @@ reference-microphone processing — but `filter_spec` band edges and
`servo_config` bandwidths are absolute Hz designed for ~48 kHz, and running
another rate with unscaled defaults silently costs quality (measured:
~32 dB at 16 kHz). Start any non-48 kHz deployment from
`srt::config::for_sample_rate(rate_hz)`, which rescales both (plus the servo
`tap::samplerate::config::for_sample_rate(rate_hz)`, which rescales both (plus the servo
hold times); `filter_spec::scaled_to` / `servo_config::scaled_to` exist for
custom presets. Measured through that factory
(`tests/test_asrc_quality_16k.cpp`), 16 kHz matches the 48 kHz
Expand Down Expand Up @@ -353,7 +353,7 @@ Indicative numbers from a shared machine (Intel(R) Xeon(R) Processor @ 2.80GHz,

## Sample types

The datapath is templated on the sample type via `srt::sample_traits`
The datapath is templated on the sample type via `tap::samplerate::sample_traits`
(`include/srt/sample_traits.h`). Three formats are provided:

| Type | Alias | Format | Measured SNR (997 Hz / 19.5 kHz, half scale, +200 ppm) |
Expand Down
52 changes: 26 additions & 26 deletions bench/bench_asrc.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Host benchmarks for the SampleRateTap hot path. Two layers:
// - Kernel: srt::interpolate() in isolation (one output sample, one
// - Kernel: tap::samplerate::interpolate() in isolation (one output sample, one
// channel) — the datapath's arithmetic floor.
// - Pipeline: steady-state push()+pull() through the full converter in
// 128-frame blocks, the realistic duplex cost per frame.
Expand All @@ -22,7 +22,7 @@ namespace {
if constexpr (std::is_floating_point_v<S>)
return static_cast<S>(v);
else
return srt::detail::round_sat<S>(v * static_cast<double>(std::numeric_limits<S>::max()));
return tap::samplerate::detail::round_sat<S>(v * static_cast<double>(std::numeric_limits<S>::max()));
}

template <typename S>
Expand All @@ -35,29 +35,29 @@ namespace {
}

template <typename S>
void kernelBench(benchmark::State& state, const srt::filter_spec& spec) {
const srt::polyphase_filter_bank<S> bank(spec, 48000.0);
const auto hist = sineBlock<S>(bank.taps(), 997.0, 0.5);
double mu = 0.0;
void kernelBench(benchmark::State& state, const tap::samplerate::filter_spec& spec) {
const tap::samplerate::polyphase_filter_bank<S> bank(spec, 48000.0);
const auto hist = sineBlock<S>(bank.taps(), 997.0, 0.5);
double mu = 0.0;
for (auto _ : state) {
mu += 0.6180339887498949; // golden-ratio stride visits phases evenly
if (mu >= 1.0)
mu -= 1.0;
benchmark::DoNotOptimize(srt::interpolate(bank, hist.data(), mu));
benchmark::DoNotOptimize(tap::samplerate::interpolate(bank, hist.data(), mu));
}
state.SetItemsProcessed(static_cast<std::int64_t>(state.iterations()));
}

template <typename S>
void pipelineBench(benchmark::State& state, const srt::filter_spec& spec, std::size_t channels) {
constexpr std::size_t kBlock = 128;
srt::config cfg;
void pipelineBench(benchmark::State& state, const tap::samplerate::filter_spec& spec, std::size_t channels) {
constexpr std::size_t kBlock = 128;
tap::samplerate::config cfg;
cfg.channels = channels;
cfg.filter = spec;
// The FIFO setpoint must exceed the pull block size (see README latency
// notes); 2 blocks gives headroom without distorting per-frame cost.
cfg.target_latency_frames = 2 * kBlock;
srt::basic_async_sample_rate_converter<S> asrc(cfg);
tap::samplerate::basic_async_sample_rate_converter<S> asrc(cfg);

// One second of pregenerated input so signal synthesis stays out of the
// measured region; consumed cyclically.
Expand Down Expand Up @@ -87,19 +87,19 @@ namespace {

// --- Kernel: type x preset ------------------------------------------------
void BM_Kernel_Float_Fast(benchmark::State& s) {
kernelBench<float>(s, srt::filter_spec::fast());
kernelBench<float>(s, tap::samplerate::filter_spec::fast());
}
void BM_Kernel_Float_Balanced(benchmark::State& s) {
kernelBench<float>(s, srt::filter_spec::balanced());
kernelBench<float>(s, tap::samplerate::filter_spec::balanced());
}
void BM_Kernel_Float_Transparent(benchmark::State& s) {
kernelBench<float>(s, srt::filter_spec::transparent());
kernelBench<float>(s, tap::samplerate::filter_spec::transparent());
}
void BM_Kernel_Q15_Balanced(benchmark::State& s) {
kernelBench<std::int16_t>(s, srt::filter_spec::balanced());
kernelBench<std::int16_t>(s, tap::samplerate::filter_spec::balanced());
}
void BM_Kernel_Q31_Balanced(benchmark::State& s) {
kernelBench<std::int32_t>(s, srt::filter_spec::balanced());
kernelBench<std::int32_t>(s, tap::samplerate::filter_spec::balanced());
}
BENCHMARK(BM_Kernel_Float_Fast);
BENCHMARK(BM_Kernel_Float_Balanced);
Expand All @@ -109,36 +109,36 @@ namespace {

// --- Pipeline: type x channels (balanced), plus the transparent ceiling ---
void BM_Pipeline_Float_Balanced_1ch(benchmark::State& s) {
pipelineBench<float>(s, srt::filter_spec::balanced(), 1);
pipelineBench<float>(s, tap::samplerate::filter_spec::balanced(), 1);
}
void BM_Pipeline_Float_Balanced_2ch(benchmark::State& s) {
pipelineBench<float>(s, srt::filter_spec::balanced(), 2);
pipelineBench<float>(s, tap::samplerate::filter_spec::balanced(), 2);
}
void BM_Pipeline_Float_Balanced_8ch(benchmark::State& s) {
pipelineBench<float>(s, srt::filter_spec::balanced(), 8);
pipelineBench<float>(s, tap::samplerate::filter_spec::balanced(), 8);
}
void BM_Pipeline_Q15_Balanced_2ch(benchmark::State& s) {
pipelineBench<std::int16_t>(s, srt::filter_spec::balanced(), 2);
pipelineBench<std::int16_t>(s, tap::samplerate::filter_spec::balanced(), 2);
}
void BM_Pipeline_Q31_Balanced_2ch(benchmark::State& s) {
pipelineBench<std::int32_t>(s, srt::filter_spec::balanced(), 2);
pipelineBench<std::int32_t>(s, tap::samplerate::filter_spec::balanced(), 2);
}
void BM_Pipeline_Float_Transparent_2ch(benchmark::State& s) {
pipelineBench<float>(s, srt::filter_spec::transparent(), 2);
pipelineBench<float>(s, tap::samplerate::filter_spec::transparent(), 2);
}
// Deployment shapes: 12 channels (7.1.4 surround), 16 (AVB stream bundling
// reference microphones with the program feed).
void BM_Pipeline_Float_Balanced_12ch(benchmark::State& s) {
pipelineBench<float>(s, srt::filter_spec::balanced(), 12);
pipelineBench<float>(s, tap::samplerate::filter_spec::balanced(), 12);
}
void BM_Pipeline_Q15_Balanced_12ch(benchmark::State& s) {
pipelineBench<std::int16_t>(s, srt::filter_spec::balanced(), 12);
pipelineBench<std::int16_t>(s, tap::samplerate::filter_spec::balanced(), 12);
}
void BM_Pipeline_Float_Balanced_16ch(benchmark::State& s) {
pipelineBench<float>(s, srt::filter_spec::balanced(), 16);
pipelineBench<float>(s, tap::samplerate::filter_spec::balanced(), 16);
}
void BM_Pipeline_Q15_Balanced_16ch(benchmark::State& s) {
pipelineBench<std::int16_t>(s, srt::filter_spec::balanced(), 16);
pipelineBench<std::int16_t>(s, tap::samplerate::filter_spec::balanced(), 16);
}
BENCHMARK(BM_Pipeline_Float_Balanced_1ch);
BENCHMARK(BM_Pipeline_Float_Balanced_2ch);
Expand Down
22 changes: 11 additions & 11 deletions bench/compare/bench_compare.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ namespace {
};

template <typename S>
void srtBench(benchmark::State& state, const srt::filter_spec& spec, std::size_t channels) {
const srt::polyphase_filter_bank<S> bank(spec, 48000.0);
srt::fractional_resampler<S> rs(bank, channels);
InputTap inFloat(48000, channels);
void srtBench(benchmark::State& state, const tap::samplerate::filter_spec& spec, std::size_t channels) {
const tap::samplerate::polyphase_filter_bank<S> bank(spec, 48000.0);
tap::samplerate::fractional_resampler<S> rs(bank, channels);
InputTap inFloat(48000, channels);
// Requantize the shared float source once at setup for fixed-point runs.
std::vector<S> buf(48000 * channels);
{
Expand All @@ -86,8 +86,8 @@ namespace {
if constexpr (std::is_floating_point_v<S>)
buf[i] = tmp[i];
else
buf[i] = srt::detail::round_sat<S>(static_cast<double>(tmp[i])
* static_cast<double>(std::numeric_limits<S>::max()));
buf[i] = tap::samplerate::detail::round_sat<S>(
static_cast<double>(tmp[i]) * static_cast<double>(std::numeric_limits<S>::max()));
}
}
std::size_t pos = 0;
Expand Down Expand Up @@ -176,13 +176,13 @@ namespace {

// --- ~120 dB tier: mono / stereo / 8ch -------------------------------------
void BM_SRT_Balanced_1ch(benchmark::State& s) {
srtBench<float>(s, srt::filter_spec::balanced(), 1);
srtBench<float>(s, tap::samplerate::filter_spec::balanced(), 1);
}
void BM_SRT_Balanced_2ch(benchmark::State& s) {
srtBench<float>(s, srt::filter_spec::balanced(), 2);
srtBench<float>(s, tap::samplerate::filter_spec::balanced(), 2);
}
void BM_SRT_Balanced_8ch(benchmark::State& s) {
srtBench<float>(s, srt::filter_spec::balanced(), 8);
srtBench<float>(s, tap::samplerate::filter_spec::balanced(), 8);
}
void BM_LSR_Medium_1ch(benchmark::State& s) {
lsrBench(s, SRC_SINC_MEDIUM_QUALITY, 1);
Expand Down Expand Up @@ -214,7 +214,7 @@ namespace {

// --- ~140 dB tier, stereo ---------------------------------------------------
void BM_SRT_Transparent_2ch(benchmark::State& s) {
srtBench<float>(s, srt::filter_spec::transparent(), 2);
srtBench<float>(s, tap::samplerate::filter_spec::transparent(), 2);
}
void BM_LSR_Best_2ch(benchmark::State& s) {
lsrBench(s, SRC_SINC_BEST_QUALITY, 2);
Expand All @@ -229,7 +229,7 @@ namespace {
// --- Fixed-point (no competitor analog; libsamplerate and soxr are
// float-only engines — this is the row embedded targets actually run) ------
void BM_SRT_Q15_Balanced_2ch(benchmark::State& s) {
srtBench<std::int16_t>(s, srt::filter_spec::balanced(), 2);
srtBench<std::int16_t>(s, tap::samplerate::filter_spec::balanced(), 2);
}
BENCHMARK(BM_SRT_Q15_Balanced_2ch);

Expand Down
10 changes: 5 additions & 5 deletions bench/icount/cmp_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ namespace {
#if SRT_CMP_ENGINE == 0

double run() {
const srt::polyphase_filter_bank<float> bank(srt::filter_spec::balanced(), 48000.0);
srt::fractional_resampler<float> rs(bank, kCh);
const auto input = sineInput(12000); // 0.25 s, cycled
std::size_t pos = 0;
const auto pop = [&](float* dst, std::size_t n) {
const tap::samplerate::polyphase_filter_bank<float> bank(tap::samplerate::filter_spec::balanced(), 48000.0);
tap::samplerate::fractional_resampler<float> rs(bank, kCh);
const auto input = sineInput(12000); // 0.25 s, cycled
std::size_t pos = 0;
const auto pop = [&](float* dst, std::size_t n) {
const std::size_t avail = 12000 - pos;
const std::size_t take = n < avail ? n : avail;
for (std::size_t i = 0; i < take * kCh; ++i)
Expand Down
20 changes: 10 additions & 10 deletions bench/icount/icount_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ namespace {
if constexpr (std::is_floating_point_v<S>)
return static_cast<S>(v);
else
return srt::detail::round_sat<S>(v * static_cast<double>(std::numeric_limits<S>::max()));
return tap::samplerate::detail::round_sat<S>(v * static_cast<double>(std::numeric_limits<S>::max()));
}

template <typename S>
Expand All @@ -40,15 +40,15 @@ namespace {

template <typename S>
double runKernel() {
const srt::polyphase_filter_bank<S> bank(srt::filter_spec::balanced(), 48000.0);
const auto hist = sineBlock<S>(bank.taps(), 997.0, 0.5);
double sink = 0.0;
double mu = 0.0;
const tap::samplerate::polyphase_filter_bank<S> bank(tap::samplerate::filter_spec::balanced(), 48000.0);
const auto hist = sineBlock<S>(bank.taps(), 997.0, 0.5);
double sink = 0.0;
double mu = 0.0;
for (int i = 0; i < 200000; ++i) {
mu += 0.6180339887498949;
if (mu >= 1.0)
mu -= 1.0;
sink += static_cast<double>(srt::interpolate(bank, hist.data(), mu));
sink += static_cast<double>(tap::samplerate::interpolate(bank, hist.data(), mu));
}
return sink;
}
Expand All @@ -59,11 +59,11 @@ namespace {

template <typename S>
double runPipeline() {
constexpr std::size_t kCh = SRT_SC_CH;
constexpr std::size_t kBlock = 32;
srt::config cfg;
constexpr std::size_t kCh = SRT_SC_CH;
constexpr std::size_t kBlock = 32;
tap::samplerate::config cfg;
cfg.channels = kCh;
srt::basic_async_sample_rate_converter<S> asrc(cfg);
tap::samplerate::basic_async_sample_rate_converter<S> asrc(cfg);

const auto input = sineBlock<S>(12000 * kCh, 997.0, 0.5); // 0.25 s, cycled
std::vector<S> out(kBlock * kCh);
Expand Down
4 changes: 2 additions & 2 deletions book/src/appendix/cpp-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,8 +600,8 @@ the conversion is a `reinterpret_cast` in a pair of helpers:
extern "C" { struct SrtHandle; } // opaque

namespace {
srt::async_sample_rate_converter* impl(SrtHandle* h) noexcept { ... }
const srt::async_sample_rate_converter* impl(const SrtHandle* h) noexcept { ... }
tap::samplerate::async_sample_rate_converter* impl(SrtHandle* h) noexcept { ... }
const tap::samplerate::async_sample_rate_converter* impl(const SrtHandle* h) noexcept { ... }
}
```

Expand Down
2 changes: 1 addition & 1 deletion book/src/part1/sample-traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ python3 -c "print(32767*65535, 2**31-1, 1 - 32767*65535/(2**31-1))"
# and DcGainIsUnityQ15 fails its ±4 tolerance.
# 2. In finalize (Q15), delete clamp_sat and cast directly — the full-scale
# sine test detects wraparound as a blown second difference.
# 3. Instantiate srt::basic_async_sample_rate_converter<double> anywhere and
# 3. Instantiate tap::samplerate::basic_async_sample_rate_converter<double> anywhere and
# read the concept diagnostic: every missing operation, by name, at the
# line you wrote.
```
Expand Down
2 changes: 1 addition & 1 deletion book/src/part2/tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ And a representative enforcement:

```cpp
TEST(AsrcQuality, Balanced997Hz) {
EXPECT_GT(measure_snr_db(srt::filter_spec::balanced(), 997.0), 128.0);
EXPECT_GT(measure_snr_db(tap::samplerate::filter_spec::balanced(), 997.0), 128.0);
}
```

Expand Down
4 changes: 2 additions & 2 deletions book/src/part5/scaling.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ by a factor of three because its tuning was written in hertz.
The remedy is the `scaled_to` trio, and the factory that applies it:

```cpp
srt::config cfg = srt::config::for_sample_rate(16000.0);
tap::samplerate::config cfg = tap::samplerate::config::for_sample_rate(16000.0);
cfg.channels = ...; // then adjust as usual
```

Expand Down Expand Up @@ -293,7 +293,7 @@ ctest --test-dir build -R AsrcQuality16k --output-on-failure

# The -32 dB failure itself, reproduced: in test_asrc_quality_16k.cpp,
# keep config::for_sample_rate(k_fs) but overwrite the servo with unscaled
# defaults (cfg.servo = srt::servo_config{};) — the converter still builds
# defaults (cfg.servo = tap::samplerate::servo_config{};) — the converter still builds
# and locks, and every threshold fails by ~30 dB, falling 6 dB per octave
# of tone frequency: the FM signature. (Restoring the unscaled *filter*
# instead fails fast: the constructor rejects band edges above the input
Expand Down
2 changes: 1 addition & 1 deletion docs/PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ the hot path follow it.
|---|---|---|
| Throughput | ns per output frame, steady-state `pull()`+`push()`, reported as ×realtime at 48 kHz | host (Google Benchmark) |
| Tail latency | p99/max per-call time for `pull(128)` over long runs — the RT budget lives in the tail, not the mean | host |
| Kernel cost | `srt::interpolate()` in isolation (≈ all datapath cycles: taps × channels MACs) | host |
| Kernel cost | `tap::samplerate::interpolate()` in isolation (≈ all datapath cycles: taps × channels MACs) | host |
| Embedded cost | **executed instructions** per output frame via QEMU TCG plugins — deterministic to the instruction, noise-free, well-correlated with real cost for scalar code | Hexagon (qemu-user), Cortex-M55 and Cortex-M33 (qemu-system) |

Cycle-accurate embedded numbers require vendor simulators (Hexagon SDK
Expand Down
12 changes: 6 additions & 6 deletions examples/alsa_bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ namespace {
g_stop.store(true, std::memory_order_relaxed);
}

const char* state_name(srt::converter_state s) {
const char* state_name(tap::samplerate::converter_state s) {
switch (s) {
case srt::converter_state::filling:
case tap::samplerate::converter_state::filling:
return "Filling";
case srt::converter_state::acquiring:
case tap::samplerate::converter_state::acquiring:
return "Acquiring";
case srt::converter_state::locked:
case tap::samplerate::converter_state::locked:
return "Locked";
}
return "?";
Expand Down Expand Up @@ -223,7 +223,7 @@ int main(int argc, char** argv) {
|| !open_device(out, args.out_dev, SND_PCM_STREAM_PLAYBACK, args))
return 1;

srt::config cfg;
tap::samplerate::config cfg;
cfg.sample_rate_hz = static_cast<double>(args.rate);
cfg.channels = args.channels;
cfg.target_latency_frames = args.latency;
Expand All @@ -232,7 +232,7 @@ int main(int argc, char** argv) {
// occupancy excursions can demote the servo stage spuriously.
cfg.servo.unlock_threshold_frames =
std::max(cfg.servo.unlock_threshold_frames, 1.5 * static_cast<double>(args.period));
srt::async_sample_rate_converter asrc(cfg);
tap::samplerate::async_sample_rate_converter asrc(cfg);
std::printf("designed latency: %.2f ms%s\n", asrc.designed_latency_seconds() * 1e3,
args.tone_hz > 0.0 ? " (tone mode: captured samples discarded)" : "");

Expand Down
Loading
Loading