fangorn/sunshine-qemu
public
Pull requests
Links
Phase 1b: guest audio out
Parent: #1. Depends on #2 (QemuSession). Can run in parallel with #5.
Requirements
- REQ-AUD-001: guest audio output is streamed to Moonlight
Out of scope: client microphone → guest. Sunshine and Moonlight have no client→host mic path (Sunshine’s mic_t is host audio capture). REQ-AUD-002 is deprecated. If Moonlight or Sunshine add mic upstream later, open a new issue.
Background (verified)
- QEMU:
-audiodev dbus,id=snd0plus an audio device (intel-hda+hda-output,audiodev=snd0, orvirtio-sound-pci,audiodev=snd0), and-display dbus,audiodev=snd0. Our client callsorg.qemu.Display1.Audio.RegisterOutListener(h)on/org/qemu/Display1/Audiowith a socketpair fd (same pattern as the console listener) and exportsorg.qemu.Display1.AudioOutListenerat/org/qemu/Display1/AudioOutListener. - Listener methods:
Init(id, bits, is_signed, is_float, freq, nchannels, bytes_per_frame, bytes_per_second, be),Write(id, ay data),SetEnabled(id, b),SetVolume(id, mute, ay per-channel 0-255),Fini(id). There can be several streams (ids). TheNSamplesproperty is the frame size (default 480, 10 ms at 48 kHz). - Sunshine:
src/audio.cpp:320callsplatf::audio_control(), thencontrol->microphone(mapping, channels, sample_rate, frame_size, continuous, host_audio)(common.h:840), which returns amic_twhosesample(std::vector<float>&)must block until a full frame of interleaved float PCM is ready. The Linux impl is PulseAudio insrc/platform/linux/audio.cpp.
Tasks
- Routing: when
capture == qemu,platf::audio_control()returnsqemu::audio_control_tinstead of the PulseAudio one.set_sink/sink_infobecome no-ops that return a synthetic sink so the rest ofaudio.cppworks unchanged. Keep the change toaudio.cppminimal; ideally none. -
qemu::mic_t: a ring buffer fed fromWriteon the GMainContext thread and drained bysample().- Convert any PCM format from
Init(s16/s32/f32, LE/BE) to float. - Resample to Sunshine’s requested
sample_rateif it differs (QEMU is usually 44.1 or 48 kHz; use FFmpeg swresample, already linked). - Remix channels to the requested
mapping(stereo, 5.1, 7.1). - Mix multiple stream ids if present.
- Apply
SetVolume/mute. - On underrun, return silence when
continuousis set, otherwise wait (match the PulseAudio impl’s semantics).
- Convert any PCM format from
- Bounded buffer with drop-oldest on overflow, so a slow client can’t make latency grow without limit. Log overflow at debug level.
- Clean up on
Finiand when the VM disconnects.
Tests
- Unit: fake QEMU sends
Init(s16le 44100 Hz stereo) andWritewith a known sine; assertsample()returns float 48 kHz frames with the expected frequency, RMS and channel layout. Also cover volume/mute, multiple streams, underrun, and overflow. - E2E: guest plays a known tone (e.g.
speaker-test -t sine -f 1000in the Linux test guest). The test client decodes Opus and asserts the dominant frequency is 1000 Hz ± tolerance.
Done when
REQ-AUD-001 is linked to passing unit and E2E tests, audio works through a real Moonlight client (manual check, noted in the PR), and anvil requirement status passes. PR Closes #4.
Progress on phase-1 (commits ba2a8a97..97389de1):
- With capture = qemu, platf::audio_control() returns a QEMU audio control. Its microphones read an AudioOutListener shared per VM session. PCM of any QEMU layout is converted, volume and mute are applied, the audio is resampled to 48 kHz and remixed to stereo, 5.1 or 7.1. Guest streams are mixed. A bounded 200 ms buffer drops the oldest audio. On underrun, sample() returns silence with continuous audio and times out without it. Fini, disconnect and reconnect are handled.
- Deviation: FFmpeg swresample is not linked: the build-deps FFmpeg has CONFIG_SWRESAMPLE 0. A windowed-sinc resampler is used instead, with unit tests for frequency, level, continuity and anti-aliasing.
- 27 unit tests (mixer plus fake QEMU over D-Bus) are linked to REQ-AUD-001, along with e2e_stream.sh.
- E2E_AUDIO=1 uses the Linux test guest’s 1000 Hz tone. The client decodes Opus and measured 999.997 Hz on both channels with 0 silent blocks. It passes on QEMU 11.1.1 and 8.2.2 (Debug, software encoding) and with Release plus NVENC. E2E_SESSIONS=3 passes. The pre-change build fails the same test with 0 Hz (silence).
- run_vm.sh now passes -display dbus,…,audiodev=snd0. QEMU needs it to export the Audio object.
- anvil requirement status passes.
Still open: a manual check with a real Moonlight client (not possible on this host), and a multichannel guest stream end to end.