Commit 9edfd743 by PLN (Algolia)

fix(audio): wireplumber start-limit takes all audio out, and the doctor could not see it

PLN: 'media keys dont work' + 'tried bluetooth headset couldnt get sound there'.
Cause was neither:

    wireplumber        failed (start-limit-hit)
    Sinks:  100. Dummy Output      <- the ONLY sink
    0 bluez objects

pipewire is the graph; wireplumber is the session manager that puts DEVICES in
it. Without it there are no device nodes at all -- every hardware sink vanishes,
PipeWire invents Dummy Output, media keys act on nothing, and a Bluetooth
headset cannot appear however well it pairs. It reads exactly like dead
hardware.

The trap is StartLimitBurst=5 / 5min with Restart=on-failure. Five hand-issued
restarts inside two minutes exhausted it, and from then on restarting AGAIN
cannot work. Cure is reset-failed, then start. Third instance of this exact
shape in one session after parvagues-sc twice: on this box, 'X will not start'
means check reset-failed first.

rig-doctor's check_pipewire tested pipewire on PATH, pw-jack on PATH, and
pipewire.service is-active -- and pipewire.service was ACTIVE for the whole
outage, so the check was green while the box had no audio at all. Presence
versus function again. It now also asserts wireplumber is active and that the
graph holds at least one real output (alsa_output/bluez_output, never
auto_null/Dummy), FAILs with the reset-failed cure in the fix field, and WARNs
on a dummy or unset default. It caught a live regression the first time it ran.

The headset itself was only MUTED: bluez_card active profile was already
a2dp-sink, and the sink read '[vol: 0.20 MUTED]' -- per-device state wireplumber
persists. Unmuted by NAME, because the BT node id moves between calls while the
device reconnects. Not caused by the quantum/mute testing, which ran at 00:10
with no BT device connected and left force-quantum verified at 0.

latency-lens --print-config used to hand over 'systemctl --user restart pipewire
pipewire-pulse wireplumber' with no warning, which repeated is precisely this
trap. It now says ONCE, states the consequence, gives the cure, and points at
the runtime setting that needs no restart.

