Repository navigation
host_audio: resample every card with one Catmull-Rom resampler - #692
Open
jamesbraid wants to merge 1 commit into
Open
jamesbraid wants to merge 1 commit into
jamesbraid wants to merge 1 commit into
Conversation
The host output had two copies of the same Catmull-Rom resampler: `Resampler`, stereo and writing into the host ring, for HAL2 and RAD1; and `FrameResampler`, up to eight channels and appending to a vector, for the ICE1724. MACE used the stereo one but wanted its output back to mix two outputs by position, so it built a temporary ring on every pass just to read the frames out again. Fixing the interpolation or the refused-frame rule meant fixing it twice, and the next change (one link from a card to its host stream) needs a single resampler type to hold. There is now one `Resampler`, for 1 to 8 channels, that hands each output frame to a caller-supplied closure. `HostOut::push` writes them to the ring and counts the refusals, as before; MACE adds them into its mix by position (the shorter output still plays alone, so pad by position stays); the ICE1724 pushes each frame as it is made, which drops its intermediate vector. No behaviour changes: the f32 operations, the exact rational step and the whole-frame refusal rule are the same. Evidence that nothing changed. Before removing either type, a test drove each card's real path with fixed signals (a 1 kHz and 440 Hz tone, a full-scale square pair, pseudo-random noise over the whole 16-bit range), at every guest rate 8000, 11025, 22050, 32000, 44100 and 48000 to host 48000 and 44100, a frame at a time (HAL2) and in bursts of 1 to 1024 (RAD1, MACE, ICE1724), into a roomy ring and into a 41-slot ring that refuses most bursts. The same test passes on the merged code, and sha256 of the bytes that reached the ring (plus the refused-frame count) is identical for all twelve runs: hal2-one/free dbb43912...803e hal2-one/tight 99b0d40e...d567 hal2-burst/free dbb43912...803e hal2-burst/tight 72024f6f...70e8 rad1-sink/free dbb43912...803e rad1-sink/tight 72024f6f...70e8 mace/free a3f6ddec...ecd7 mace/tight e850e9d2...3626 ice1724-2ch/free 6e667673...a644 ice1724-2ch/tight 2ac4a647...e7f3 ice1724-8ch/free 56a1a9b5...30a7 ice1724-8ch/tight 914f935a...46da (Full digests are in the PR description. The test holds the same bytes as blake3 digests; it fails if the interpolation, the carried phase, the sample history or the refusal count is changed.) The free runs of HAL2 one frame at a time, HAL2 in bursts and RAD1 agree with each other, so the resampler is also independent of how frames are pushed. HAL2 pushes one frame at a time, so its cost per frame is measured with an ignored benchmark (`cargo test --release -p iris --lib -- --ignored hal2_push_cost --nocapture`), five A B B A pairs on x86-64, in ns per input frame: 44.1 to 48 kHz 32.4 before, 27.7 after; 11.025 to 48 kHz 69.5 before, 79.8 after; 8 to 48 kHz 82.7 before, 106.1 after. A low guest rate makes more output frames per input frame and each costs a few ns more through a slice of run-time length, at most 0.03% of a core. arm64 is unmeasured. HAL2's resampler tests move to host_audio next to the type.
jamesbraid
force-pushed
the
audio/resampler
branch
from
November 13, 2026 04:50
ea21404 to
67a19bc
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.
The host output had two copies of the same Catmull-Rom resampler:
Resampler(stereo, writing into the host ring; HAL2 and RAD1) andFrameResampler(up to eight channels, appending to a vector; the ICE1724). MACE used the stereo one but wanted the frames back to mix its two outputs by position, so every pass built a temporary ring just to read them out again. A fix to the interpolation or the refused-frame rule had to be made twice, and the next PR in this stack (one link from a card to its host stream) needs a single resampler type to hold.This replaces both with one
Resamplerfor 1 to 8 channels. It calls a closure with each output frame it completes.HostOut::pushwrites them to the ring and counts refusals as before; MACE adds them into its mix by position; the ICE1724 pushes each frame as it is made, so its intermediate vector goes too. Nothing a guest or the host hears changes.What changes
One commit,
host_audio: resample every card with one Catmull-Rom resampler:Resampler::new(in, out, channels)andpush(frame, emit)replace both types;HostOut::pushtakes a frame slice.host_audio.rsnext to the type.What to check
MaceAudio::play:mixed[k]is added by output position, as before.Verified
Golden hashes, taken from the old code before either type was removed. The test drives each card's real path (HAL2
push_to_ring, RAD1Sink::play, MACEplaywith two outputs at different rates and passes where one is empty, ICE1724PcmOut::flushat 2 channels with S/PDIF mixed in and at 8) with a 1 kHz and 440 Hz tone pair, a full-scale square pair and pseudo-random noise, at guest rates 8000, 11025, 22050, 32000, 44100, 48000 to host 48000 and 44100, a frame at a time (HAL2) or in bursts of 1 to 1024, into a roomy ring ("free") and a 41-slot ring that refuses most bursts ("tight"). sha256 of the bytes that reached the ring and the refused-frame count, old code and new code identical for all twelve:dbb439122cb684f91f582ae751a9b8ee8b1cb14e3c86ad326e6ee44f52e2803e99b0d40ef86a0da5988a87caf4f89ee03c6699c721c814b5da575732fc02d567dbb439122cb684f91f582ae751a9b8ee8b1cb14e3c86ad326e6ee44f52e2803e72024f6f0d5f77794ec696455e44efd569db2e9a45f406df1e5260820b0170e8dbb439122cb684f91f582ae751a9b8ee8b1cb14e3c86ad326e6ee44f52e2803e72024f6f0d5f77794ec696455e44efd569db2e9a45f406df1e5260820b0170e8a3f6ddecab21d8798fe100654765f3d127de6abdf299a4c384ec30f61187ecd7e850e9d2d67178d3e6ac828f8f644ffbe362df5853570a2004ea58e97a2836266e6676739e2bd1a0a51eef18bbee05127316306996b6587f9787a84d7cb8a6442ac4a64719dd8e81e97653c34b81fede7acfab52a5c3887fea48fe8e37dae7f356a1a9b5f3017dc9ab41fe043f5402da5b79e827c2db1419bd15aa7ec8ca30a7914f935a9af7ede6e7aa8876ce2fa795246561e8a68537976db468822f4746daThe committed test holds the same bytes as blake3 digests; the sha256 values above were taken by dumping the same bytes. Mutation checks, each failing the golden tests: interpolation coefficient changed, phase accumulator reset instead of carried, one channel's history not shifted, refused frames not counted; MACE add-by-position broken fails the MACE test. The HAL2 one-frame, HAL2 burst and RAD1 free runs hash the same, so output is also independent of push size.
cargo test --release -p iris --lib -- --ignored hal2_push_cost --nocapture), x86-64, release, five A B B A pairs, ns per input frame, before to after: 44.1 to 48 kHz 32.4 to 27.7; 11.025 to 48 kHz 69.5 to 79.8; 8 to 48 kHz 82.7 to 106.1. At most 0.03% of a core. arm64 not measured.[audio] file, headless release builds, before and after: Indy (embedded PROM): the sounding span of the WAV is byte-identical (200596 words, same sha256). Octane (6.5 PROM): the pre-resampleIRIS_AUDIO_DUMPis identical; the WAV holds the same 91261 non-silent frames in the same order (identical sha256), and differs only in where the host's real-time player inserted silence on an underrun, which follows wall-clock time.cargo check --tests -p iris, default and--features jitv2;cargo test -p iris --libfor host_audio, host_pcm, hal2, mace_audio, rad1, ice1724, audio_dump and monitor, default and--features developer;cargo test -p iris --test snapshot_schemas. All pass. No resampler state is saved or loaded.Not run: arm64, Windows and macOS (CI); real-device loopback tone tests; the Indigo2, O2 and Tezro chimes on a guest; the full workspace tests (CI).