Skip to content

virtio-snd: cpal/CoreAudio host backend, MSB_SND=1 wiring (macOS) - #4

Open
ya-luotao wants to merge 2 commits into
gpu-m0from
virtio-snd
Open

virtio-snd: cpal/CoreAudio host backend, MSB_SND=1 wiring (macOS)#4
ya-luotao wants to merge 2 commits into
gpu-m0from
virtio-snd

Conversation

@ya-luotao

Copy link
Copy Markdown
Owner

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 pipewire has no macOS bottle and PipeWire has no CoreAudio sink), so snd could not even be compiled into the runtime on macOS.

  • third_party/msb_krun_devices: new audio_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 (read warns once). BackendType picks PipeWire on Linux and cpal on macOS; pw/cpal are target-gated optional deps. The worker now answers a failing backend call with the proper VIRTIO_SND_S_* status instead of unwrap()ing — a panic there takes down the thread that drives every queue of the device. The crate's stream.rs test module (written against vhost_user_backend/virtio_queue::mock) never compiled; its unadapted tests are dropped so cargo test --features snd runs (81 tests).
  • crates/runtime: msb_krun/snd is 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=1 attaches the device (ConsoleBuilder::sound(true)); env var only, no CLI flag yet. On non-macOS the variable logs a warning once.
  • MSB_SND_STATS=1 logs frames/s per stream per second.

Verified on a snd-test sandbox (msb-omarchy:dev, HVF, --init auto): /proc/asound/cards0 [SoundCard]: virtio-snd - VirtIO SoundCard at platform/a010000.virtio_mmio/virtio13, no virtio_snd errors in dmesg, the guest's PipeWire exposes a stereo sink, and pw-play of a 440 Hz tone logs stream 0 -> "MacBook Pro Speakers": guest 48000 Hz 2 ch S32, host 48000 Hz 2 ch F32 then 48001 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:

MSB_SND=1 MSB_SND_STATS=1 MSB_GPU=1 MSB_GPU_DISPLAY=1920x1080 msb run --name snd-test --replace --init auto --cpus 2 --memory 2G --root-disk 16G msb-omarchy:dev -- sleep infinity
msb exec snd-test -- ffmpeg -y -v error -f lavfi -i sine=frequency=440:duration=5 -ar 48000 -ac 2 /tmp/tone.wav
msb exec snd-test -- runuser -l omarchy -c "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.8"
msb exec snd-test -- runuser -l omarchy -c "pw-play /tmp/tone.wav"

The device patch is self-contained in third_party/msb_krun_devices so it can become an upstream libkrun PR later (not opened).

🤖 Generated with Claude Code

https://claude.ai/code/session_01PN7mepn7ryjXupoHbQjFmR

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.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-30T16:38:58.955927Z 3c7bd6a PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +609 to +613
"virtio-snd cpal: capture is not implemented; guest recording from stream \
{stream_id} will read silence and its buffers stay pending"
);
}
return Ok(());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +571 to +572
if request.channels == 0 {
return Err(Error::ChannelNotSupported(request.channels));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +667 to +671
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}"))
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

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.

1 participant