Synchronous 44.1 ↔ 48 kHz sample rate conversion, as fast as possible.
One rational ratio pair — 160/147 up, 147/160 down — one clock, and the entire optimization budget spent on exactly that. Header-only C++20, built on the Tap family's shared FIR substrate (DspTap: Kaiser prototype design, float/Q15/Q31 sample-format traits, measured dot-product kernels, row-sum quantization, measurement instruments).
Status: v0.2 (M7 codegen campaign). v0.1 shipped the converter for all three sample formats: float (the golden model, pinned against committed scipy reference vectors sample-for-sample), Q31 (tracks float within −147 dB), and Q15 (format-limited: pair it with
economy, which is both cheaper and quieter thantransparentat 16 bits), plus the golden cross-validation against SampleRateTap's async engine (−109 dB down / −99 dB up over every phase), thebluetooth_bridgeexample, the C ABI, and the executed demo notebook. v0.2 is the measured optimization campaign on top — four levers, each gated by the instruction-count ratchet, outputs bit-identical throughout: the superblock walk, committed compile-time trip counts, and symmetry-halved tables. Since the campaign's baselines: Q15 −59%/−60% and float −35%/−37% on Cortex-M55, Q31 −26%/−27% on Cortex-M33, Q15 −13%/−10% on Hexagon — with table storage halved (economy Q15 up: 6.9 KiB). The remaining PLAN §7 levers (multistage, minimum-phase, IIR, FFT) change the output contract and stay deferred until a consumer needs them. PLAN.md is the authoritative roadmap (charter, architecture decisions, milestones, acceptance criteria, per-lever measurements); HANDOFF.md is the original design brief it grew from.
#include <tap/ratio/ratio.h>
tap::ratio::converter_to_44k1 down(2); // 48 -> 44.1, stereo, economy
std::vector<float> out(down.outputs_for(n_in) * 2);
std::size_t made = down.process(in, n_in, out.data()); // noexcept, alloc-free
// ... and at end of stream:
std::vector<float> tail(down.flush_output_frames() * 2);
down.flush(tail.data());Direction is a compile-time type (converter_to_48k / converter_to_44k1,
plus _q15 / _q31 fixed-point variants); pull(out, n, pop_fn) is the
callback-driven shape, and frames_needed(n) is exact arithmetic. For
44.1↔48 across independent clocks (a Bluetooth chip on its own crystal),
compose with SampleRateTap — examples/bluetooth_bridge.cpp is the
documented recipe: +200 ppm crystal, servo locked, 997 Hz recovered
exactly, 2.0 ms total latency.
- No other ratios. Not 2:1, not 96→44.1, not arbitrary L/M. The public surface is 44.1↔48 only, which is what licenses the optimization work (straight-line superblock codegen, baked tables, multistage decomposition) to hard-commit to phase counts of exactly 147 and 160.
- No asynchronous conversion. If the two ends of your chain run on
different crystals — even at nominally 44.1-vs-48 — that is the
SampleRateTap near-unity ASRC's
problem, reached by composition: RatioTap converts the number, the
ASRC absorbs the clock. Which engine applies is a property of the
clock topology, never inferred from a float ratio. The
bluetooth_bridgeexample (milestone M6) documents the composition. - Speed-first. Direction is a compile-time parameter; the default quality profile takes the speed side of every inaudible trade (all alias products confined above 20 kHz by arithmetic — see the plan's profile section), with a pristine 120 dB profile behind the same design path.
┌────────────────────────────┐
│ DspTap │ shared substrate (submodule)
│ kaiser design · sample │
│ traits (float/Q15/Q31) · │
│ FIR dot kernels · row-sum │
│ quantization · analysis │
└──────┬──────────────┬──────┘
│ │
┌────────────┴───┐ ┌──────┴─────────┐
│ SampleRateTap │ │ RatioTap │
│ async, near- │ │ sync, 44.1↔48, │
│ unity, servo │ │ speed-first │
└────────────┬───┘ └──────┬─────────┘
│ │
└──── test-only│dependency:
golden cross-validation
git clone --recurse-submodules https://github.com/tap/RatioTap
cmake -S RatioTap -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failureConsume with add_subdirectory (or FetchContent) and link tap::ratio;
the DspTap submodule rides along automatically.
The deployment cores are CI targets, not aspirations: every push runs the
emulation-sized test suite on Cortex-M33 (QEMU mps2-an505 — Raspberry
Pi Pico 2 class), Cortex-M55 (mps3-an547) and Hexagon
(qemu-hexagon, static musl), and gates eight fixed conversion workloads
(direction × float/Q15/Q31) against committed per-target instruction
counts (bench/baselines.json, two-sided ±3% — see scripts/icount.py).
The counts are deterministic, so the M7 optimization campaign in
PLAN.md lands one measured lever at a time:
cmake -B build-m55 -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=cmake/arm-cortex-m55-mps3.cmake \
-DTAP_RATIO_BUILD_TESTS=OFF -DTAP_RATIO_BUILD_EXAMPLES=OFF \
-DTAP_RATIO_BUILD_ICOUNT_BENCH=ON
cmake --build build-m55 -j
python3 scripts/icount.py --target m55 --build-dir build-m55 --plugin libinsncount.soMIT (see LICENSE), consistent with the family. Style is the shared Tap House Rules, enforced by pre-commit clang-format, the drift check, and clang-tidy in CI.