virtio-snd: cpal/CoreAudio host backend, MSB_SND=1 wiring (macOS) - #4
virtio-snd: cpal/CoreAudio host backend, MSB_SND=1 wiring (macOS)#4ya-luotao wants to merge 2 commits into
Conversation
The vendored virtio-snd device only had a PipeWire host backend, and the `pw` crate is Linux-only in practice (no macOS bottle, no CoreAudio sink), so `snd` could not even be compiled into the runtime on macOS. devices: add `audio_backends/cpal.rs`, a playback backend built on cpal (CoreAudio on macOS). It opens one host output stream per prepared virtio stream and, from the host audio callback, decodes the guest's queued TX buffers (U8/S16/S24/S32 little endian) into the host sample format, padding with silence on underrun. Dropping a consumed buffer is what completes the guest's request, so the host audio clock paces the guest. It asks cpal for the guest's own rate and channel count, and falls back to linear interpolation only when the device cannot take that rate. Capture is deliberately not implemented: `read` warns once. `BackendType` now picks the backend per target (PipeWire on Linux, cpal elsewhere), `pw` moved under the Linux target table and `cpal` under the macOS one, and the worker answers a failing backend call with the proper virtio status instead of `unwrap()`ing it — a panic there takes down the thread that drives every queue of the device. For the same reason the render loop clamps how much it drains from its source buffer: at a host rate below half the guest's, one output frame can step past everything the callback managed to pull. The unadapted mock-vring tests in `stream.rs` are dropped: they were written against `vhost_user_backend`/`virtio_queue::mock` and a different `IOMessage`, so the crate's test target did not compile at all. runtime: enable `msb_krun/snd` next to `gpu`/`input`, and attach the device when `MSB_SND=1` (env var only, no CLI flag yet). Verified on the `snd-test` sandbox (msb-omarchy:dev, HVF, --init auto): `/proc/asound/cards` shows `0 [SoundCard]: virtio-snd - VirtIO SoundCard at platform/a010000.virtio_mmio/virtio13`, dmesg reports it via ALSA, and the guest's PipeWire exposes it as a stereo sink. Playing a 440 Hz tone with `pw-play` logs, with MSB_SND_STATS=1: virtio-snd cpal: stream 0 -> "MacBook Pro Speakers": guest 48000 Hz 2 ch S32, host 48000 Hz 2 ch F32 virtio-snd cpal: stream 0: 48001 frames/s to the host device (0 silence frames in 1.00s) 40 stats lines across four playbacks, no underruns, no errors, and the guest PCM closes cleanly. The listening test itself is still unverified.
`msb_krun/snd` was in the unconditional feature list, which made every
Linux build of this branch pull the `pw` (PipeWire) crate and need the
PipeWire client library at compile time. The only host backend that
exists is cpal/CoreAudio, so nothing on Linux gained from it.
Move the feature into a `cfg(target_os = "macos")` dependency table —
Cargo activates a target-specific dependency's features only when
building for that target — and cfg the `MSB_SND` wiring in `vm.rs` to
match. On other Unix targets `MSB_SND=1` now logs one warning saying the
device is macOS-only in this build instead of being silently ignored.
Feature resolution, `cargo tree -e features -i msb_krun -p
microsandbox-runtime`:
--target x86_64-unknown-linux-gnu: blk, default, gpu, input,
krun_display, krun_input, net
host (aarch64-apple-darwin): the same, plus "snd"
and `cargo tree --target x86_64-unknown-linux-gnu -p microsandbox-cli`
lists no pipewire and no cpal crate at all. The macOS build and
`--locked` are unaffected; Cargo.lock does not change.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3c7bd6ab8f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "virtio-snd cpal: capture is not implemented; guest recording from stream \ | ||
| {stream_id} will read silence and its buffers stay pending" | ||
| ); | ||
| } | ||
| return Ok(()); |
There was a problem hiding this comment.
Reject capture instead of leaving RX requests pending
When a guest opens the advertised input stream, PREPARE returns success here and START also succeeds, but no cpal input callback ever drains its RX buffers; a normal blocking recorder therefore hangs indefinitely rather than receiving silence or an error. Either stop advertising the input PCM or return an explicit unsupported status and complete submitted requests.
AGENTS.md reference: AGENTS.md:L100-L100
Useful? React with 👍 / 👎.
| if request.channels == 0 { | ||
| return Err(Error::ChannelNotSupported(request.channels)); |
There was a problem hiding this comment.
Reject channel counts above the advertised maximum
When a guest supplies 7–255 channels, this check accepts the request even though PCM_INFO advertises a maximum of 6. open_output then falls back to a host configuration while Playback treats each claimed channel group as a frame, corrupting the audio and silently discarding most channels; validate the request against the stream's channels_min and channels_max at this guest boundary.
AGENTS.md reference: AGENTS.md:L98-L98
Useful? React with 👍 / 👎.
| let host_streams = self.host_streams.read().map_err(lock_poisoned)?; | ||
| match host_streams.get(&stream_id) { | ||
| Some(host) => host.play().map_err(|e| { | ||
| Error::UnexpectedAudioBackendError(format!("could not start output stream: {e}")) | ||
| }), |
There was a problem hiding this comment.
Roll back Start state when CoreAudio refuses playback
If the output device disappears or otherwise makes host.play() fail after PREPARE, the code has already changed the virtio stream state to Start. The worker returns IO_ERR, but a retry of START becomes BAD_MSG and RELEASE is invalid from Start, preventing normal recovery and cleanup; commit the state transition only after play() succeeds or restore the previous state on error.
Useful? React with 👍 / 👎.
Independent of the display stack (#1/#2/#3); based on
gpu-m0. M3 candidate for msb-omarchy: audio out of the microVM on a Mac.The vendored virtio-snd device only had a PipeWire host backend, which is Linux-only in practice (Homebrew's
pipewirehas no macOS bottle and PipeWire has no CoreAudio sink), sosndcould not even be compiled into the runtime on macOS.third_party/msb_krun_devices: newaudio_backends/cpal.rs, a playback backend on cpal 0.18.2 (CoreAudio on macOS). One host output stream per prepared virtio stream; the audio callback decodes the guest's queued TX buffers (U8/S16/S24/S32 LE) into the host format, pads with silence on underrun, and completes the guest's requests as it consumes them, so the host audio clock paces the guest. Asks for the guest's own rate/channels; linear interpolation only when the device cannot take that rate. Capture is deliberately not implemented (readwarns once).BackendTypepicks PipeWire on Linux and cpal on macOS;pw/cpalare target-gated optional deps. The worker now answers a failing backend call with the properVIRTIO_SND_S_*status instead ofunwrap()ing — a panic there takes down the thread that drives every queue of the device. The crate'sstream.rstest module (written againstvhost_user_backend/virtio_queue::mock) never compiled; its unadapted tests are dropped socargo test --features sndruns (81 tests).crates/runtime:msb_krun/sndis enabled from a[target.'cfg(target_os = "macos")'.dependencies]table only, so Linux builds do not need libpipewire (cargo tree --target x86_64-unknown-linux-gnu -p microsandbox-cli | grep -c pipewire→ 0).MSB_SND=1attaches the device (ConsoleBuilder::sound(true)); env var only, no CLI flag yet. On non-macOS the variable logs a warning once.MSB_SND_STATS=1logs frames/s per stream per second.Verified on a
snd-testsandbox (msb-omarchy:dev, HVF,--init auto):/proc/asound/cards→0 [SoundCard]: virtio-snd - VirtIO SoundCard at platform/a010000.virtio_mmio/virtio13, novirtio_snderrors in dmesg, the guest's PipeWire exposes a stereo sink, andpw-playof a 440 Hz tone logsstream 0 -> "MacBook Pro Speakers": guest 48000 Hz 2 ch S32, host 48000 Hz 2 ch F32then48001 frames/s to the host device (0 silence frames in 1.00s)— 50 stats windows over five playbacks, zero underruns, zero errors, PCM closes cleanly. No eventfd/worker-thread problems on HVF (the libkrun superradcompany#116 fix is already vendored). The listening test itself is left to a human:The device patch is self-contained in
third_party/msb_krun_devicesso it can become an upstream libkrun PR later (not opened).🤖 Generated with Claude Code
https://claude.ai/code/session_01PN7mepn7ryjXupoHbQjFmR