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
74 changes: 74 additions & 0 deletions .github/workflows/book-pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Publishes the book to GitHub Pages on every push to main.
#
# The site is PUBLIC (https://tap.github.io/SampleRateTap/) even though the
# repository is private; it contains the book's code excerpts by design.
# One-time setup if the first run complains: Settings -> Pages -> Source:
# "GitHub Actions" (configure-pages below attempts to enable it itself).
name: book-pages

on:
push:
branches: [main]
paths:
- "book/**"
- "include/**"
- "platform/**"
- "tests/support/**"
- "tools/**"
- "cmake/**"
- ".github/workflows/book-pages.yml"
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: false

env:
MDBOOK_URL: https://github.com/rust-lang/mdBook/releases/download/v0.4.40/mdbook-v0.4.40-x86_64-unknown-linux-gnu.tar.gz
MDBOOK_SHA256: "9ef07fd288ba58ff3b99d1c94e6d414d431c9a61fdb20348e5beb74b823d546b"

jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 10
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- name: Install mdBook (pinned)
run: |
curl -sfLo /tmp/mdbook.tar.gz "$MDBOOK_URL"
actual=$(sha256sum /tmp/mdbook.tar.gz | cut -d' ' -f1)
if [ "$actual" != "$MDBOOK_SHA256" ]; then
echo "::error::mdbook checksum mismatch"; exit 1
fi
tar -xzf /tmp/mdbook.tar.gz -C /tmp

- name: Build (warnings are errors)
run: |
/tmp/mdbook build book 2>&1 | tee /tmp/book-build.log
if grep -qiE 'warning|error' /tmp/book-build.log; then
echo "::error::mdbook reported warnings/errors"
exit 1
fi

- name: Configure Pages
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
with:
enablement: true

- name: Upload site
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
with:
path: book/book

