Skip to content

fix(viewer): route audio_output to a live PulseAudio sink on x86/arm64#3209

Merged
vpetersson merged 7 commits into
masterfrom
fix/x86-arm64-audio-output-routing
Jul 20, 2026
Merged

fix(viewer): route audio_output to a live PulseAudio sink on x86/arm64#3209
vpetersson merged 7 commits into
masterfrom
fix/x86-arm64-audio-output-routing

Conversation

@vpetersson

@vpetersson vpetersson commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Problem

On x86 and arm64 (non-Raspberry-Pi) boards, the HDMI / 3.5mm audio output selector in Settings is a no-op. get_alsa_audio_device() collapsed every setting to QMediaDevices::defaultAudioOutput() (the PulseAudio default sink), so playback audio followed whatever sink pulse defaulted to instead of the port the user chose.

Reported on the forum (Dell Latitude E5570, Debian 13): HDMI selected in the UI but sound only played through the internal analog speakers; selecting 3.5mm changed nothing.

Root cause, in src/anthias_viewer/media_player.py:

  • arm64 returned default before even reading settings['audio_output'].
  • x86: hdmidefault; localplughw:CARD=Headphones (a Pi-only card that matches nothing on x86, so it also fell back to the default sink).

Whether HDMI or analog happened to work was pure luck of which sink PulseAudio defaulted to.

Fix

On non-Pi (Qt 6 + PulseAudio) boards, discover sinks at runtime with pactl list short sinks and map the setting onto the matching sink by its standardised profile token in the sink name:

  • hdmi → the HDMI sink (*.hdmi-* / iec958 / displayport)
  • local → the analog / headphone sink (*.analog-* / headphone / speaker)

The bare sink name is returned; VideoView::resolveAlsaDevice already matches it against QAudioDevice::id() (no C++ change needed). Falls back to default when pulse is unreachable or no matching sink exists, preserving prior behaviour. Raspberry Pi paths (fixed vc4hdmi* / Headphones card names) are unchanged.

The profile token — not the card name — is the discriminator: on the reporter's Dell both an analog and an HDMI sink live on the same HDA card, so card name alone can't tell them apart.

Testing

  • Unit tests for sink selection (hdmi/local, same-card disambiguation, no-match fallback), pactl parsing, and the arm64/x86 routing paths. Full suite green; ruff check + ruff format --check clean.
  • Full testbed fleet validated (no regression):
    • x86: local routes to the analog sink live; HDMI sink only exists with a display attached (headless testbed correctly falls back to default).
    • Rock Pi 4: decisive test — its default sink is HDMI, and selecting local moved audio to the analog sink (verified RUNNING), proving the runtime sink actually overrides the default end-to-end; hdmi routes to the HDMI sink.
    • Pi 2 / 3 / 4 / 5: device resolution byte-for-byte unchanged.

Fixes #3208

Update: single-card HDA hardening

