diff --git a/README.md b/README.md index 1b63831..9a7bf9f 100644 --- a/README.md +++ b/README.md @@ -369,10 +369,62 @@ the float path; Q15's floor is the 16-bit format itself. The servo and the filter design always run in double (control path / one-time init, a handful of operations per block). +## Position in the Tap family + +SampleRateTap is one of two rate converters in the **Tap** family, both +built on the same shared substrate: + +``` + ┌────────────────────────────┐ + │ 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 +``` + +[DspTap](https://github.com/tap/DspTap) (vendored at `submodules/dsptap`) +provides the Kaiser prototype design, the float/Q15/Q31 sample-format +traits, the measured FIR dot-product kernels, the row-sum quantization, +and the analysis instruments shared by the tests and notebooks. +[RatioTap](https://github.com/tap/RatioTap) is the synchronous sibling: +exactly one rational ratio pair (160/147 up, 147/160 down — 44.1 ↔ 48 kHz), +one clock, speed-first. + +Which converter you need is a property of the **clock topology**, never +inferred from a float ratio: + +- Same nominal rate on both sides, independent oscillators (ppm drift) — + this library. +- 44.1 ↔ 48 kHz on one clock (file conversion, a single interface) — + RatioTap. +- 44.1 ↔ 48 kHz across *independent* oscillators (a Bluetooth chip on its + own crystal) — both, composed: RatioTap converts the *number*, this + library absorbs the *clock*. RatioTap's `bluetooth_bridge` example is + the documented recipe (+200 ppm crystal, servo locked, 997 Hz recovered + exactly, 2.0 ms total latency). + +The two converters check each other: RatioTap's suite cross-validates its +output against this library's async engine at −109 dB (down) / −99 dB (up) +over every polyphase phase. + ## Limitations - Near-unity ratios only (±`max_deviation_ppm`, default 1000 ppm). No - 44.1 ↔ 48 kHz conversion. + 44.1 ↔ 48 kHz conversion — that job belongs to + [RatioTap](https://github.com/tap/RatioTap), and 44.1 ↔ 48 across + independent clocks to the composition of the two (see + [Position in the Tap family](#position-in-the-tap-family)). - The rate estimate is derived from FIFO counts only. With block-quantized transfer its instantaneous value wobbles at the block-beat frequency (see `converter_status.ppm` vs. a few seconds of averaging), and ultra-quiet servo diff --git a/book/src/part0/two-crystals.md b/book/src/part0/two-crystals.md index 96c5d50..6064b3e 100644 --- a/book/src/part0/two-crystals.md +++ b/book/src/part0/two-crystals.md @@ -167,7 +167,12 @@ chips solve. It handles *only* the near-unity case: two domains at nominally the same rate, within ±1000 ppm by default. It will never convert 44.1 kHz to 48 kHz — the README lists this first among its limitations, and `docs/COMPARISON.md` is blunt that for genuine rate -*conversion* you should put soxr or libsamplerate in the chain. +*conversion* you should put a synchronous resampler in the chain: soxr, +libsamplerate, or — for exactly the 44.1 ↔ 48 pair, built on the same +DspTap substrate as this library and cross-validated against its engine — +the family's own [RatioTap](https://github.com/tap/RatioTap). Part V +returns to that composition and its decision rule: RatioTap converts the +*number*, the ASRC absorbs the *clock*. Here is what the restriction buys. A general-ratio converter must be able to place output samples anywhere relative to input samples, at any diff --git a/book/src/part5/scaling.md b/book/src/part5/scaling.md index 8a5703a..c24a5f9 100644 --- a/book/src/part5/scaling.md +++ b/book/src/part5/scaling.md @@ -183,6 +183,44 @@ samples*, which is three times as many milliseconds at 16 kHz (1.5 ms vs slower — the 16 kHz test runs 120 s where the 48 kHz one ran 40 s, the same number of samples and of time constants. +### A different rate on each side belongs to RatioTap + +Everything above scales the *common* nominal rate; it does not license +44.1 ↔ 48. With the default configuration the servo clamps its estimate +at ±1.5 × `max_deviation_ppm` — ±1500 ppm against an 8.8% gap — and the +near-unity datapath has none of the band-limiting machinery a genuine +downward conversion needs. That conversion is a different problem — +synchronous, rational, known at compile time — and in the Tap family it +has a different engine: [RatioTap](https://github.com/tap/RatioTap), +built on the same DspTap substrate this book has been reading (the same +Kaiser design path, sample traits, and dot kernels), with its entire +optimization budget spent on exactly one ratio pair, 160/147 up and +147/160 down. + +The decision rule is worth stating the way RatioTap's README states it, +because the failure mode is choosing an engine from a float: **which +converter applies is a property of the clock topology, never inferred +from the ratio.** A 44.1 kHz file rendered for a 48 kHz interface is +RatioTap alone — one clock, a number to convert. Two 48 kHz devices on +separate crystals is this library alone — one nominal rate, a clock to +absorb. A Bluetooth chip running 44.1 kHz on its own crystal into a +48 kHz host is both, composed: RatioTap converts the *number*, the ASRC +absorbs the *clock*. + +The composition is not hypothetical, and it is measured. RatioTap's +`bluetooth_bridge` example is the documented recipe — +200 ppm on the +simulated crystal, servo locked, 997 Hz recovered exactly, 2.0 ms of +total latency through both stages — and the two engines check each +other: RatioTap's suite pins its output against this library's async +engine at −109 dB (down) / −99 dB (up) over every polyphase phase. Two +independent implementations of the same mathematics agreeing at the +noise floor is the strongest mutual check either repository runs. + +The working code lives in RatioTap's repository, which is why this +section is prose rather than a code walk: the book's include-anchor +contract quotes only from this tree, and the recipe is RatioTap's to +keep honest. What this book owes you is the decision rule above. + ## Blocks: feasibility, then observability The block-size axis has two boundaries, one hard and one @@ -308,6 +346,13 @@ jupyter nbconvert --execute notebooks/asrc_block_size_study.ipynb # adaptive raise reports itself in effective_target_latency_frames instead # of dropping out: ./build/examples/drifting_clocks + +# The 44.1↔48 composition (RatioTap converts the number, this library +# absorbs the clock) runs from the sibling repository: +git clone --recurse-submodules https://github.com/tap/RatioTap +cmake -S RatioTap -B rt-build -DCMAKE_BUILD_TYPE=Release +cmake --build rt-build --target bluetooth_bridge -j 4 +./rt-build/examples/bluetooth_bridge ``` The break-it-on-purpose suggestions are, as ever, the chapter in diff --git a/docs/COMPARISON.md b/docs/COMPARISON.md index c65304f..62a5ffb 100644 --- a/docs/COMPARISON.md +++ b/docs/COMPARISON.md @@ -132,7 +132,10 @@ libsamplerate has no fixed-point path, so its cheapest option on such parts cost beyond; SampleRateTap deliberately handles only clock *drift* around a common nominal rate — that restriction is what buys the 48-tap datapath, 0.5 ms filter delay, and embedded-class compute. For genuine rate - *conversion*, put soxr/libsamplerate in the chain. + *conversion*, put a synchronous resampler in the chain — + soxr/libsamplerate, or for exactly 44.1↔48 the family's own + [RatioTap](https://github.com/tap/RatioTap), which cross-validates its + output against this library's engine. - **Coarse-block operation is a different regime** (cent-scale low-rate FM over a 53–61 dB floor — measured in [the block-size study](../notebooks/asrc_block_size_study.ipynb)); the