- name: Deploy
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -486,3 +486,56 @@ jobs:
bench/*.cpp bench/icount/*.cpp bench/compare/*.cpp \
tools/capi/*.cpp tools/qemu_insn_plugin/*.c \
tests/*.cpp tests/support/*.hpp examples/*.cpp platform/*.c

# The book (book/) quotes library code via mdBook anchor includes; this
# gate makes a refactor that orphans an excerpt fail CI, the same
# freshness contract as the README's generated tables. Warnings are
# errors: a missing anchor is a warning, and a missing anchor is rot.
book:
name: Book build
runs-on: ubuntu-latest
timeout-minutes: 10
env:
MDBOOK_URL: https://github.com/rust-lang/mdBook/releases/download/v0.4.40/mdbook-v0.4.40-x86_64-unknown-linux-gnu.tar.gz
MDBOOK_SHA256: "9ef07fd288ba58ff3b99d1c94e6d414d431c9a61fdb20348e5beb74b823d546b"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- name: Install mdBook (pinned)
run: |
curl -sfLo /tmp/mdbook.tar.gz "$MDBOOK_URL"
actual=$(sha256sum /tmp/mdbook.tar.gz | cut -d' ' -f1)
if [ "$actual" != "$MDBOOK_SHA256" ]; then
echo "::error::mdbook checksum mismatch"; exit 1
fi
tar -xzf /tmp/mdbook.tar.gz -C /tmp

- name: Build (warnings are errors)
run: |
/tmp/mdbook build book 2>&1 | tee /tmp/book-build.log
if grep -qiE 'warning|error' /tmp/book-build.log; then
echo "::error::mdbook reported warnings/errors (stale anchor or broken include?)"
exit 1
fi

# mdBook does not fail on a missing image, so check every relative
# image reference resolves. (The SVGs are committed, generated by
# scripts/book_figures.py; regeneration is not gated because
# matplotlib's SVG output is not byte-stable across versions.)
- name: Check image references resolve
run: |
python3 - <<'EOF'
import pathlib, re, sys
src = pathlib.Path("book/src")
missing = []
for md in src.rglob("*.md"):
for target in re.findall(r"!\[[^\]]*\]\(([^)#?]+)", md.read_text()):
if target.startswith(("http://", "https://")):
continue
if not (md.parent / target).resolve().exists():
missing.append(f"{md}: {target}")
if missing:
print("::error::broken image reference(s):")
print("\n".join(missing))
sys.exit(1)
EOF
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ CMakeUserPresets.json
.vscode/
.idea/
.claude/
book/book/
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,31 @@ there are no install/package rules yet. Version 0.1.0 (`SRT_VERSION_*` in
`srt/srt.hpp`, `srt_version()` over the C ABI); pre-1.0, the API may
still change between versions.

## The book

The repository includes a full-length tutorial book (`book/`) that walks
every header file line by line — the DSP, the C++ idioms chosen and
rejected, how thread safety works, how the servo was tuned, and the
optimization campaign with its dead ends preserved. It is written for a
reader learning C++, DSP, and real-time concurrency with this converter as
the running example. Three mechanical commitments keep it honest: every
code excerpt is included live from the actual headers at build time (CI
fails on a stale anchor), every figure is regenerated from the same math
and measured traces by `scripts/book_figures.py`, and every chapter ends
with runnable commands that reproduce its claims.

Read it at **<https://tap.github.io/SampleRateTap/>** (published from
`main` by the `book-pages` workflow; note the site is public even though
this repository is private), or build it locally with
[mdBook](https://rust-lang.github.io/mdBook/) (CI pins v0.4.40):

```sh
mdbook build book # or: mdbook serve book --open
```

Start at `book/src/SUMMARY.md` for the table of contents, or read the
sources directly — they are plain Markdown.

## How it works

The design follows the classic commercial-ASRC architecture (AD1896-style
Expand Down
14 changes: 14 additions & 0 deletions book/book.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[book]
title = "SampleRateTap: The Story of an Asynchronous Sample Rate Converter"
description = "A working tour of a real-time asynchronous sample rate converter: the DSP, the C++, the concurrency, and the measurements that hold it together."
authors = ["The SampleRateTap project"]
language = "en"
src = "src"

[build]
create-missing = false

[output.html]
default-theme = "rust"
git-repository-url = "https://github.com/tap/SampleRateTap"
site-url = "/SampleRateTap/"
47 changes: 47 additions & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Summary

[Introduction](introduction.md)

# Part 0 — The problem

- [Two crystals, one stream](part0/two-crystals.md)
- [Budgets: latency, quality, compute](part0/budgets.md)

# Part I — The machine, file by file

- [Designing the filter: kaiser.hpp](part1/kaiser.md)
- [The polyphase bank](part1/polyphase-bank.md)
- [Sample types as a customization point: sample_traits.hpp](part1/sample-traits.md)
- [The lock-free ring: spsc_ring.hpp](part1/spsc-ring.md)
- [The clock servo: pi_servo.hpp](part1/pi-servo.md)
- [The fractional resampler](part1/fractional-resampler.md)
- [Composition: asrc.hpp](part1/asrc.md)

# Part II — The proof system

- [Tests as specifications](part2/tests.md)
- [Counting instructions, deterministically](part2/icount.md)
- [Notebooks as calibrated instruments](part2/notebooks.md)

# Part III — Optimizing honestly

- [Profile first, claim later (C1–C2)](part3/c1-c2.md)
- [The integer phase and the wide MACs (C3–C5)](part3/c3-c5.md)
- [The channel axis (C6)](part3/c6.md)

# Part IV — Portability

- [Hexagon: a DSP that keeps secrets](part4/hexagon.md)
- [Cortex-M: bare metal, two ways](part4/cortex-m.md)
- [The C ABI](part4/c-abi.md)

# Part V — Deployment

- [Real clocks: bridges and firmware](part5/hardware.md)
- [Channels, rates, and the rules that scale](part5/scaling.md)

---

[Appendix A: The C++ decision log](appendix/cpp-decisions.md)
[Appendix B: Glossary](appendix/glossary.md)
[Appendix C: Annotated bibliography](appendix/bibliography.md)
107 changes: 107 additions & 0 deletions book/src/appendix/bibliography.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Appendix C: Annotated bibliography

> If I have seen further it is by standing on the shoulders of giants.
>
> — Isaac Newton, letter to Robert Hooke

This project's provenance statement is short: all code implements
long-published methods, and no third-party source was copied. This
appendix lists those methods' sources — plus the tools and competitors the
measurements depend on — with a note on what the project *actually took*
from each. It deliberately cites nothing the codebase does not genuinely
draw on.

## Signal processing

**J. F. Kaiser, "Nonrecursive digital filter design using the I₀-sinh
window function," *Proc. IEEE Int. Symp. Circuits and Systems*, 1974.**
The origin of the Kaiser window and of the two empirical fits the library
evaluates verbatim in `include/srt/detail/kaiser.hpp`: stopband
attenuation → window shape parameter β, and the attenuation/transition-
width → filter-length estimate. The project took the closed forms exactly
as published — the value of the Kaiser window here is precisely that its
design procedure is a page of code with known error bounds, needing no
iterative optimization at construction time.

**f. harris, *Multirate Signal Processing for Communication Systems*,
Prentice Hall, 2004.** The standard reference for polyphase decomposition
— factoring one long prototype filter into L short branches indexed by
fractional delay — which is the structure of the library's coefficient
table. The tap-length estimate in `estimateTaps()` is the Kaiser/harris
formula in the form `N = (A − 8) / (2.285 · Δω)`, applied per polyphase
branch; the codebase credits both names, as the literature does.

**J. O. Smith, "Digital Audio Resampling Home Page" (and the *Bandlimited
Interpolation* material), CCRMA, Stanford University.** The theory the
datapath implements: resampling as evaluation of a windowed-sinc
interpolation kernel at fractional positions, with a finite table of
kernel phases and *linear interpolation between adjacent table entries*.
Smith's analysis of that last step is where the library's most-quoted
scaling law comes from — interpolation residue falling ~12 dB per doubling
of the table size L and rising with signal frequency — which Part 0 turns
into the budget arithmetic connecting L to decibels.

**Analog Devices, AD1896 datasheet ("192 kHz Stereo Asynchronous Sample
Rate Converter").** The architectural ancestor. The README describes the
library as "the classic commercial-ASRC architecture (AD1896-style
polyphase FIR + clock servo), specialized for the near-unity regime," and
the datasheet documents that architecture: a polyphase interpolation
filter addressed by a recovered rate ratio, with a FIFO between the clock
domains. It also supplies the hardware row in the comparison table —
quoted as datasheet values, with the caveats about measurement environment
stated in `docs/COMPARISON.md`.

**AES17, *AES standard method for digital audio engineering — Measurement
of digital audio equipment* (Audio Engineering Society).** The measurement
definition behind the headline quality numbers: remove the fundamental,
integrate the residual across the audio band for THD+N, measure dynamic
range at −60 dBFS with A-weighting. The comparison notebook implements an
AES17-style procedure (exact fit plus ±20 Hz notch, 20 Hz–20 kHz
integration) and calibrates it against synthetic signals before use — the
standard is what makes the −132 dB figure commensurable with silicon
datasheets rather than a house metric.

## The measured competitors

**libsamplerate (Secret Rabbit Code), E. de Castro Lopo —
documentation at libsndfile.github.io/libsamplerate.** The closest
architectural analog (streaming time-domain polyphase resampler) and one
of the two software subjects measured under identical conditions in
`docs/COMPARISON.md` and the comparison notebook. Its documentation also
supplied the honesty check the comparison repeats: the published "97 dB
worst case" figure applies to aggressive ratios, so near-unity results at
the format ceiling are its *easy* regime, not a contradiction.

**soxr (the SoX Resampler library) — github.com/chirlu/soxr.** The second
measured competitor, and the source of its own latency figure via
`soxr_delay()`. What the project took from soxr is mostly a boundary
lesson made quantitative: soxr wins raw host throughput decisively and
carries ~12–16 ms of latency doing it, which is the measured statement of
why a 1–2 ms live-monitoring budget needs a different design.

## C++

**Anthony Williams, *C++ Concurrency in Action*, 2nd ed., Manning, 2019.**
The working reference for the C++ memory model as this book teaches it:
acquire/release pairing as the establishment of happens-before, the
legitimacy of relaxed loads of data a thread itself owns, and lock-free
queue design generally. The ring chapter's proof style — argue the two
release/acquire pairs, then treat everything else as sequential code —
is the book's method applied to a hundred-line class.

**cppreference.com — in particular `std::memory_order`,
`std::atomic<T>::is_always_lock_free`, `std::bit_ceil`, and
`std::hardware_destructive_interference_size`.** The day-to-day authority
for the exact semantics the headers rely on: the ordering guarantees the
ring asserts, the compile-time lock-freedom predicate the audit added,
the power-of-two rounding used by the ring and the polyphase table, and —
for the interference-size constant — the documented ABI fragility that
justified *rejecting* the standard facility in favor of a literal `64`.

## Tooling

**mdBook — rust-lang.github.io/mdBook.** The tool this book is built
with. Its `\{{#include path:anchor}}` mechanism is what makes the book's
central honesty commitment mechanical rather than aspirational: code
excerpts are pulled from the real headers at build time, so prose that
drifts from the code breaks the build in CI instead of quietly lying.
Loading
Loading