Skip to content

Repository files navigation

SampleRateTap — the tap::sr sample-rate family

CI License: MIT C++20

One tree, two engines, one substrate. Header-only C++20, namespace tap::sr, built on the Tap family's shared FIR substrate DspTap (tap::dsp: Kaiser design, sample-format traits, FIR dot kernels, row-sum quantization, measurement instruments).

Engine Namespace What it does Where
async tap::sr::async Asynchronous, near-unity (±max_deviation_ppm, default 1000 ppm): two clock domains at nominally the same rate, one thread pushing at the input clock and one pulling at the output clock. Absorbs the clock. async/
bridge tap::sr::bridge Synchronous 44.1 ↔ 48 kHz (160/147 up, 147/160 down) and the pair at 2× and 4× (88.2 ↔ 96, 176.4 ↔ 192), direction and rate scale fixed at compile time, speed-first with Q15/Q31 profiles for M33/M55-class targets. Converts the number. bridge/

The engines never route by rate. The caller declares the clock topology by choosing a type: async when the clocks are independent, bridge when the ratio is the fixed 44.1/48 pair, and their composition — bridge converts the number, async absorbs the clock — for 44.1 ↔ 48 across independent clocks (bridge/examples/bluetooth_bridge.cpp). async is never chained behind a lookup, and no rate is ever served by an engine whose charter does not name it (PLAN.md, D12 and section 2). The engines planned next — rational (small-factor L/M within a rate family), pdm, varispeed — get their own charters and directories the same way.

Quick start

add_subdirectory(SampleRateTap)               # or FetchContent; submodules: recursive
target_link_libraries(app PRIVATE tap::sr::async)    # one engine
target_link_libraries(app PRIVATE tap::sr::bridge)   # the other
target_link_libraries(app PRIVATE tap::sr)           # both (the umbrella)
#include <tap/sr/async/async.h>   // tap::sr::async::converter, converter_q15, converter_q31
#include <tap/sr/bridge/ratio.h>  // tap::sr::bridge::converter_to_48k, converter_to_44k1, ...

Every configure builds both engines. CI selects an engine only when it runs tests, by ctest label. The family options are TAP_SR_*:

Option Default Builds
TAP_SR_BUILD_TESTS ON both engines' tests and the family's own (tests/)
TAP_SR_BUILD_EXAMPLES ON both engines' examples
TAP_SR_BUILD_CAPI OFF both engines' C ABI shared libraries (libtap_sr_async_capi, libtap_sr_bridge_capi)
TAP_SR_BUILD_ICOUNT_BENCH OFF both engines' instruction-count ratchet workloads
TAP_SR_BUILD_BENCHMARKS, TAP_SR_BUILD_COMPARE_BENCH, TAP_SR_BUILD_COMPARE_SHIM OFF the async engine's host-only benchmarks and comparison tooling
TAP_SR_ASYNC_WERROR, TAP_SR_BRIDGE_WERROR OFF warnings as errors, per engine

A retired pre-family option (SRT_*, TAP_RATIO_*) fails the configure loudly (cmake/retired_options.cmake) rather than dropping a gate silently.

Version. One family version, TAP_SR_VERSION_{MAJOR,MINOR,PATCH} (0.4.0), defined identically in each engine's umbrella header and returned bit-packed — (major << 16) | (minor << 8) | patch — by each C ABI's tap_sr_async_version() / tap_sr_bridge_version(). Tags are vX.Y.Z.

Build and test

git clone --recurse-submodules https://github.com/tap/SampleRateTap
cmake -S SampleRateTap -B build -DCMAKE_BUILD_TYPE=Release \
      -DTAP_SR_ASYNC_WERROR=ON -DTAP_SR_BRIDGE_WERROR=ON
cmake --build build -j
ctest --test-dir build --output-on-failure              # everything
ctest --test-dir build --output-on-failure -L '^async$' # one engine
ctest --test-dir build --output-on-failure -L '^bridge$'
scripts/tidy.sh                                         # the CI clang-tidy gate, locally

Tests are GoogleTest, named async.<Suite>.<Test> / bridge.<Suite>.<Test> and labelled by engine. The family's own tests (tests/, under both labels) enforce the dependency rule below and the version macros. CI runs the battery on Linux (GCC, Clang, ASan/UBSan, TSan), macOS and Windows, on Cortex-M33 and Cortex-M55 under qemu-system-arm, on Hexagon under qemu-hexagon, and on arm64; the instruction-count ratchet (scripts/icount.py --engine async|bridge) gates every workload of both engines two-sided at ±3 % against the committed <engine>/bench/baselines.json.

Layout

CMakeLists.txt          the family: options (D9), tap::dsp once, both engines, tap::sr, tests/
PLAN.md  CLAUDE.md      the family plan (charters, coverage rule, decisions) and working rules
LICENSE  STYLE.md       MIT; the shared Tap house style (.clang-format, .clang-tidy)
submodules/dsptap       the substrate, pinned once
cmake/  platform/       toolchains (Cortex-M33/M55, Hexagon), bare-metal startup, retired-option tripwire
scripts/                icount.py (the ratchet), tidy.sh, fetch_hexagon_toolchain.sh, doc updaters
tools/qemu_insn_plugin  the QEMU instruction-counting plugin
tests/                  the family's own tests: the dependency rule and the version macros
book/                   the async engine's book (published at https://tap.github.io/SampleRateTap/)
docs/                   Doxyfile; the migration's run record
async/                  include/tap/sr/async  tests  bench  examples  capi  notebooks  docs  README  PLAN
bridge/                 include/tap/sr/bridge tests  bench  examples  capi  notebooks  docs  README  PLAN

The dependency rule

async and bridge each depend on tap::dsp only. An engine may reach its sibling only under its own tests/ and examples/ — bridge's golden cross-validation against async, and bluetooth_bridge — never from a shipped header; capi/ is per engine. tests/ enforces this per engine: the link interface is exactly tap::dsp, every public header compiles in its own translation unit with only tap::dsp and the engine's include directory on the path, no public header names srt/, a sibling's tap/sr/<engine>/ or ../, and the public header count is pinned. Shared code (design math, traits, kernels, quantization, instruments) lands in DspTap first; this tree bumps the submodule pin.

Provenance and license

MIT (LICENSE: Timothy Place and the SampleRateTap contributors). The tree merged two repositories in 2026 with their histories intact: SampleRateTap v0.1.0 became the async engine and RatioTap v0.3.0 the bridge engine (bridge/docs/HISTORY.md maps RatioTap's commits and pull requests to their imported SHAs). Bisect across that merge with git bisect start --first-parent. The migration's gate runs are recorded in docs/MIGRATION_RUNS.md.

About

An ASRC (Asynchronous Sample Rate Converter) for passing audio across clock domains.

Resources

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages