Commit 7b33c2a3 by PLN (Algolia)

audio: a default sink with no receiver is now one line, not a morning

PLN on the NVIDIA card: 'it NEVER served me, 100% of the time'. He is right,
and the reason it kept getting chosen was a saved pin to pro-audio — a profile
whose 'available' flag is unconditionally yes, because it maps raw PCMs and
asks no questions about cables. So four sinks reported perfectly healthy while
every ELD on the card read monitor_present 0 and every connector read
disconnected. Sound was not muted or misrouted, it was converted and discarded.

The pin is retired (that card is now off, and WirePlumber's own findBestProfile
will not go back to pro-audio by name). The check that would have said so in
one line now exists, keyed to the ELD rather than to the profile's own opinion.

Narrow on purpose: it accuses only a sink that leaves the box over a video
cable, reusing fold-orbits' predicate, because the sof card carries Speaker AND
an HDMI output — a check keyed on the CARD would have accused the laptop
speakers while sound was coming out of them.

The first resolver called realpath() on /proc/asound/cardN, which is a plain
directory, matched nothing, and made the detector dead code that reported
success. A test now asserts it reads sysfs.
parent 73bb18f1
......@@ -49,6 +49,7 @@ from __future__ import annotations
import argparse
import datetime as dt
import json
import os
import re
import shutil
import subprocess
......@@ -161,6 +162,61 @@ def dead_fragments(ver: tuple[int, int] | None) -> list[Path]:
# ── findings ───────────────────────────────────────────────────────────────
def default_sink() -> str:
"""The sink everything lands on unless an app says otherwise."""
rc, out = _run(["pactl", "get-default-sink"])
return out.strip() if rc == 0 else ""
# Same predicate `tools/fold-orbits.py` already uses to recognise a sink that
# leaves the box over a video cable. One vocabulary: if that list grows, it
# grows in both places for the same reason.
HDMI_SINK_RE = re.compile(r"hdmi|displayport|\bdp\b|pro-output", re.I)
def _sys_card_of(sink: str) -> str | None:
"""`alsa_output.pci-0000_01_00.1.pro-output-9` -> `card0`.
Resolved through /sys/class/sound/cardN/device, NOT through
/proc/asound/cardN -- the latter is a plain directory, so an earlier
version of this that called realpath() on it silently matched nothing and
the whole check was dead code that reported success.
"""
m = re.search(r"pci-([0-9a-f]{4})_([0-9a-f]{2})_([0-9a-f]{2})\.(\d+)",
sink or "")
if not m:
return None
addr = "%s:%s:%s.%s" % m.groups()
for link in sorted(Path("/sys/class/sound").glob("card[0-9]*")):
try:
if addr in os.path.realpath(link / "device"):
return link.name
except OSError:
continue
return None
def _has_no_receiver(card: str) -> bool:
"""True when the card exposes ELDs and NOT ONE of them sees a monitor.
A card with no ELD is not an HDMI card, so it is none of this check's
business: absent is not broken. Unreadable is not broken either -- never
accuse on missing evidence.
"""
elds = sorted(Path("/proc/asound").glob(f"{card}/eld*"))
if not elds:
return False
for eld in elds:
try:
body = eld.read_text(errors="replace")
except OSError:
return False
for ln in body.splitlines():
if ln.startswith("monitor_present") and ln.split()[-1] == "1":
return False
return True
def findings() -> list[dict]:
out: list[dict] = []
cs, pinned, ver = cards(), pins(), wp_version()
......@@ -246,6 +302,25 @@ def findings() -> list[dict]:
"nothing to fold onto.",
fix="plug the UMC202HD, or fix the profiles above"))
# 3b. THE ONE-LINE DETECTOR (PLN, 2026-09-23: NVIDIA "NEVER served me,
# 100% of the time"). On 2026-09-23 the default sink was
# alsa_output.pci-0000_01_00.1.pro-output-9 -- an HDMI output on a GPU whose
# every connector read `disconnected` and whose every ELD read
# `monitor_present 0`. Audio was not muted and not misrouted, it was
# TERMINATED, and the box reported four healthy sinks the whole time. That
# cost a morning of archaeology to state, so now it is one line.
dflt = default_sink()
card = _sys_card_of(dflt) if HDMI_SINK_RE.search(dflt) else None
if card and _has_no_receiver(card):
out.append(dict(
level=WARN, what="default sink has no receiver", fixable=False,
detail=(f"{dflt} is an HDMI/DP output on {card}, whose every ELD "
"reads monitor_present 0 — nothing is plugged in. Sound "
"sent there is converted and discarded, and the sink goes "
"on looking perfectly healthy."),
fix=("pactl set-default-sink <a sink on a card with speakers>, then "
"retire the pin that chose this one: tools/check-audio.py --fix")))
# 4. the trap that makes the rest invisible
for frag in dead_fragments(ver):
out.append(dict(
......
......@@ -266,3 +266,74 @@ def test_a_card_pinned_off_is_reported_but_never_acted_on(monkeypatch):
assert hits[0]["fixable"] is False, "--fix would switch the glitchy codec on"
assert "pactl set-card-profile" in hits[0]["fix"], "no manual path offered"
assert "audio-profile-stuck" in hits[0]["fix"], "the procedure is not cited"
# ── the default sink that goes nowhere (2026-09-23) ────────────────────────
# PLN: NVIDIA "NEVER served me, 100% of the time, when output / sink was nvidia
# whatever, i did nothing but toggle it away". The morning it was diagnosed, the
# default sink was an HDMI output on a GPU with every connector disconnected —
# and pactl reported four perfectly healthy sinks throughout.
def _fake_proc(monkeypatch, tmp_path, card: str, values: list[int]):
"""Stand up a believable /proc/asound/<card>/eld* and point the module at
it. Exercises `_has_no_receiver` itself rather than asserting on the
fixture, which is the trap a test like this walks into."""
d = tmp_path / card
d.mkdir(parents=True, exist_ok=True)
for i, v in enumerate(values):
(d / f"eld#0.{i}").write_text(
f"monitor_present\t\t{v}\neld_valid\t\t{v}\n")
real = CA.Path
monkeypatch.setattr(CA, "Path",
lambda arg: tmp_path if str(arg) == "/proc/asound"
else real(arg))
def test_a_card_whose_every_eld_is_dead_has_no_receiver(tmp_path, monkeypatch):
_fake_proc(monkeypatch, tmp_path, "card0", [0] * 16)
assert CA._has_no_receiver("card0") is True
def test_one_live_eld_is_enough_to_clear_the_card(tmp_path, monkeypatch):
# The real sof card on 2026-09-23: 36 ELDs, exactly one reading 1. A card
# with a screen attached must never be accused.
_fake_proc(monkeypatch, tmp_path, "card1", [0] * 12 + [1] + [0] * 23)
assert CA._has_no_receiver("card1") is False
def test_a_card_with_no_eld_at_all_is_not_this_checks_business(tmp_path, monkeypatch):
# Absent is not broken. A USB interface has no ELD and must never be
# accused of having no receiver.
_fake_proc(monkeypatch, tmp_path, "card2", [])
assert CA._has_no_receiver("card2") is False
def test_only_a_video_cable_sink_is_ever_accused():
# The false positive this guard exists for: the sof card carries Speaker AND
# an HDMI output. With no screen plugged in, every ELD on it reads 0 — so a
# check keyed only on the CARD would accuse the laptop speakers of having no
# receiver while sound was coming out of them.
speakers = "alsa_output.pci-0000_00_1f.3-platform-sof_sdw.HiFi__hw_sofsoundwire_2__sink"
assert not CA.HDMI_SINK_RE.search(speakers)
for dead in ("alsa_output.pci-0000_01_00.1.pro-output-9",
"alsa_output.pci-0000_01_00.1.hdmi-stereo",
"alsa_output.something.displayport-2"):
assert CA.HDMI_SINK_RE.search(dead), dead
def test_the_resolver_uses_sysfs_not_proc():
"""/proc/asound/cardN is a plain DIRECTORY, so realpath() on it contains no
PCI address. The first version of this resolver did exactly that, matched
nothing, and made the whole check dead code that reported success — the
'green check is not a working system' failure, in the detector itself."""
src = open(CA.__file__).read()
fn = src[src.index("def _sys_card_of"):src.index("def _has_no_receiver")]
body = fn.split('"""')[-1] # code only; the docstring says why
assert "/sys/class/sound" in body
assert "/proc/asound" not in body
def test_a_sink_with_no_pci_address_resolves_to_nothing():
for odd in ("", "bluez_output.70_26_05_D0_D5_90.1", "not-a-sink",
"alsa_output.usb-BEHRINGER_UMC202HD_192k-00.HiFi__Line__sink"):
assert CA._sys_card_of(odd) is None, odd
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment