Repository navigation
audio: test what each card sends to the host, end to end - #617
Open
jamesbraid wants to merge 2 commits into
Open
jamesbraid wants to merge 2 commits into
jamesbraid wants to merge 2 commits into
Conversation
jamesbraid
marked this pull request as ready for review
November 12, 2026 02:26
jamesbraid
force-pushed
the
ci/audio-real-device
branch
from
November 12, 2026 06:17
0bf6946 to
5f98e6c
Compare
jamesbraid
changed the base branch from
audio/host-audio
to
audio/onyx2-mediaio
November 12, 2026 06:17
jamesbraid
force-pushed
the
ci/audio-real-device
branch
from
November 12, 2026 09:06
5f98e6c to
88f416a
Compare
jamesbraid
force-pushed
the
ci/audio-real-device
branch
from
November 12, 2026 09:12
88f416a to
f0ff4dc
Compare
jamesbraid
changed the base branch from
audio/onyx2-mediaio
to
audio/file-backend
November 12, 2026 09:12
jamesbraid
force-pushed
the
ci/audio-real-device
branch
from
November 12, 2026 09:44
f0ff4dc to
95d2c9e
Compare
The cards had tests of their registers and of the frames they handed a test tap. None ran the way a machine does: the clock was set by hand, the host output was put in place by the test, nothing exercised Device::start opening the stream or the card's own timer moving the samples, and none stopped the producer to run the host stream dry and check what came after. A card could pass all of its tests and play nothing, or come back from an underrun a sample late, which swaps its channels for good. HAL2, O2 audio and RAD1 each get a test that builds the card as a machine does: on the host's clock, with the machine's timers, its output opened through enable_output and Device::start into an `[audio] file`. It plays a tone from memory laid out the way the driver lays it out (HPC3 words for HAL2, the O2's big-endian ring kept full, RAD1's output ring), with no register access while it plays. The capture, less the underrun silence, must be the guest's samples in order with each channel on its own side. The clock must have moved at least half the frames the wall time allows: a busy host holds a timer back. For RAD1 and the O2 the mute word must silence what follows. Where the card's rate is 48 kHz (HAL2, O2) nothing is resampled and the comparison is exact; RAD1 at 44.1 kHz is checked by frequency and level. The same three cards and the bare host output each get a second test that plays a tone with a different frequency on each channel, stops the producer for 0.3 s (the guest falls behind for HAL2, the guest stops the output for the others) and plays again. On both sides of the gap each channel must keep its frequency within 1%, reach half the tone's level, and last no more than 110% of what the card played and at least half of it. A tighter lower bound failed one run in three on a loaded host: the card's service pass caught up in a burst that overflowed the host ring. The card's underrun counter must move during the gap. Each runs on both backends: the `[audio] file`, and the host's real default output recorded back from the loopback IRIS_AUDIO_LOOPBACK names (`<output host>:<PulseAudio source>`, for example `alsa:iris_sink.monitor`), which sees cpal, the sound server and its mixer. With the variable unset a real-device test prints SKIPPED and passes, so plain `cargo test` stays green. With it set, a missing source or silence fails. The recording runs `parec`: cpal's own PulseAudio record stream did not start until a default fragment of about two seconds had filled. The real-device tests run one at a time, in a nextest group and behind a lock, since two cards on one output would mix. Each of these deliberate breaks makes at least one test fail: a clock that stands still; RAD1's and HAL2's samples zeroed; channels swapped where frames enter the host ring; the O2's channels swapped; RAD1's and the O2's mute ignored; the file output never draining or failing to open; resuming the consumer one sample late after an underrun (seven of the eight underrun tests); a HAL2 guest resuming one word late (both HAL2 underrun tests, 880 Hz on the left after the pause). In a container with a PulseAudio 17 null sink all four underrun tests pass through the ALSA host.
The hosted runners have no sound device, so the real-device audio tests skip in the test run, and a card that plays nothing through cpal and the sound server stays green. Setting up a null sink by hand is also easy to get subtly wrong: cpal's PulseAudio client finds the server only through PULSE_SERVER or XDG_RUNTIME_DIR, which a container may not set, and parec loses what it buffered when a signal stops it. A loopback that records only silence would look exactly like the bug the tests look for. scripts/audio-loopback-test.sh starts a PulseAudio server if none answers, makes a null sink the default output and its monitor the default input, names the server's socket, and checks that a second of noise played into the sink comes back from the monitor before it runs anything. It then runs the given command once per output host in IRIS_AUDIO_HOSTS: ALSA through the pulse plugin by default. The Test job on x86-64 Linux installs PulseAudio, runs the script and runs those tests against the null sink, reusing the run's build and its feature set. The step gates. arm64 and the other platforms are left out until a run has shown the setup works there. TESTING.md says how to run the same thing locally and what the tests do not cover.
jamesbraid
force-pushed
the
ci/audio-real-device
branch
from
November 12, 2026 14:49
95d2c9e to
d6d11c8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Nothing in CI hears what a sound card sends to the host, so a card that plays nothing, or comes back from an underrun a sample late (which swaps its channels for good), stays green. This tests every card through the
[audio] filebackend from the PR below, and runs the real output path in CI on Linux.Two commits:
IRIS_AUDIO_LOOPBACK(SKIPPED when unset). The real-device tests run one at a time: areal-audionextest group, a lock under plain cargo test. main's nextest profiles are untouched.scripts/audio-loopback-test.shsets up a PulseAudio null sink and checks it before running anything, and the Linux x86-64 Test job runs the real-device tests against it (ALSA host, same build and${{ matrix.features }}as the main run). TESTING.md says how to run it and what it does not cover.The PulseAudio host stalled on a dev build with unoptimised dependencies; main's opt-level = 1 for dependencies avoids it (root cause: the pulseaudio crate zero-fills 1 MiB per socket read: 19 ms at opt-level 0, 0.06 ms at opt-level 3). The PulseAudio host stays out of the required CI step until the roughly 2-in-85 intermittent underrun seen at opt-level 3 is re-measured at opt-level 1.
Verification: with
IRIS_AUDIO_LOOPBACKunset the four real-device tests print SKIPPED and pass; in a container with a PulseAudio 17 null sink they pass through the ALSA host. Deliberate breaks (stopped clock, zeroed samples, swapped channels, ignored mute, never-draining file, resuming one sample late) failed them on the original build of these commits and were not repeated after the rebase. The CI step has not run on a hosted runner.