Sink-listing alone only routes correctly on multi-card topologies (Rock Pi's separate hdmisound/Analog cards, or a discrete HDMI card beside onboard analog), where both sinks are always instantiated. On a single-card HDA system — laptops/desktops, exactly the forum #6749 Dell topology — PulseAudio instantiates only the active profile's sink, so pactl list short sinks never shows the HDMI sink while analog is active; selecting HDMI would silently fall back to analog.

On a sink miss, the fix now parses pactl list cards, finds the card whose pure output profile (output:hdmi-* / output:analog-*) matches the requested output and is available, and pactl set-card-profiles it. Combined output:hdmi-*+input:analog-stereo profiles are ignored because their available: yes is masked by the analog input even when the HDMI output is unusable (the Dell's exact quirk). When no available matching profile exists (video-only HDMI display), nothing is switched and it falls back to default. The card work runs only on a miss, so steady state stays a single cached pactl list short sinks.

Validated on real single-card hardware: with analog active, requesting HDMI switched the card to its available digital profile and routed to the freshly-created sink; requesting analog switched back.

On x86 and arm64 SBCs the HDMI / 3.5mm output selector was a no-op:
every setting collapsed to QMediaDevices::defaultAudioOutput() (the
PulseAudio default sink), so audio followed whatever sink pulse
defaulted to rather than the port the user picked (forum #6749 —
Dell Latitude, HDMI selected but sound only on the internal speakers).

Discover sinks at runtime via `pactl list short sinks` and map the
setting onto the matching sink by its standardised profile token
(hdmi-* / analog-*), returning the sink name for
VideoView::resolveAlsaDevice to match. Fall back to `default` when
pulse is unreachable or no sink matches, preserving prior behaviour.
Raspberry Pi paths (fixed vc4hdmi* / Headphones card names) are
unchanged.

Validated across the full testbed fleet: x86 and Rock Pi 4 route
hdmi/local to the correct sink (Rock Pi `local` moved audio off the
HDMI default sink to the analog sink, verified RUNNING); Pi 2/3/4/5
device resolution unchanged.

Fixes #3208

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vpetersson
vpetersson requested a review from a team as a code owner July 17, 2026 17:49
@vpetersson
vpetersson requested a review from Copilot July 17, 2026 17:49

Copilot AI 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.

Pull request overview

This PR fixes the viewer’s audio_output routing on non-Raspberry-Pi devices (x86 + generic arm64) by resolving the configured output to an actual, currently-available PulseAudio sink instead of always following PulseAudio’s default sink.

Changes:

  • Added runtime PulseAudio sink discovery (pactl list short sinks) and selection logic to map audio_output (hdmi/local) to a matching sink on x86/arm64, with a safe fallback to default.
  • Refactored Raspberry Pi ALSA-device resolution into a dedicated _pi_alsa_device() path while keeping Pi behavior unchanged (including the existing Pi 5 “local → HDMI” behavior).
  • Expanded unit tests to cover sink selection, pactl parsing, x86/arm64 routing, and fallback behavior (issue #3208).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/anthias_viewer/media_player.py Implements PulseAudio sink listing/selection for x86/arm64 while preserving Pi-specific ALSA mapping and adding transition-based logging.
tests/test_media_player.py Adds unit coverage for sink selection, pactl parsing, and the new arm64/x86 routing + fallback paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/anthias_viewer/media_player.py Outdated
Comment thread src/anthias_viewer/media_player.py
- Cache `pactl list short sinks` for a short TTL so a fast-rotating
  playlist doesn't spawn a subprocess on every play()/set_asset();
  the window is short enough to pick up an HDMI hotplug within
  seconds, and only successful lists are cached.
- Note in `_pi_alsa_device` that the Pi 5 `local` selection resolves
  to the connected HDMI device (no analog jack; the UI hides 3.5mm).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

vpetersson and others added 2 commits July 17, 2026 19:02
_select_pulse_sink assigned a 4-tuple then a 3-tuple to `keywords`;
mypy inferred the fixed-length type from the first branch and rejected
the second. Annotate as `tuple[str, ...]`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uting

The sink-listing approach only worked on multi-card topologies (Rock
Pi's separate hdmisound/Analog cards, or a discrete HDMI card next to
onboard analog), where both sinks are always instantiated. On a
single-card HDA system — laptops/desktops, the forum #6749 Dell
topology — PulseAudio instantiates only the *active* profile's sink, so
`pactl list short sinks` never shows the HDMI sink while analog is
active. Selecting HDMI then silently fell back to the default (analog)
even with an HDMI-audio-capable display attached.

On a sink miss, parse `pactl list cards`, find the card whose *pure*
output profile (`output:hdmi-*` / `output:analog-*`, ignoring the
combined `+input:` profiles whose availability is masked by the analog
input — issue #6749) matches the requested output and is available,
and `pactl set-card-profile` it if not already active. Then re-list and
route to the freshly instantiated sink. When no available matching
profile exists (a video-only HDMI display, all `output:hdmi-*`
available: no), nothing is switched and it falls back to default as
before. The card work runs only on a miss, so the steady-state path
stays a single cached `pactl list short sinks`.

Validated on real single-card hardware: with analog active, requesting
HDMI switched the card to its available digital profile and routed to
the newly-created sink; requesting analog switched back.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread src/anthias_viewer/media_player.py
Comment thread src/anthias_viewer/media_player.py Outdated
… key log on setting

- Skip the `pactl list cards` profile-activation probe when
  `_list_pulse_sinks()` returns empty (pulse unreachable) — the single-
  card recovery path always has ≥1 active sink, so guarding on `if
  sinks` avoids a redundant failing subprocess in the down case.
- Key the resolve-log suppression on (audio_output, result) instead of
  the resolved sink alone, so a setting change (hdmi→local) that both
  fall back to `default` still logs once and stays correlatable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread tests/test_media_player.py
test_x86_falls_back_to_default_when_no_matching_sink now patches
_activate_output_profile so the miss-recovery path doesn't spawn a real
`pactl list cards` subprocess on the test runner.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread src/anthias_viewer/media_player.py Outdated
Comment thread src/anthias_viewer/media_player.py Outdated
…ailable

- _select_pulse_sink now applies a stable preference among matches
  (plain *-stereo first, then non-surround, then lexical) so a
  multi-HDMI box doesn't pick a surround/secondary port from pactl's
  unspecified ordering.
- _list_pulse_cards treats only `available: yes` as available;
  `available: unknown` (port not probeable) no longer counts, so we
  never switch a working analog profile for an unconfirmed HDMI one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@vpetersson
vpetersson merged commit f6ecaf4 into master Jul 20, 2026
10 checks passed
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.

x86 / arm64: HDMI vs 3.5mm audio_output selector is a no-op (always uses PulseAudio default sink)

2 participants