Skip to content

A11a: the ALSA bridge becomes an instrument, with its report - #94

Merged
tap merged 1 commit into
mainfrom
claude/sample-rate-solutions-comparison-mqc190
Oct 15, 2026
Merged

tap merged 1 commit into
mainfrom
claude/sample-rate-solutions-comparison-mqc190

Conversation

@tap

@tap tap commented Oct 15, 2026

Copy link
Copy Markdown
Owner

What this changes

This is the first of three PRs for the roadmap's A11, the harness Block B's bench runs. It changes the ALSA bridge (async/examples/alsa_bridge.cpp), adds a CSV report script, and makes both CI-proven on Linux.

The bridge:

  • hw: devices only. --in and --out are now required, and a plugin device (default, plughw:, dmix) is refused before anything is opened.
  • Per-device counters. Each audio thread counts its cumulative frames and its ALSA xruns (-EPIPE), kept apart from its other recoveries.
  • Hardware timestamps. Both devices timestamp every pointer update on CLOCK_MONOTONIC_RAW. After each transfer the thread publishes the last one (snd_pcm_htimestamp) as the frames the hardware had moved at that instant, through a sequence counter. A driver without timestamps leaves those fields empty.
  • --csv now writes rather than appends. Each per-second row holds:
    • status();
    • CLOCK_MONOTONIC_RAW;
    • both devices' frame counts, xruns and other recoveries;
    • the hardware pairs.
  • --rt runs both audio threads SCHED_FIFO with mlockall, and stops if the system refuses either.
  • The file is now clang-tidy-clean: braces throughout, and the two conversion helpers renamed to snake_case.

examples/alsa_bridge_report.py (standard library; --plot uses matplotlib) reads that CSV:

  • A least-squares line through each device's frames against time gives that crystal's rate, from the thread counts and from the hardware pairs. Their ratio is the true offset in ppm.
  • It compares that offset with the servo's mean while locked.
  • It reports the ppm wobble, the FIFO's breathing, and the final discontinuity counts.

CI: the Linux build-and-test legs and the clang-tidy job install libasound2-dev.

Why

Block B's verdict on async (row P8) is "clock recovery proven on independent crystals". The bridge reported the servo's own estimate, but nothing measured the crystals independently to check it. It would also silently accept a plugin device that resamples (on a Pi, default is PipeWire), and then measure the wrong clock.

Verification

  • --self-test builds a synthetic hour with an 80 ppm offset. The thread counts recover 80.025 ppm and the hardware pairs 80.000 ppm, and the wobble and breathing match the injected noise.
  • Two tests run without hardware:
    • async.Examples.AlsaBridgeReportSelfTest runs the self-test wherever Python is.
    • async.Examples.AlsaBridgeRefusesPluginDevices checks that --in default is refused.
  • Locally, with libasound2-dev installed:
    • clang 18 -Werror and GCC 13 build the bridge without a warning;
    • clang-tidy is clean;
    • the full clang suite passes, 329 of 329.
  • I could not run the bridge itself: there are no audio devices here. Its first real run is Block B's.

Notes for the reviewer

  • Behaviour change: the bridge no longer defaults to "default"; --in/--out must be hw: devices. The book's commands already used hw:1,0/hw:2,0.
  • A11b (pico2_cyccnt gains bridge and rational cases and copy_to_ram) and A11c (the UAC2 asynchronous-mode firmware) follow as their own PRs.

🤖 Generated with Claude Code

https://claude.ai/code/session_016GKzq1VSdcMYCRMhddiMaf


Generated by Claude Code

The first part of the roadmap's A11, the harness Block B's bench runs.

examples/alsa_bridge.cpp:
- Only hw: devices. --in and --out are required, and a plugin device
  ("default", plughw:, dmix) is refused before anything is opened. On a
  Pi, "default" routes through PipeWire and its resampler, and the
  bridge would measure that converter's clock, not the crystal's.
- Per-device counters. Each audio thread counts the frames it moves and
  its ALSA xruns (-EPIPE), kept apart from its other recoveries.
- Hardware timestamps. Both devices timestamp every pointer update on
  CLOCK_MONOTONIC_RAW. After each transfer the thread publishes the last
  update (snd_pcm_htimestamp) as the frames the hardware had moved at
  that instant, through a sequence counter, so the main thread never
  pairs a position with another update's time. A driver without
  timestamps leaves the fields empty.
- --csv writes, no longer appends, the per-second record: status(),
  CLOCK_MONOTONIC_RAW, both frame counts, xruns, other recoveries and
  the hardware pairs.
- --rt runs both audio threads SCHED_FIFO (priority 80) with every page
  locked (mlockall), and stops the run if the system refuses either.
- The file is now clang-tidy-clean: braces throughout, and the two
  conversion helpers in snake_case.

examples/alsa_bridge_report.py (standard library; --plot uses matplotlib)
reads that CSV:
- A least-squares line through each device's frames against time gives
  each crystal's rate, from the thread counts and, where present, from
  the hardware pairs. Their ratio is the true offset in ppm.
- It compares that offset with the servo's mean while locked.
- It states the ppm wobble and the FIFO's breathing (standard deviation
  and peak to peak) and the final underrun, overrun, resync and xrun
  counts.
- --self-test checks the arithmetic on a synthetic hour with an 80 ppm
  offset. The thread counts recover 80.025 ppm and the hardware pairs
  80.000; wobble and breathing match the injected noise.

CI:
- The Linux build-and-test legs and the clang-tidy job install
  libasound2-dev, so the bridge is built and tidy-gated on every PR.
- Two tests run without hardware: AlsaBridgeRefusesPluginDevices (built
  with the bridge) and AlsaBridgeReportSelfTest (wherever Python is).

HARDWARE_TESTING.md, the book's hardware chapter and the roadmap describe
it; A11b (pico2_cyccnt cases) and A11c (the UAC2 firmware) follow.

Locally, with ALSA installed: clang 18 -Werror and GCC 13 build the bridge
without a warning, clang-tidy is clean, and the full clang suite passes,
329 of 329, the two new tests included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GKzq1VSdcMYCRMhddiMaf
@tap
tap merged commit 1b34642 into main Oct 15, 2026
32 checks passed
@tap
tap deleted the claude/sample-rate-solutions-comparison-mqc190 branch October 15, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants