Skip to content

Repository files navigation

RatioTap

CI License: MIT C++20

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 than transparent at 16 bits), plus the golden cross-validation against SampleRateTap's async engine (−109 dB down / −99 dB up over every phase), the bluetooth_bridge example, 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.

Quick start

#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.

The boundaries are identity, not policy

  • 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_bridge example (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.

Position in the Tap family

                    ┌────────────────────────────┐
                    │           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

Build

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-failure

Consume with add_subdirectory (or FetchContent) and link tap::ratio; the DspTap submodule rides along automatically.

Embedded targets and the instruction-count ratchet

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.so

License

MIT (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.

About

Sample Rate Conversion optimized for the 48Khz to 44.1Khz ratio (and vice-versa).

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages