Skip to content

Match the QEMU audio mixer to the Mac output device rate - #283

Open
stevederico wants to merge 2 commits into
omacom:mainfrom
stevederico:fix/audio-host-sample-rate
Open

stevederico wants to merge 2 commits into
omacom:mainfrom
stevederico:fix/audio-host-sample-rate

Conversation

@stevederico

Copy link
Copy Markdown
Contributor

Fixes #265.

Cause

QEMU's audiodev opens its mixer at 44100 Hz by default, whatever the Mac's output device is doing. When the default output device runs at a different rate, the guest's PCM stream is drained at the wrong speed and playback drifts. 48000 Hz is the macOS built-in default, which is why the symptom is the common case.

This is not specific to the SDL backend. I rebuilt the runtime with --audio-drv-list=sdl,coreaudio to check, and coreaudio drifts too, slightly worse and very consistently (18.531 s, sd 0.021). It is also not the guest's rate: forcing the guest's PipeWire graph to 44100 while the device stayed at 48000 changed nothing (18.515 s).

Setting out.frequency to the device's rate removes the drift; setting it to a fixed 48000 simply reverses the mismatch on a 44100 device (21.920 s, ~9.6% slow). So the rate has to be read from the host rather than hardcoded.

Fix

Query CoreAudio via system_profiler/plutil for the default output device's nominal rate and pass it as out.frequency/in.frequency. Falls back to 48000 if the query cannot answer. No new dependencies.

Measurements

A 20.000 s stereo tone played with pw-play in the guest, timed from the host over an SSH port forward so the guest clock is outside the measurement path. The guest's own timing was recorded in parallel and agreed with the host to within ~40 ms on every run. 8-10 runs per condition.

Mac output device rate Before After
48000 19.033 s / 19.090 s (~4.8% fast) 20.158 s (sd 0.019)
44100 20.013 s / 19.867 s 20.161 s (sd 0.017)

Both rates now land on the same value, so playback speed no longer depends on the device rate. The residual 0.16 s is constant pw-play startup and appears in every configuration, including the ones that were already correct.

Detection was verified to return the right value at 44100, 48000, 88200 and 96000.

Limitations

  • End-to-end playback was tested at 48000 and 44100. 88200 and 96000 were only verified for detection.
  • The rate is read once at launch. Changing the output device or its rate mid-session will reintroduce drift until the VM is restarted. Following device changes at runtime would need QEMU-side work.
  • On an unpatched build, a 96000 Hz device measured ~2-3% fast even with the mixer matched (19.409 s with coreaudio, 19.627 s with SDL). That does not fit the ratio above and may be a separate issue; I did not chase it.

Testing notes

Measured on a MacBook Air (M3), macOS 15.6, against a re-signed 0.4.1 bundle and a runtime built from this tree. Single unrepeated timings are not reliable here: per-run spread on some configurations reaches 0.4 s, and an early round of one-shot manual tests produced both fast and correct results at the same device rate. All figures above are 8-10 run means with standard deviations.

QEMU's audiodev defaults to a 44100 Hz mixer regardless of the host
output device. When the Mac's default output device runs at any other
rate, the guest's PCM stream is drained at the wrong speed and playback
drifts. At 48000 Hz, the macOS built-in default, a 20.000 s clip plays
in about 19.0 s.

Query CoreAudio for the default output device's nominal rate and pass it
as out.frequency/in.frequency so the mixer and the device agree. Fall
back to 48000 when the query cannot answer.

Measured with a 20.000 s tone played by pw-play in the guest and timed
from the host over an SSH forward, so the guest clock is outside the
measurement path (8-10 runs per condition):

  device 48000, before: 19.033 s / 19.090 s (about 4.8% fast)
  device 48000, after:  20.158 s (sd 0.019)
  device 44100, before: 20.013 s / 19.867 s
  device 44100, after:  20.161 s (sd 0.017)

Both rates now land on the same figure, so playback speed no longer
depends on the host device rate. The residual 0.16 s is constant pw-play
startup and is present in every configuration.
The launcher now passes the Mac output device's rate to the audiodev, so
the exact -audiodev line the test pinned no longer matched. Give the test
a system_profiler shim that reports a 44100 Hz default output device and
expect out.frequency and in.frequency at that rate, which also checks the
detection instead of the 48000 fallback.
digital-brew pushed a commit to digital-brew/try-omarchy that referenced this pull request Sep 29, 2026
QEMU's audiodev opens its mixer at 44100 Hz regardless of the Mac's output
device. With the device at macOS's default 48000 Hz the guest's PCM stream is
drained at the wrong speed and every app plays about 5 percent fast and
sharp. Read the default output device's nominal rate from CoreAudio through
system_profiler and pass it as out.frequency and in.frequency, falling back
to 48000 when the query cannot answer. From omacom#283.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018W8fhTegW8d16zpAjRpsT9
digital-brew pushed a commit to digital-brew/try-omarchy that referenced this pull request Sep 29, 2026
QEMU's audiodev opens its mixer at 44100 Hz regardless of the Mac's output
device. With the device at macOS's default 48000 Hz the guest's PCM stream is
drained at the wrong speed and every app plays about 5 percent fast and
sharp. Read the default output device's nominal rate from CoreAudio through
system_profiler and pass it as out.frequency and in.frequency, falling back
to 48000 when the query cannot answer. From omacom#283.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018W8fhTegW8d16zpAjRpsT9
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.

Guest audio plays ~6% fast/sharp: emulated HDA card drains PCM faster than real time and drives the PipeWire graph

1 participant