Doctor: 48 checks, 0 fail, 10 warn, 38 pass.
parent 79ff7c9f
...@@ -2355,6 +2355,65 @@ scattered through this file resolve locally. ...@@ -2355,6 +2355,65 @@ scattered through this file resolve locally.
where swept stems land (`take-master.py`/`take-lens.py` should own it, not a new tool); where swept stems land (`take-master.py`/`take-lens.py` should own it, not a new tool);
whether the local 53G ever comes back or the mirror stays the archive of record. whether the local 53G ever comes back or the mirror stays the archive of record.
## Audio session — opened 2026-09-07
- 🔴 **`wireplumber` hitting `start-limit-hit` takes ALL audio out, and looks
like dead hardware.** Reported by PLN as "media keys dont work" + "tried
bluetooth headset couldnt get sound there". Diagnosis:
wireplumber failed (start-limit-hit)
Sinks: 100. Dummy Output <- the ONLY sink
0 bluez objects
pipewire is the graph; **wireplumber is the session manager that puts DEVICES
in the graph**. Without it there are no device nodes at all: every hardware
sink disappears, PipeWire invents `Dummy Output`, media keys act on nothing,
and a Bluetooth headset cannot appear however perfectly it pairs.
**The trap**: `StartLimitBurst=5` / `StartLimitIntervalUSec=5min` with
`Restart=on-failure`. Five restarts inside two minutes (10:25:01, 10:25:51,
10:26:07, 10:26:26, 10:27:05 — each `stopped by signal: Terminated` then
immediately `Started`, i.e. hand-issued `systemctl --user restart`) exhausted
it, and from then on **restarting again cannot work** — the journal says
`Start request repeated too quickly` and nothing starts. It recurred at
11:43-11:44 while this was being written, with
`systemctl[2200836]: Job for wireplumber.service failed` naming the client.
**Cure, and it is not another restart:**
systemctl --user reset-failed wireplumber && systemctl --user start wireplumber
**Third instance of this exact shape in one session** — `parvagues-sc` twice
(2026-09-06 23:17 and the boot-grace bug) and `wireplumber` here. On this box,
"X will not start" means check `reset-failed` BEFORE anything else.
- ~~**`rig-doctor` could not see a total audio outage**~~ **FIXED 2026-09-07.**
`check_pipewire` tested `pipewire` on PATH, `pw-jack` on PATH, and
`pipewire.service is-active` — and **`pipewire.service` was `active` for the
whole outage**, so the check was green while the box had no audio whatsoever.
Presence versus function, again. It now also asserts wireplumber is active AND
that the graph holds at least one real output (`alsa_output.*` /
`bluez_output.*`, never `auto_null`/Dummy), FAILs with the `reset-failed` cure
in the fix field, and WARNs if a dummy is present or the default is unset.
Caught a LIVE regression the first time it ran: `wireplumber is 'failed' — 0
real sink(s), 1 dummy`, while PLN was still retrying by hand.
Doctor now: **48 checks, 0 fail, 10 warn, 38 pass.**
- ~~**Bluetooth headset had no sound**~~ **FIXED 2026-09-07: it was muted.** Once
wireplumber was up, `bluez_card.88_C9_E8_9F_C1_49` was present with
`Active Profile: a2dp-sink` (High Fidelity Playback, SBC) — the correct
profile, never the problem. The sink read
`137. WH-1000XM5 [vol: 0.20 MUTED]`: per-device mute that wireplumber
persists in its own state, at 20% volume. `pactl set-sink-mute
bluez_output.88_C9_E8_9F_C1_49.1 0` → `Mute: no`. **Not caused by the
quantum/mute testing**: that ran at 00:10 when no BT device was connected, and
`clock.force-quantum` was verified back at 0.
Second half of "no sound there", left for PLN on purpose: the XM5 is **not the
default sink**, so nothing routes to it until selected —
`wpctl set-default <id>` or the sound tray. Switching a live default would
yank audio off the speakers mid-session.
NB the BT node id MOVES between calls while the device reconnects (105183 →
105373 within a minute), so address it by NAME, never by id.
- **`latency-lens --print-config` used to hand you the loaded gun.** Its "Apply"
line is `systemctl --user restart pipewire pipewire-pulse wireplumber`, which
run a few times in a row is exactly the trap above. It now says ONCE, explains
the start-limit consequence, gives the `reset-failed` cure, and points at the
runtime `pw-metadata` setting that needs no restart at all.
## Housekeeping ## Housekeeping
- **`rig-doctor` does not check LV2 plugins** — the same gap class as the PySide6 miss. - **`rig-doctor` does not check LV2 plugins** — the same gap class as the PySide6 miss.
- ~~**`parvagues-protect` burns 10.7% of a core, forever, on a battery laptop.**~~ - ~~**`parvagues-protect` burns 10.7% of a core, forever, on a battery laptop.**~~
......
...@@ -307,6 +307,19 @@ context.properties = {{ ...@@ -307,6 +307,19 @@ context.properties = {{
}} }}
# Apply: systemctl --user restart pipewire pipewire-pulse wireplumber # Apply: systemctl --user restart pipewire pipewire-pulse wireplumber
#
# ONCE. Not in a loop while you fiddle. wireplumber ships StartLimitBurst=5 in a
# 5-minute window, and the 6th restart does not fail loudly — it leaves the unit
# `failed (start-limit-hit)` with NO SESSION MANAGER, which means no device nodes
# at all: every sink vanishes, the only one left is `Dummy Output`, media keys
# act on nothing and Bluetooth cannot appear. That happened on this box on
# 2026-09-07 (five restarts in two minutes) and reads exactly like "the audio
# hardware died". The cure is not another restart:
#
# systemctl --user reset-failed wireplumber && systemctl --user start wireplumber
#
# Prefer the runtime setting below while experimenting — it needs no restart at
# all, and reverts with one command.
# Revert: delete the file, same restart. Runtime-only equivalent, no restart: # Revert: delete the file, same restart. Runtime-only equivalent, no restart:
# pw-metadata -n settings 0 clock.force-quantum {q} # pw-metadata -n settings 0 clock.force-quantum {q}
# pw-metadata -n settings 0 clock.force-quantum 0 # back to default # pw-metadata -n settings 0 clock.force-quantum 0 # back to default
......
...@@ -495,6 +495,60 @@ def check_pipewire() -> None: ...@@ -495,6 +495,60 @@ def check_pipewire() -> None:
add("TOOLCHAIN", "pipewire.service", WARN, state_str, add("TOOLCHAIN", "pipewire.service", WARN, state_str,
"systemctl --user start pipewire.service pipewire-pulse.service") "systemctl --user start pipewire.service pipewire-pulse.service")
# WIREPLUMBER, AND WHETHER ANY REAL OUTPUT EXISTS.
#
# `pipewire.service` was `active` for the entire 2026-09-07 outage in which
# this box had NO AUDIO AT ALL — so the check above was green through it.
# pipewire is the graph; wireplumber is the session manager that puts DEVICES
# in the graph. Without it there are no device nodes: every sink disappears,
# the only one left is `Dummy Output`, media keys act on nothing, and a
# Bluetooth headset cannot appear however well it pairs. It reads exactly
# like dead audio hardware.
#
# How it got there is the trap worth knowing: wireplumber ships
# StartLimitBurst=5 / 5min, and the 6th restart does not fail loudly — it
# leaves the unit `failed (start-limit-hit)`, permanently, so restarting
# again cannot fix it. Five restarts in two minutes did it. Same shape as
# parvagues-sc's own start-limit trap, which bit twice in the same session.
rc, out, err = run_cmd(["systemctl", "--user", "is-active",
"wireplumber.service"])
wp = out.strip() or "?"
# Real outputs only: alsa_output.* is hardware, bluez_output.* is a headset.
# `auto_null` / Dummy Output is what PipeWire invents when there is nothing.
rc, out, err = run_cmd(["pactl", "list", "short", "sinks"])
names = [ln.split("\t")[1] for ln in out.splitlines()
if "\t" in ln and len(ln.split("\t")) > 1]
real = [n for n in names
if n.startswith(("alsa_output", "bluez_output"))]
dummy = [n for n in names if "auto_null" in n or "dummy" in n.lower()]
rc, dflt, err = run_cmd(["pactl", "get-default-sink"])
dflt = dflt.strip()
reset_cure = ("systemctl --user reset-failed wireplumber && "
"systemctl --user start wireplumber "
"# NOT another restart — the start limit is already tripped")
if wp != "active":
add("TOOLCHAIN", "audio session (wireplumber)", FAIL,
f"wireplumber is {wp!r} — no session manager, so NO DEVICES are in "
f"the graph. {len(real)} real sink(s), {len(dummy)} dummy. This is "
f"total silence that looks like broken hardware.", reset_cure)
elif not real:
add("TOOLCHAIN", "audio session (wireplumber)", FAIL,
f"wireplumber is active but the graph has NO real output "
f"(alsa_output/bluez_output); sinks seen: {names or 'none'}",
reset_cure)
elif dummy or not dflt or "auto_null" in dflt:
add("TOOLCHAIN", "audio session (wireplumber)", WARN,
f"{len(real)} real sink(s) but default is {dflt or 'unset'} "
f"and {len(dummy)} dummy sink(s) present",
"pick a real output: wpctl set-default <id> (wpctl status lists them)")
else:
add("TOOLCHAIN", "audio session (wireplumber)", PASS,
f"wireplumber active, {len(real)} real output(s), default = "
f"{dflt.split('.')[-1] if '.' in dflt else dflt}")
def check_alsa_ffmpeg() -> None: def check_alsa_ffmpeg() -> None:
for binname, why in ( for binname, why in (
......
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