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
16 changes: 11 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -519,9 +519,9 @@ jobs:
echo "$(dirname "$clangxx")" >> "$GITHUB_PATH"
echo "$HOME/qemu-hexagon-plugins" >> "$GITHUB_PATH"

# Release (-O2), matching how the baselines were recorded; both engines'
# workloads in one tree, tests and examples off.
- name: Build workloads (both engines)
# Release (-O2), matching how the baselines were recorded; every
# engine's workloads in one tree, tests and examples off.
- name: Build workloads (every engine)
run: |
case "${{ matrix.target }}" in
m55) tc=cmake/arm-cortex-m55-mps3.cmake ;;
Expand All @@ -539,13 +539,19 @@ jobs:
--build-dir build --plugin /tmp/libinsncount.so

# Runs even if async failed: each engine's numbers are independent
# evidence. The job still fails if either step failed.
# evidence. The job still fails if any step failed.
- name: Ratchet bridge
if: ${{ !cancelled() }}
run: >
python3 scripts/icount.py --engine bridge --target ${{ matrix.target }}
--build-dir build --plugin /tmp/libinsncount.so

- name: Ratchet rational
if: ${{ !cancelled() }}
run: >
python3 scripts/icount.py --engine rational --target ${{ matrix.target }}
--build-dir build --plugin /tmp/libinsncount.so

# Each engine README's instruction-count table derives 1:1 from its
# committed baselines; regenerating it must produce no diff.
icount-docs:
Expand All @@ -555,7 +561,7 @@ jobs:
strategy:
fail-fast: false
matrix:
engine: [async, bridge]
engine: [async, bridge, rational]
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Docs freshness
Expand Down
23 changes: 12 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
over one substrate. `async/` (`tap::sr::async`) is the asynchronous near-unity converter that
*absorbs the clock*; `bridge/` (`tap::sr::bridge`) is the synchronous 44.1 ↔ 48 kHz converter that
*converts the number*; `rational/` (`tap::sr::rational`) is the synchronous small-factor L/M
converter within a rate family (L, M ∈ {2^a·3^b}), chains of Nyquist stages, at M5 of its plan
(ratio types, pinned designs, the single stages, the named chains, the 182-row coverage matrix and
the fixed-point profiles measured per stage; the C ABI follows). All build on DspTap (`submodules/dsptap`,
`tap::dsp`), pinned once at the root. Each engine has its own `README.md`, `PLAN.md` and `CLAUDE.md`;
converter within a rate family (L, M ∈ {2^a·3^b}), chains of Nyquist stages, at M6 of its plan
(ratio types, pinned designs, the single stages, the named chains, the 182-row coverage matrix, the
fixed-point profiles measured per stage, the C ABI, the executed matrix notebook and the ratchet).
All build on DspTap (`submodules/dsptap`, `tap::dsp`), pinned once at the root. Each engine has its own `README.md`, `PLAN.md` and `CLAUDE.md`;
read the engine's before touching its code.

**`PLAN.md` is the family plan**: the charters and the coverage rule (section 2), the settled
Expand Down Expand Up @@ -42,10 +42,10 @@ made this tree (sections 5–6). Do not re-derive what it settles.
- **Substrate discipline.** Shared code (design math, sample traits, kernels, quantization,
measurement instruments) lands in DspTap first; this tree bumps the submodule pin. Never fork
substrate code into an engine.
- **One version (D13).** `TAP_SR_VERSION_*` is 0.4.0, defined token-identically in each umbrella
- **One version (D13).** `TAP_SR_VERSION_*` is 0.5.0, defined token-identically in each umbrella
header (checked by `tests/family/version_macros.cpp`) and returned bit-packed by each C ABI's
`tap_sr_<engine>_version()` (pinned by `CApi.VersionIsBitPacked`). Tags are `vX.Y.Z`; bump all
three headers and the root `project()` together (0.5.0 at `rational`'s M6).
three headers and the root `project()` together (0.5.0 came with `rational`'s M6).
- **Clean renames, no aliases (D7).** Retired options fail the configure
(`cmake/retired_options.cmake`); retired override macros hit an `#error`. Do not add aliases.

Expand All @@ -67,12 +67,12 @@ over statistical sampling, and measured numbers stated with their provenance.

**Embedded legs.** CI runs every engine's battery on Cortex-M33 and M55 under `qemu-system-arm`
(`cmake/arm-cortex-*.cmake`, one-shot `bare_metal_main.cpp` per engine) and on Hexagon under
`qemu-hexagon`, and the instruction-count ratchet gates every workload of async and bridge
(`rational`'s arrive at its M6) two-sided at
±3 % (`scripts/icount.py --engine async|bridge --target m33|m55|hexagon`, baselines in
`qemu-hexagon`, and the instruction-count ratchet gates every workload of every engine two-sided
at ±3 % (`scripts/icount.py --engine async|bridge|rational --target m33|m55|hexagon`, baselines in
`<engine>/bench/baselines.json`). A change that moves a count beyond tolerance re-records the
baseline in the same PR; an improvement beyond tolerance fails too, by design. Guest markers
(`SRT_ICOUNT_DONE`, `RATIO_ICOUNT_DONE`) are part of the counted binaries and never change.
(`SRT_ICOUNT_DONE`, `RATIO_ICOUNT_DONE`, `RATIONAL_ICOUNT_DONE`) are part of the counted binaries
and never change.

## Style

Expand All @@ -98,5 +98,6 @@ migration's gates and their run record are in `PLAN.md` sections 5–6 and `docs
DspTap changes land in DspTap first, then this tree bumps `submodules/dsptap`. The book
(`book/`, https://tap.github.io/SampleRateTap/) is published from `main` by `book-pages`; the
notebooks are committed executed against the shipping C++ through each engine's C ABI and
binding (`async/notebooks/`, `bridge/notebooks/tap_sr_bridge_py.py`) — re-execute them when the
binding (`async/notebooks/`, `bridge/notebooks/tap_sr_bridge_py.py`,
`rational/notebooks/tap_sr_rational_py.py`) — re-execute them when the
behaviour they measure changes.
6 changes: 3 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.24)
# composes them over one tap::dsp, declares the family options (D9) and the
# umbrella target, and adds the family's own tests (the dependency rule of
# PLAN.md 4.2).
project(SampleRateTap VERSION 0.4.0 LANGUAGES CXX)
project(SampleRateTap VERSION 0.5.0 LANGUAGES CXX)
enable_testing()

include(cmake/retired_options.cmake)
Expand All @@ -19,8 +19,8 @@ include(cmake/retired_options.cmake)
# calls find the cache entry set.
option(TAP_SR_BUILD_TESTS "Build the engines' tests and the family's own" ON)
option(TAP_SR_BUILD_EXAMPLES "Build the engines' examples" ON)
option(TAP_SR_BUILD_CAPI "Build both engines' C ABI shared libraries" OFF)
option(TAP_SR_BUILD_ICOUNT_BENCH "Build both engines' instruction-count ratchet workloads" OFF)
option(TAP_SR_BUILD_CAPI "Build every engine's C ABI shared library" OFF)
option(TAP_SR_BUILD_ICOUNT_BENCH "Build every engine's instruction-count ratchet workloads" OFF)
option(TAP_SR_BUILD_BENCHMARKS "Build the async engine's benchmarks (host-only)" OFF)
option(TAP_SR_BUILD_COMPARE_BENCH "Build the async engine's resampler comparison benchmarks (host-only)" OFF)
option(TAP_SR_BUILD_COMPARE_SHIM "Build the async engine's r8brain shim for its comparison notebook" OFF)
Expand Down
11 changes: 9 additions & 2 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1024,8 +1024,15 @@ G13 and G14, plus G9 from 3.7.
stage: bit-pinned Q15 / Q31 tables, exact unity, saturation, Q31 within
3.4e−9 of double, Q15's floor and attained stopband stated (a Q15
decimator by 6 or 8 attains −65 / −63 dB, not 70) — landed
(`rational/PLAN.md` v0.8). Next: the C ABI, notebook and icount
baselines with the family version 0.5.0 (M6).
(`rational/PLAN.md` v0.8). M6 — the C ABI (the named chains as
constants, never a rate; one stage at a stated design divisor), the
binding and `matrix.ipynb` executed through the C ABIs (728 of 728
pins reproduced), the icount ratchet on M33 / M55 / Hexagon, and the
family version 0.5.0 — landed (`rational/PLAN.md` v0.9). The plan's
milestones are complete; the codegen levers it defers (sparse rows for
the mixed ratios going down, the symmetry-halved table, the Q15
decimators' per-branch quantization, an MVE Q15 kernel) wait for a
consumer.

---

Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ quantization, measurement instruments).
|---|---|---|---|
| **`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/`](async/README.md) |
| **`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/`](bridge/README.md) |
| **`rational`** | `tap::sr::rational` | Synchronous small-factor L/M *within* a rate family (L, M ∈ {2^a·3^b}: ↑2, ↓3, 2/3, …), as chains of Nyquist (L-th-band) stages, the ratio a compile-time type. **Converts the number.** M1–M5 landed (ratio types, pinned designs, the single stages for every ratio of the vocabulary, the named chains and the 182-row coverage matrix generated from the measured lengths, the fixed-point profiles measured per stage); the C ABI follows its plan. | [`rational/`](rational/README.md) |
| **`rational`** | `tap::sr::rational` | Synchronous small-factor L/M *within* a rate family (L, M ∈ {2^a·3^b}: ↑2, ↓3, 2/3, …), as chains of Nyquist (L-th-band) stages, the ratio a compile-time type. **Converts the number.** M1–M6 landed (ratio types, pinned designs, the single stages for every ratio of the vocabulary, the named chains and the 182-row coverage matrix generated from the measured lengths, the fixed-point profiles measured per stage, the C ABI, the executed matrix notebook and the instruction-count ratchet): its plan is complete. | [`rational/`](rational/README.md) |

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
Expand Down Expand Up @@ -49,18 +49,20 @@ tests, by ctest label. The family options are `TAP_SR_*`:
|---|---|---|
| `TAP_SR_BUILD_TESTS` | ON | the engines' tests and the family's own (`tests/`) |
| `TAP_SR_BUILD_EXAMPLES` | ON | the engines' examples |
| `TAP_SR_BUILD_CAPI` | OFF | the engines' C ABI shared libraries (`libtap_sr_async_capi`, `libtap_sr_bridge_capi`; `rational`'s at its M6) |
| `TAP_SR_BUILD_ICOUNT_BENCH` | OFF | the engines' instruction-count ratchet workloads (`rational`'s at its M6) |
| `TAP_SR_BUILD_CAPI` | OFF | the engines' C ABI shared libraries (`libtap_sr_async_capi`, `libtap_sr_bridge_capi`, `libtap_sr_rational_capi`) |
| `TAP_SR_BUILD_ICOUNT_BENCH` | OFF | every engine's 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`, `TAP_SR_RATIONAL_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`.
(0.5.0, the minor bump the third engine's C ABI brought), 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()` /
`tap_sr_rational_version()`. Tags are `vX.Y.Z`.

## Build and test

Expand Down
3 changes: 2 additions & 1 deletion async/capi/tap_sr_async_capi.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ typedef struct tap_sr_async_converter tap_sr_async_converter;

/* ABI/version probe: the family version, bit-packed as
* (TAP_SR_VERSION_MAJOR << 16) | (TAP_SR_VERSION_MINOR << 8) | TAP_SR_VERSION_PATCH
* (0x000400 for 0.4.0); tap_sr_bridge_version returns the same value. */
* (0x000500 for 0.5.0); tap_sr_bridge_version and tap_sr_rational_version
* return the same value. */
unsigned tap_sr_async_version(void);

/* preset: 0 = fast, 1 = balanced, 2 = transparent.
Expand Down
2 changes: 1 addition & 1 deletion async/include/tap/sr/async/async.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
#pragma once

#define TAP_SR_VERSION_MAJOR 0
#define TAP_SR_VERSION_MINOR 4
#define TAP_SR_VERSION_MINOR 5
#define TAP_SR_VERSION_PATCH 0

#include "tap/sr/async/converter.h"
4 changes: 2 additions & 2 deletions async/tests/test_capi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ namespace {
static_cast<unsigned>((TAP_SR_VERSION_MAJOR << 16) | (TAP_SR_VERSION_MINOR << 8) | TAP_SR_VERSION_PATCH);
const unsigned v = tap_sr_async_version();
EXPECT_EQ(v, k_packed);
EXPECT_EQ(v, 0x000400u); // 0.4.0
EXPECT_EQ(v, 0x000500u); // 0.5.0
EXPECT_EQ(v >> 16, 0u);
EXPECT_EQ((v >> 8) & 0xFFu, 4u);
EXPECT_EQ((v >> 8) & 0xFFu, 5u);
EXPECT_EQ(v & 0xFFu, 0u);
}

Expand Down
7 changes: 4 additions & 3 deletions book/src/part4/c-abi.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,9 @@ of sentence you only think to write after watching Part IV's 32-bit ports
in action.

**`tap_sr_async_version()` is a probe.** It returns the family version
bit-packed, `(major << 16) | (minor << 8) | patch` — `0x000400` (1024) for
0.4.0, the value `tap_sr_bridge_version()` returns as well. A version
bit-packed, `(major << 16) | (minor << 8) | patch` — `0x000500` (1280) for
0.5.0, the value `tap_sr_bridge_version()` and `tap_sr_rational_version()`
return as well. A version
*macro* would vanish into the caller's compile; a version *function*
reports what the loaded shared library actually is, which is the question
an FFI user is really asking when their symbols don't match their
Expand Down Expand Up @@ -302,7 +303,7 @@ cmake --build build --target tap_sr_async_capi -j
# from this file (the impl() helpers are invisible, as promised):
nm -D --defined-only build/async/capi/libtap_sr_async_capi.so | grep tap_sr_async_

# The one-integer smoke test (0.4.0 -> 1024, i.e. 0x000400):
# The one-integer smoke test (0.5.0 -> 1280, i.e. 0x000500):
python3 -c "import ctypes; \
print(ctypes.CDLL('build/async/capi/libtap_sr_async_capi.so').tap_sr_async_version())"

Expand Down
2 changes: 1 addition & 1 deletion bridge/include/tap/sr/bridge/ratio.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
#include "tap/sr/bridge/schedule.h" // IWYU pragma: export

#define TAP_SR_VERSION_MAJOR 0
#define TAP_SR_VERSION_MINOR 4
#define TAP_SR_VERSION_MINOR 5
#define TAP_SR_VERSION_PATCH 0

namespace tap::sr::bridge {
Expand Down
4 changes: 2 additions & 2 deletions bridge/tests/test_capi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ namespace {
static_cast<unsigned>((TAP_SR_VERSION_MAJOR << 16) | (TAP_SR_VERSION_MINOR << 8) | TAP_SR_VERSION_PATCH);
const unsigned v = tap_sr_bridge_version();
EXPECT_EQ(v, k_packed);
EXPECT_EQ(v, 0x000400u); // 0.4.0
EXPECT_EQ(v, 0x000500u); // 0.5.0
EXPECT_EQ(v >> 16, 0u);
EXPECT_EQ((v >> 8) & 0xFFu, 4u);
EXPECT_EQ((v >> 8) & 0xFFu, 5u);
EXPECT_EQ(v & 0xFFu, 0u);
}

Expand Down
13 changes: 9 additions & 4 deletions rational/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ anything; do not re-derive what it settles (the compile-time ratio, one Nyquist
stage factoring by MACs then by stage count, `bridge` at the lowest k, the profile vocabulary,
latency as an exact rational).

Current state: **M5** — the tree, `ratio<L, M>` with its charter `static_assert`s and
Current state: **M6**, the plan complete — the tree, `ratio<L, M>` with its charter `static_assert`s and
`ratio_traits`, `profile` carrying the pinned taps per branch for every band of the vocabulary
(the M2 design spike, `notebooks/design_spike.ipynb`, executed; the pins found on a
16384-point grid and enforced by `tests/test_design.cpp`) and, since M4, the relaxation tables
Expand All @@ -35,8 +35,13 @@ generated from the pinned lengths by `tools/coverage/matrix.py` (section 3 of th
cross-family rows through `tests/support/bridge_stage.h`), and, since M5, the fixed-point
profiles measured per stage by `tests/test_fixed_point.cpp` (bit-pinned Q15 / Q31 tables, exact
unity, saturation, the Q15 floors and attained stopbands stated in PLAN.md section 6 — a
change that moves a table pin is a numeric change to the fixed-point datapath). M6 (C ABI,
notebook, icount baselines; family version 0.5.0) follows in PLAN.md section 6.
change that moves a table pin is a numeric change to the fixed-point datapath), and, since M6,
the C ABI (`capi/`: the named chains as stable `TAP_SR_RATIONAL_*` constants, one stage at a
stated design divisor, exactly sixteen exported symbols; `CApi.*` pins it bit for bit against
`basic_chain`), the ctypes binding and `notebooks/matrix.ipynb` executed through the C ABIs, and
the icount ratchet (`bench/icount/`, `bench/baselines.json`, marker `RATIONAL_ICOUNT_DONE`; a
change that moves a count beyond ±3 % re-records the baselines in the same PR). The codegen
levers PLAN.md defers after M6 wait for a consumer.

## The charter constraints (load-bearing)

Expand All @@ -45,7 +50,7 @@ notebook, icount baselines; family version 0.5.0) follows in PLAN.md section 6.
writes; absorbing a clock is `async`, by composition. The charter is a `static_assert` on
`ratio<L, M>`: L and M of the form 2^a · 3^b, lowest terms, L ≠ M. Nothing else compiles.
- **Never routed to by rate (D12).** `ratio<L, M>` names the number, a chain names the stages;
there is no `(in_hz, out_hz)` lookup here or in the C ABI (whose enumerators name chains). The
there is no `(in_hz, out_hz)` lookup here or in the C ABI (whose constants name chains). The
coverage matrix in PLAN.md documents chains; it dispatches nothing.
- **Speed-first, like `bridge`.** The ratio is a compile-time type, so every trip count and
schedule is a compile-time fact; the stage factoring is chosen by MACs on the measured lengths
Expand Down
16 changes: 16 additions & 0 deletions rational/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ else()
option(TAP_SR_BUILD_TESTS "Build the rational engine's tests" OFF)
endif()

# C ABI shared library for FFI consumers (the notebooks drive the shipping
# C++ through it — notebooks/matrix.ipynb).
option(TAP_SR_BUILD_CAPI "Build the C ABI shared library" OFF)

# Fixed deterministic workloads for the QEMU instruction-count ratchet;
# buildable for any target including bare metal (PLAN.md section 5).
option(TAP_SR_BUILD_ICOUNT_BENCH "Build instruction-count ratchet workloads" OFF)

# Warning flags for this engine's own tests; never exported to consumers of
# the INTERFACE library.
option(TAP_SR_RATIONAL_WERROR "Treat warnings as errors in the rational engine's own targets" OFF)
Expand Down Expand Up @@ -66,3 +74,11 @@ if(TAP_SR_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()

if(TAP_SR_BUILD_CAPI)
add_subdirectory(capi)
endif()

if(TAP_SR_BUILD_ICOUNT_BENCH)
add_subdirectory(bench/icount)
endif()
Loading
Loading