Skip to content

Record a Windows game's sound in a VM that has no sound card ​

What to know ​

A game under Wine in a VM can play into a sound server instead of a device, so QEMU needs no sound card (adding one means a restart, and kernel drivers that a cloud kernel may lack). Inside the guest, start PulseAudio with only a null sink at the game's own format and record its monitor:

sh
pulseaudio -D -n --exit-idle-time=-1 --load=module-native-protocol-unix \
  --load="module-null-sink sink_name=rec rate=22050 format=s16le channels=2" --load=module-always-sink
parec -d rec.monitor --rate=22050 --format=s16le --channels=2 --raw out.raw

The 32-bit game needs the i386 ALSA and Pulse libraries (libasound2-plugins:i386; Wine's libpulse0:i386 is usually there). Check with pactl list sink-inputs that the game's stream has the sink's rate, so nothing is resampled. In POD the game then starts without its "no sound driver" question.

What the recording is good for: levels, pans, which sound plays when. In POD the menu sound came out with a gain of 1.00006 against the port and 94% of samples within ±1. What it is not good for: sample-for-sample checks. Wine's audio path adds ±1 noise (inferred: it converts to floating point and back), and an emulated PC (QEMU's TCG) misses deadlines, leaving gaps of silence in the recording; in POD half of a race's sound was lost until Wine got a second of PulseAudio buffer (PULSE_LATENCY_MSEC=1000), after which none was. Line the recording up with the port window by window (measure/sound_recorded.py), not once for the whole sound, and keep the emulator's measurement of the mixer as the exact evidence.

Debian enables a systemd user socket, pulseaudio.socket, that starts a default daemon (a 44,100 Hz sink) whenever a client connects and none runs; stop and mask it, and set autospawn = no, or the game's sound is quietly resampled into the wrong sink.

Two traps on a VM shared by several sessions: pgrep -x Xvfb finds another session's display and a start script then skips starting its own; and pkill -f "Xvfb :1" run over SSH matches the SSH command itself. Match the process name with pgrep -a -x and filter on the display.

CD audio is a separate problem: QEMU's CD drive takes a plain ISO, so an image's audio tracks never reach the guest, and the game's MCI calls play nothing. Inside the guest, CDEmu fixes it: vhba-dkms (a virtual SCSI host, built for the running kernel) and cdemu-daemon -a pulse load the whole CUE/BIN into a drive that reports the audio tracks and plays them. Wine's MCI cdaudio then plays them digitally (raw sector reads, out through DirectSound), so the music arrives in PulseAudio as a second stream of the game, which can be moved to a sink of its own (pactl move-sink-input) and recorded apart. In POD that showed when the game starts its music (with a race's countdown), which the code alone had not. Under QEMU's TCG the digital path stalls: the music came in consecutive bursts at about 45% of real time. Good for when and which track, not for listening.

Code: Apache 2.0. Content: CC0 1.0. No game material: bring your own copy.