Skip to content

host/audio: play 5.1 on output routes that take six channels - #2234

Open
maxdavidson wants to merge 1 commit into
seedds:mainfrom
maxdavidson:multichannel-output
Open

maxdavidson wants to merge 1 commit into
seedds:mainfrom
maxdavidson:multichannel-output

Conversation

@maxdavidson

@maxdavidson maxdavidson commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

What and why

On a Mac over HDMI to a 5.1 TV, receiver or soundbar (or an iPhone on a multichannel route), a game that writes six channels plays in stereo. GetActiveAudioDeviceChannelCountForOutput reported two channels on every route, and every six-channel output was folded to stereo before Core Audio.

The change:

  • Channel count: the host reports the route's count. It is six when the Mac's output device has a stream format of six or more channels, or when the iOS session's output does. Game threads read it from a cache (dm_audio_route) because a Core Audio read can need more stack than a game thread has.
  • Six-channel outputs: on a six-channel route, a six-channel AudioOut, and the in-process renderer's DeviceSink, go to the AudioUnit as they are, with the input layout MPEG_5_1_A. Core Audio then maps them onto the device's own layout. That matters on HDMI, whose order puts the LFE before the center.
  • Stereo routes: the host downmixes a six-channel output with the renderer's default coefficients. Outputs of other channel counts go as the guest writes them.
  • Route changes: if the route changes while an output plays (HDMI to the speakers or back), the outputs whose channel count no longer fits get a new unit. This goes through the media services reset path. On iOS the session asks for the new route's channels first.
  • Test aids: DM_AUDIO_DEVICE (Mac, diagnostic) pins the output to a device by name. t_audio_live and dm/tools/audio_channel_check.py check the channels on a loopback device.

This overlaps #1381, the opt-in Spatial Audio through macOS's renderer. Both PRs hand six channels on in hle_audio.c and hle_audren.c. This PR is the default path for six-channel devices and iOS, and #1381 is the opt-in system spatializer for headphones. Whichever lands second adapts to the other.

Reference entry: Stereo only on a 5.1 system.

Checked

  • Tests:
    • make -C dm/host/tests GAME=010015100B514000 run passes. That includes t_audio_device, which uses mock units and covers:
      • six channels with the layout set;
      • the downmix values and their clamp;
      • a route change in both directions;
      • a failed new unit, made again at the next change;
      • stereo and mono outputs left as they are.
    • t_audio_device passed 20 runs in a row.
    • It also includes t_audren_layout: six channels in sink order, and a mono sink on the front pair.
    • make -C dm/gfx/tests GAME=010015100B514000 run passes.
    • dm/tools/tests: test_docs, test_env_switches, test_pr_areas and test_audio_channel_check pass, and so does env_switches.py --check.
    • Lint: lint_python reports 0 new. lint_c.py tidy (LLVM 23.1.4) reports nothing in the files this change touches; its seven findings above the baseline are in five dm/gfx/tests files that are the same on main.
    • hle_audio.c, hle_audren.c and front_ios.m compile for iOS 26.
    • On loopback devices (BlackHole 16ch and 2ch, audio_channel_check.py, test tones captured in-process): each tone on its own channel, the others more than 100 dB down. The downmix came within 0.05 dB of the host's coefficients. With BlackHole 16ch set to HDMI's layout, the center and the LFE land on HDMI's channels 4 and 3.
    • Switching while playing: t_audio_live moved the output between the two devices mid-run. Each device carried the 5.1 signal or the downmix right after the switch.
  • Games run: none yet. The iPhone route-change path has not run on a device either. Spatial Audio over AirPods is not part of this change.
  • Devices and OS: MacBook Pro M4 Pro, macOS 27.2. By ear over HDMI to an LG TV, passed over eARC to a Sonos system: t_audio_live played a spoken channel name on each channel, and each came from the right speaker.
  • regress.py: not run yet; the report will be added here. regress.py impact names Hades, Breath of the Wild, Pokémon Scarlet and Tears of the Kingdom by their usage manifests.

Docs

  • The finding is in its docs/reference/audio.md entry, and python3 dm/tools/docs_symptoms.py was run (the symptom index)
  • No game files, keys or data extracted from a game in the change or the attachments

🤖 Generated with Claude Code

@github-actions github-actions Bot added host The Horizon host: SVCs, services, threads, memory (dm/host) ios The iPhone and iPad app (dm/ios) tools Tools and scripts (dm/tools, scripts) docs Documentation labels Nov 10, 2026
@maxdavidson
maxdavidson marked this pull request as ready for review November 10, 2026 21:49
@maxdavidson maxdavidson changed the title 5.1 audio output where the route takes six channels (HDMI, multichannel devices, iOS routes) 5.1 audio output on six-channel routes Nov 10, 2026
@maxdavidson
maxdavidson force-pushed the multichannel-output branch 3 times, most recently from ced43ba to b12f4fc Compare November 13, 2026 12:56
Audio plays in 5.1 where the output route takes six channels: GetActiveAudioDeviceChannelCountForOutput reports the route's count (six when the Mac's output device has a stream format of six or more channels, or when the iOS session's output does; stereo otherwise), a six-channel output goes to the AudioUnit as it is with the input layout MPEG_5_1_A, so Core Audio puts each channel on its speaker even on an HDMI sink whose order has the LFE before the center, and the in-process renderer opens at six. On a stereo route the host downmixes a six-channel output with the renderer's default coefficients (Core Audio's own 6 → 2 would keep only the front pair); outputs of other channel counts go as the guest writes them. A route change while an output plays replaces the units whose channel count no longer fits the route, through the media services reset path (au_reopen), and an output whose new unit could not be made is tried again at the next change; on iOS the session asks for the new route's channels first. Checked on BlackHole 16ch and 2ch (dm/tools/audio_channel_check.py: each tone on its own channel, the downmix within 0.05 dB of the host's coefficients; t_audio_live moved the output between the two while it played), and by ear on a Mac over HDMI to an LG TV and over eARC to a Sonos system. DM_AUDIO_DEVICE pins the Mac's output to a device by name, for tests. Not yet checked in a game or on an iPhone; Spatial Audio over AirPods is not done

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@maxdavidson maxdavidson changed the title 5.1 audio output on six-channel routes host/audio: play 5.1 on output routes that take six channels Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation host The Horizon host: SVCs, services, threads, memory (dm/host) ios The iPhone and iPad app (dm/ios) tools Tools and scripts (dm/tools, scripts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants