docs/scripts(kaisa): add doctor verify mode and HDMI readiness checks

Add a more detailed --verify flow to capture Jack/ELD/subdevices, route per sink, and collect kernel error windows. Improve --fix with readiness gating, retries, and connected-only selection; document single-monitor pcm mapping behavior and ignore local logs/artifacts.

Made-with: Cursor
This commit is contained in:
2026-04-09 22:26:35 +08:00
parent bda6b60c15
commit 60b7cd389f
14 changed files with 1406 additions and 76 deletions

800
scripts/kaisa-audio-doctor.sh Executable file
View File

@@ -0,0 +1,800 @@
#!/usr/bin/env bash
# Kaisa (sof-rt5682) audio one-shot diagnostics (UCM/HiFi mainline).
# Collects high-signal state across ALSA/UCM2, PipeWire, WirePlumber, and policy/state files.
set -euo pipefail
ts="$(date +%Y%m%d_%H%M%S)"
out=""
fix=0
fix_only=0
verify=0
only_pcm=""
retries=1
only_connected=0
log_dir="${KAISA_LOG_DIR:-./_logs}"
usage() {
cat <<'EOF'
Usage:
./scripts/kaisa-audio-doctor.sh [--fix] [--fix-only] [--verify] [--only-pcm N] [--only-connected] [--retries N] [-o|--out /path/to/log]
Modes:
default Diagnostics only (no changes)
--fix Best-effort recovery, then full diagnostics report
--fix-only Same recovery as --fix, then exit (for login/boot automation; use with -o for a short log)
--verify More detailed verification (route/ports/playback + kernel error window), best used right after boot
Options:
--only-pcm N Restrict fix/verify to a single PCM (0/2/3/4). Useful for single-monitor A/B tests.
--only-connected Restrict fix/verify to HDMI PCMs that are currently connected (Jack=on + ELD non-empty).
--retries N Playback retries per PCM (default 1). On failure, restart user audio services then retry.
Recovery steps:
- restart user PipeWire/WirePlumber
- set card profile to HiFi
- choose an "available" HDMI port (prefer pcm=3/4, then pcm=2), else fallback to Analog (pcm=0)
- enable matching IEC958 switch (pcm=2->IEC958,0; pcm=3->IEC958,1; pcm=4->IEC958,2)
- quick playback test (timeout)
Notes:
- Run as your desktop user (NOT via sudo).
- This script assumes card name contains "cml_rt5682_def".
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--fix-only) fix=1; fix_only=1; shift ;;
--fix) fix=1; shift ;;
--verify) verify=1; shift ;;
--only-pcm) only_pcm="${2:-}"; shift 2 ;;
--only-connected) only_connected=1; shift ;;
--retries) retries="${2:-}"; shift 2 ;;
-o|--out) out="${2:-}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
*)
# Backward compatible: treat the first positional argument as output path.
if [[ -z "${out}" ]]; then
out="$1"
shift
else
echo "Unexpected argument: $1" >&2
usage >&2
exit 2
fi
;;
esac
done
out="${out:-${log_dir}/kaisa-audio-doctor_${ts}.log}"
mkdir -p "$(dirname "$out")"
exec > >(tee "$out") 2>&1
hr() { printf '\n%s\n' "================================================================================"; }
sec() { hr; printf '%s\n' "$1"; hr; }
cmd() { printf '\n$ %s\n' "$*"; "$@"; }
maybe() { printf '\n$ %s\n' "$*"; "$@" || true; }
note() { printf '\n[NOTE] %s\n' "$*"; }
warn() { printf '\n[WARN] %s\n' "$*"; }
is_int() { [[ "${1:-}" =~ ^[0-9]+$ ]]; }
get_kaisa_card_name() {
pactl list cards short 2>/dev/null | awk '/cml_rt5682_def/ {print $2; exit}'
}
amixer_find_numid() {
# Find numid by matching a single control line from `amixer -c0 controls`.
# Usage:
# amixer_find_numid "iface=PCM" "name='ELD'" "device=2"
# amixer_find_numid "iface=CARD" "name='HDMI/DP,pcm=2 Jack'"
local want_iface="${1:-}"
local want_name="${2:-}"
local want_device="${3:-}"
amixer -c0 controls 2>/dev/null | awk -v wi="$want_iface" -v wn="$want_name" -v wd="$want_device" '
{
ok=1
if (wi!="" && $0 !~ wi) ok=0
if (wn!="" && $0 !~ wn) ok=0
if (wd!="" && $0 !~ wd) ok=0
if (ok) {
# line begins with: numid=XX,iface=...
if (match($0, /numid=[0-9]+/)) {
s=substr($0, RSTART, RLENGTH)
sub(/^numid=/, "", s)
print s
exit 0
}
}
}
'
}
amixer_cget_by_control() {
# Best-effort cget using stable identifiers; falls back to numid lookup.
# For Jack: iface=CARD + exact name
# For ELD: iface=PCM + name='ELD' + device=N
local iface="${1:?iface}"
local name="${2:?name}"
local device="${3:-}"
local numid
if [[ -n "$device" ]]; then
numid="$(amixer_find_numid "iface=${iface}" "name='${name}'" "device=${device}" || true)"
else
numid="$(amixer_find_numid "iface=${iface}" "name='${name}'" "" || true)"
fi
if [[ -n "$numid" ]]; then
maybe amixer -c0 cget "numid=${numid}"
else
warn "Could not find numid for iface=${iface} name='${name}' device=${device:-<none>}"
fi
}
read_jack_on_for_pcm() {
# Returns 0 if Jack is on, 1 otherwise/unknown.
# Uses stable name->numid resolution.
local pcm="${1:?pcm}"
local name="HDMI/DP,pcm=${pcm} Jack"
local numid
numid="$(amixer_find_numid "iface=CARD" "name='${name}'" "" || true)"
[[ -n "$numid" ]] || return 1
# Example output contains ": values=on/off"
amixer -c0 cget "numid=${numid}" 2>/dev/null | awk '
BEGIN { ok=0; seen=0 }
$1==":" && $2 ~ /^values=/ {
seen=1
sub(/^values=/,"",$2)
if ($2=="on" || $2=="1") ok=1
else ok=0
exit
}
END {
if (seen && ok) exit 0
exit 1
}
'
}
eld_bytes_len_for_pcm() {
# Return byte count for ELD device=N (0 if empty/unknown).
local pcm="${1:?pcm}"
local numid
numid="$(amixer_find_numid "iface=PCM" "name='ELD'" "device=${pcm}" || true)"
[[ -n "$numid" ]] || { echo 0; return 0; }
amixer -c0 cget "numid=${numid}" 2>/dev/null | awk '
BEGIN { printed=0 }
/values=/ {
# Example: "; type=BYTES,...,values=36"
if (match($0, /values=[0-9]+/)) {
s=substr($0, RSTART, RLENGTH)
sub(/^values=/,"",s)
print s
printed=1
exit
}
}
END { if (!printed) print 0 }
' || true
}
wait_for_hdmi_ready_pcm() {
# Best-effort wait until HDMI pcm looks "ready enough":
# - Jack ON
# - ELD has bytes
# - ALSA subdevices avail > 0 (if we can parse)
local pcm="${1:?pcm}"
local timeout_s="${2:-6}"
local i
for i in $(seq 1 "$timeout_s"); do
if read_jack_on_for_pcm "$pcm"; then
local eld
eld="$(eld_bytes_len_for_pcm "$pcm" || echo 0)"
local avail total
avail="$(alsa_pcm_subdevices_available "$pcm" || true)"
total="$(alsa_pcm_subdevices_total "$pcm" || true)"
if [[ "$eld" -gt 0 ]]; then
if [[ -n "$avail" && -n "$total" && "$total" -gt 0 ]]; then
if [[ "$avail" -gt 0 ]]; then
return 0
fi
else
# If we can't parse subdevices, treat Jack+ELD as enough.
return 0
fi
fi
fi
sleep 1
done
return 1
}
connected_hdmi_pcms() {
# Return space-separated HDMI PCMs that appear connected: Jack=on AND ELD has bytes.
# Ordered by preference (3/4/2).
local out=()
local pcm
for pcm in 3 4 2; do
if read_jack_on_for_pcm "$pcm"; then
local eld
eld="$(eld_bytes_len_for_pcm "$pcm" || echo 0)"
if [[ "$eld" -gt 0 ]]; then
out+=("$pcm")
fi
fi
done
echo "${out[*]-}"
}
detect_available_pcm() {
# Prefer HDMI2/HDMI3 over HDMI1 because pcm=2 is known flaky on some setups.
# Returns one of: 3 4 2 0 (fallback to analog).
local card_section
card_section="$(pactl list cards 2>/dev/null | sed -n '/cml_rt5682_def/,+220p' || true)"
if echo "$card_section" | awk 'BEGIN{ok=1} /pcm=3, available/ {ok=0} END{exit ok}'; then echo 3; return 0; fi
if echo "$card_section" | awk 'BEGIN{ok=1} /pcm=4, available/ {ok=0} END{exit ok}'; then echo 4; return 0; fi
if echo "$card_section" | awk 'BEGIN{ok=1} /pcm=2, available/ {ok=0} END{exit ok}'; then echo 2; return 0; fi
echo 0
}
alsa_pcm_subdevices_total() {
# Return total subdevices for a given DEV from `aplay -l` (expects "子设备: X/Y" or "Subdevices: X/Y").
local dev="${1:?dev}"
aplay -l 2>/dev/null | awk -v dev="$dev" '
$0 ~ ("device " dev ":") {inblk=1}
inblk && ($0 ~ /子设备:/ || $0 ~ /Subdevices:/) {
# formats: "子设备: 0/1" or "Subdevices: 0/1"
gsub(/[^0-9\/]/,"",$0)
split($0,a,"/")
print a[2]
exit 0
}
inblk && $0 ~ /^card [0-9]+:/ {inblk=0}
'
}
alsa_pcm_subdevices_available() {
# Return available subdevices count X from X/Y for a given DEV.
local dev="${1:?dev}"
aplay -l 2>/dev/null | awk -v dev="$dev" '
$0 ~ ("device " dev ":") {inblk=1}
inblk && ($0 ~ /子设备:/ || $0 ~ /Subdevices:/) {
gsub(/[^0-9\/]/,"",$0)
split($0,a,"/")
print a[1]
exit 0
}
inblk && $0 ~ /^card [0-9]+:/ {inblk=0}
'
}
iec958_index_for_pcm() {
case "${1:-}" in
2) echo 0 ;;
3) echo 1 ;;
4) echo 2 ;;
*) echo "" ;;
esac
}
sink_name_for_pcm() {
local pcm="${1:?pcm}"
echo "alsa_output.pci-0000_00_1f.3-platform-cml_rt5682_def.HiFi__hw_sofrt5682_${pcm}__sink"
}
sink_exists() {
local sink="${1:?sink}"
pactl list short sinks 2>/dev/null | awk -v s="$sink" '$2==s {found=1} END{exit !found}'
}
kernel_tail_since_ts() {
# Print kernel SOF/ASoC HDMI related lines since given timestamp.
# ts format: "YYYY-MM-DD HH:MM:SS"
local since_ts="${1:?since_ts}"
journalctl -k -b --since "$since_ts" --no-pager 2>/dev/null | \
grep -nE 'sof-audio|sof_ipc3_pcm_hw_params|ipc tx error|STREAM_PCM_PARAMS|ASoC error|set_hw_params|HDMI[0-9]|pcm[0-9]+' 2>/dev/null | \
tail -n 200 || true
}
restart_user_audio_services() {
maybe systemctl --user restart pipewire pipewire-pulse wireplumber
maybe sleep 2
}
verify_one_pcm() {
local pcm="${1:?pcm}"
local since_ts="${2:?since_ts}"
local sink
sink="$(sink_name_for_pcm "$pcm")"
echo
echo "=== VERIFY: pcm=$pcm ==="
if [[ "$pcm" -ne 0 ]]; then
local jack="off"
if read_jack_on_for_pcm "$pcm"; then jack="on"; fi
local eld
eld="$(eld_bytes_len_for_pcm "$pcm" || echo 0)"
local avail total
avail="$(alsa_pcm_subdevices_available "$pcm" || true)"
total="$(alsa_pcm_subdevices_total "$pcm" || true)"
echo "Jack=$jack ELD_bytes=$eld Subdevices=${avail:-?}/${total:-?}"
fi
if ! sink_exists "$sink"; then
warn "Sink missing in PipeWire: $sink"
note "pactl list short sinks (for reference):"
maybe pactl list short sinks
return 2
fi
note "Routing to sink: $sink"
maybe pactl set-default-sink "$sink"
maybe pactl info | sed -n '/默认音频入口/p'
maybe wpctl set-mute @DEFAULT_AUDIO_SINK@ 0
maybe wpctl set-volume @DEFAULT_AUDIO_SINK@ 1.0
local iec
iec="$(iec958_index_for_pcm "$pcm")"
if [[ -n "$iec" ]]; then
maybe amixer -c0 sset "IEC958,${iec}" on
fi
note "pw-play (attempt, 5s timeout)"
local attempt=1
local max_attempts="${retries}"
if ! is_int "$max_attempts" || [[ "$max_attempts" -lt 1 ]]; then
max_attempts=1
fi
local ok=0
while [[ "$attempt" -le "$max_attempts" ]]; do
note "pw-play attempt ${attempt}/${max_attempts}"
if timeout -k 1s 5s pw-play /usr/share/sounds/alsa/Front_Center.wav; then
note "pw-play: OK (exit 0)"
ok=1
break
fi
warn "pw-play: FAILED (non-zero exit)"
if [[ "$attempt" -lt "$max_attempts" ]]; then
note "Restarting user audio services before retry"
restart_user_audio_services
fi
attempt=$((attempt+1))
done
note "Kernel error window since $since_ts (tail)"
local k
k="$(kernel_tail_since_ts "$since_ts")"
if [[ -n "$k" ]]; then
warn "Kernel shows possible SOF/ASoC/HDMI issues in this window:"
printf '%s\n' "$k"
return 1
fi
if [[ "$ok" -eq 1 ]]; then
return 0
fi
return 3
}
verify_audio() {
sec "VERIFY mode (more detailed validation)"
note "This runs active route + playback attempts and captures kernel error windows."
note "Best run right after boot and BEFORE any manual --fix, to catch first-open races."
local since_ts
since_ts="$(date '+%F %T')"
echo "verify_start_ts=$since_ts"
sec "VERIFY snapshot (user services / routing / objects)"
maybe systemctl --user status pipewire pipewire-pulse wireplumber --no-pager
maybe pactl info
maybe pactl list short cards
maybe pactl list cards | sed -n '/cml_rt5682_def/,+220p'
maybe pactl list short sinks
maybe wpctl status
sec "VERIFY WirePlumber state files (default-*)"
maybe ls -la ~/.local/state/wireplumber
maybe sed -n '1,240p' ~/.local/state/wireplumber/default-profile
maybe sed -n '1,240p' ~/.local/state/wireplumber/default-nodes
maybe sed -n '1,240p' ~/.local/state/wireplumber/default-routes
sec "VERIFY ALSA readiness (Jack/ELD/subdevices)"
maybe aplay -l
echo
echo "HDMI/DP Jack + ELD + Subdevices:"
local pcm
for pcm in 2 3 4; do
local jack="off"
if read_jack_on_for_pcm "$pcm"; then jack="on"; fi
local eld
eld="$(eld_bytes_len_for_pcm "$pcm" || echo 0)"
local avail total
avail="$(alsa_pcm_subdevices_available "$pcm" || true)"
total="$(alsa_pcm_subdevices_total "$pcm" || true)"
echo "pcm=$pcm Jack=$jack ELD_bytes=$eld Subdevices=${avail:-?}/${total:-?}"
done
sec "VERIFY per-sink playback attempts (with kernel windows)"
local preferred
preferred="$(detect_available_pcm)"
note "Order preference: detected=$preferred then 3,4,2 then 0"
local pcms=()
if [[ -n "$only_pcm" ]]; then
pcms+=("$only_pcm")
elif [[ "$only_connected" -eq 1 ]]; then
local connected
connected="$(connected_hdmi_pcms)"
if [[ -n "$connected" ]]; then
note "Restricting VERIFY targets due to --only-connected: ${connected}"
# shellcheck disable=SC2206
pcms+=($connected)
else
warn "--only-connected set but no connected HDMI PCMs detected; falling back to pcm=0"
pcms+=(0)
fi
else
pcms+=("$preferred")
for pcm in 3 4 2 0; do
[[ "$pcm" == "$preferred" ]] || pcms+=("$pcm")
done
fi
local ok_any=0
local ok_pcm=""
local ret
for pcm in "${pcms[@]}"; do
if [[ "$pcm" -ne 0 ]]; then
if ! wait_for_hdmi_ready_pcm "$pcm" 6; then
note "pcm=$pcm not ready (Jack/ELD/subdevices); skipping verify attempt"
continue
fi
fi
verify_one_pcm "$pcm" "$since_ts" || ret=$?
ret="${ret:-0}"
if [[ "$ret" -eq 0 ]]; then
ok_any=1
ok_pcm="$pcm"
note "VERIFY succeeded on pcm=$pcm; stopping further attempts to avoid triggering flaky paths."
break
fi
ret=""
# Move the window forward so each attempt isolates new kernel lines.
since_ts="$(date '+%F %T')"
sleep 1
done
sec "VERIFY summary"
if [[ "$ok_any" -eq 1 ]]; then
note "At least one sink attempt completed with no kernel error lines in its window (pcm=${ok_pcm})."
note "Default sink is kept at this successful pcm to preserve working audio."
note "If you still have silence but no kernel errors: focus on routing/monitor input/volume persistence."
else
warn "No sink attempt was clean. If windows show ipc tx error -5 / hw_params failures, this points to kernel/SOF."
fi
}
apply_fix() {
sec "FIX mode (best-effort recovery)"
if [[ "${EUID}" -eq 0 ]]; then
warn "You are running as root. Fix mode must run as your desktop user (no sudo)."
warn "Abort fix to avoid writing state into the wrong user session."
return 1
fi
if [[ -z "${XDG_RUNTIME_DIR-}" || ! -d "${XDG_RUNTIME_DIR-}" ]]; then
warn "XDG_RUNTIME_DIR is missing; likely not in a desktop session. Abort fix."
return 1
fi
note "Restarting user audio services"
restart_user_audio_services
note "Post-restart quick state (forensics)"
maybe pactl info | sed -n '/默认音频入口/p'
maybe pactl list cards | sed -n '/cml_rt5682_def/,+120p'
maybe aplay -l
local card
card="$(get_kaisa_card_name)"
if [[ -z "$card" ]]; then
warn "Could not find card name (expected match: cml_rt5682_def). Abort fix."
return 1
fi
note "Forcing profile to HiFi on card: $card"
maybe pactl set-card-profile "$card" HiFi
maybe sleep 1
local preferred_pcm
preferred_pcm="$(detect_available_pcm)"
note "Detected available pcm (from port availability): $preferred_pcm (preference: 3/4/2, fallback 0)"
# Try a small ordered set. Start with "available" one, but if ALSA reports 0/1 subdevices or playback fails,
# fall back to other HDMI PCMs (3/4/2) then analog (0).
local candidates=()
if [[ -n "$only_pcm" ]]; then
candidates+=("$only_pcm")
note "Restricting FIX candidates due to --only-pcm: $only_pcm"
elif [[ "$only_connected" -eq 1 ]]; then
local connected
connected="$(connected_hdmi_pcms)"
if [[ -n "$connected" ]]; then
note "Restricting FIX candidates due to --only-connected: ${connected}"
# shellcheck disable=SC2206
candidates+=($connected)
# Always keep analog fallback last, in case HDMI opens but remains silent.
candidates+=(0)
else
warn "--only-connected set but no connected HDMI PCMs detected; falling back to pcm=0"
candidates+=(0)
fi
else
candidates+=("$preferred_pcm")
for p in 3 4 2 0; do
[[ "$p" == "$preferred_pcm" ]] || candidates+=("$p")
done
fi
local pcm sink iec avail total
for pcm in "${candidates[@]}"; do
# For HDMI outputs, only try PCMs whose Jack is currently ON.
if [[ "$pcm" -ne 0 ]]; then
if ! wait_for_hdmi_ready_pcm "$pcm" 6; then
note "pcm=$pcm not ready (Jack/ELD/subdevices); skipping"
continue
fi
fi
# Skip clearly broken HDMI devices (subdevices available == 0).
if [[ "$pcm" -ne 0 ]]; then
avail="$(alsa_pcm_subdevices_available "$pcm" || true)"
total="$(alsa_pcm_subdevices_total "$pcm" || true)"
if [[ -n "$avail" && -n "$total" && "$total" -gt 0 && "$avail" -eq 0 ]]; then
warn "pcm=$pcm appears unavailable at ALSA level (subdevices ${avail}/${total}); skipping"
continue
fi
fi
sink="alsa_output.pci-0000_00_1f.3-platform-cml_rt5682_def.HiFi__hw_sofrt5682_${pcm}__sink"
if [[ "$pcm" -eq 0 ]]; then
note "Trying fallback Analog (Port1) sink: $sink"
else
note "Trying HDMI pcm=$pcm sink: $sink"
fi
maybe pactl set-default-sink "$sink"
maybe pactl info | sed -n '/默认音频入口/p'
maybe wpctl set-mute @DEFAULT_AUDIO_SINK@ 0
maybe wpctl set-volume @DEFAULT_AUDIO_SINK@ 1.0
iec="$(iec958_index_for_pcm "$pcm")"
if [[ -n "$iec" ]]; then
note "Enabling IEC958 for pcm=$pcm -> IEC958,$iec"
maybe amixer -c0 sset "IEC958,${iec}" on
fi
note "Quick playback test on pcm=$pcm (timeout -k 1s 5s), retries=${retries}"
local attempt=1
local max_attempts="${retries}"
if ! is_int "$max_attempts" || [[ "$max_attempts" -lt 1 ]]; then
max_attempts=1
fi
local since_ts
since_ts="$(date '+%F %T')"
while [[ "$attempt" -le "$max_attempts" ]]; do
note "pw-play attempt ${attempt}/${max_attempts} (pcm=$pcm)"
if timeout -k 1s 5s pw-play /usr/share/sounds/alsa/Front_Center.wav; then
note "Playback command returned success on pcm=$pcm"
note "Post-play quick checks (kernel/user logs)"
maybe journalctl --user -u pipewire -b --no-pager | tail -n 40
local k
k="$(kernel_tail_since_ts "$since_ts")"
if [[ -n "$k" ]]; then
warn "Kernel window since $since_ts shows possible SOF/ASoC issues:"
printf '%s\n' "$k"
fi
return 0
fi
warn "Playback test failed on pcm=$pcm (attempt ${attempt}/${max_attempts})"
local k
k="$(kernel_tail_since_ts "$since_ts")"
if [[ -n "$k" ]]; then
warn "Kernel window since $since_ts shows possible SOF/ASoC issues:"
printf '%s\n' "$k"
fi
if [[ "$attempt" -lt "$max_attempts" ]]; then
note "Restarting user audio services before retry"
restart_user_audio_services
since_ts="$(date '+%F %T')"
fi
attempt=$((attempt+1))
done
warn "Playback test failed on pcm=$pcm; trying next candidate"
done
warn "Fix mode could not find a working sink automatically. Check cables/monitor input and re-run with only one HDMI plugged."
return 1
}
sec "Kaisa audio doctor (sof-rt5682) — report: $out"
maybe uname -a
maybe date
maybe id
sec "Session sanity (THIS OFTEN EXPLAINS 'no sound')"
echo
echo "If you run this as root / without a logged-in desktop session:"
echo "- systemctl --user will be offline"
echo "- /run/user/\$UID may not exist"
echo "- PipeWire/WirePlumber won't be running"
echo "- ALSA may show 'no soundcards found'"
echo
echo "Current:"
maybe bash -lc 'echo "USER=$USER UID=$UID HOME=$HOME XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR-}"'
maybe bash -lc 'if [ -n "${XDG_RUNTIME_DIR-}" ]; then ls -ld "${XDG_RUNTIME_DIR}" 2>/dev/null || true; else echo "XDG_RUNTIME_DIR is empty"; fi'
maybe bash -lc 'test -S "${XDG_RUNTIME_DIR-}/pipewire-0" && echo "pipewire socket: OK" || echo "pipewire socket: MISSING"'
maybe bash -lc 'test -S "${XDG_RUNTIME_DIR-}/pulse/native" && echo "pulse native socket: OK" || echo "pulse native socket: MISSING"'
echo
if [[ "${EUID}" -eq 0 ]]; then
echo "WARNING: running as root (EUID=0). For best results, run as your desktop user WITHOUT sudo:"
echo " ./scripts/kaisa-audio-doctor.sh"
fi
if [[ "$fix" -eq 1 ]]; then
apply_fix || true
fi
if [[ "$fix_only" -eq 1 ]]; then
exit 0
fi
if [[ "$verify" -eq 1 ]]; then
verify_audio || true
fi
sec "Versions (PipeWire / WirePlumber / ALSA utils)"
maybe pipewire --version
maybe wireplumber --version
maybe wpctl --version
maybe pactl --version
maybe pw-play --version
maybe speaker-test --version
maybe alsaucm --version
maybe amixer --version
maybe aplay --version
sec "User services status"
maybe systemctl --user is-system-running
maybe systemctl --user status pipewire pipewire-pulse wireplumber --no-pager
maybe systemctl --user status pipewire.socket wireplumber.socket --no-pager
sec "ALSA enumeration"
maybe aplay -l
sec "ALSA PCM list (check pipewire/default/pulse)"
maybe aplay -L
sec "UCM sanity"
maybe alsaucm -c sof-rt5682 list _verbs
maybe alsaucm -c sof-rt5682 list _devices
sec "IEC958 switches (all 0/1/2)"
maybe amixer -c0 sget 'IEC958',0
maybe amixer -c0 sget 'IEC958',1
maybe amixer -c0 sget 'IEC958',2
sec "HDMI Jack states (on/off) + ELD controls"
amixer_cget_by_control CARD "HDMI/DP,pcm=2 Jack"
amixer_cget_by_control CARD "HDMI/DP,pcm=3 Jack"
amixer_cget_by_control CARD "HDMI/DP,pcm=4 Jack"
amixer_cget_by_control PCM "ELD" 2
amixer_cget_by_control PCM "ELD" 3
amixer_cget_by_control PCM "ELD" 4
sec "Installed files (system paths)"
maybe ls -l /usr/share/alsa/ucm2/conf.d/sof-rt5682/sof-rt5682.conf
maybe ls -l /usr/share/alsa/ucm2/GoogleKaisa/sof-rt5682/HiFi.conf
maybe ls -l /usr/share/wireplumber/main.lua.d/60-kaisa-ucm.lua
maybe ls -l /usr/share/wireplumber/main.lua.d/60-kaisa-ucm.lua.disabled
sec "Potential conflicting WirePlumber snippets (user/system)"
maybe ls -la ~/.config/wireplumber/wireplumber.conf.d
maybe ls -la ~/.config/wireplumber/wireplumber.conf.d/*kaisa* 2>/dev/null
maybe ls -la /etc/wireplumber/wireplumber.conf.d 2>/dev/null
maybe ls -la /etc/wireplumber/wireplumber.conf.d/*kaisa* 2>/dev/null
sec "WirePlumber state (profile / nodes / routes)"
maybe ls -la ~/.local/state/wireplumber
maybe sed -n '1,200p' ~/.local/state/wireplumber/default-profile
maybe sed -n '1,200p' ~/.local/state/wireplumber/default-nodes
maybe sed -n '1,200p' ~/.local/state/wireplumber/default-routes
sec "WirePlumber default-routes: persisted volume sanity"
routes="${HOME}/.local/state/wireplumber/default-routes"
if [[ -f "$routes" ]]; then
low_lines="$(awk -F'channelVolumes=' '
NF >= 2 {
rest = $2
sub(/;.*/, "", rest)
v = rest + 0.0
if (v > 0 && v < 0.25) print $0
}
' "$routes" 2>/dev/null || true)"
if [[ -n "$low_lines" ]]; then
warn "Persisted channelVolumes below ~0.25 in default-routes (can sound like silence after reboot):"
printf '%s\n' "$low_lines"
note "See docs/linux-hdmi/ROOTCAUSE_Reboot_Silent_Analysis.md"
else
note "No suspiciously low channelVolumes lines in default-routes (threshold 0.25)."
fi
else
note "No default-routes file (yet)."
fi
sec "PipeWire card / profile / ports (focus: cml_rt5682_def)"
maybe pactl list cards short
maybe pactl list cards | sed -n '/cml_rt5682_def/,+220p'
sec "Sinks (PipeWire) + default sink"
maybe pactl info
maybe pactl list short sinks
maybe wpctl status
sec "Kernel hints (SOF/HDMI hw_params IPC errors)"
echo
echo "Note: if you see PipeWire 'set_hw_params: Input/output error', the real cause is often in kernel logs."
echo " This section tries to summarize SOF/ASoC HDMI failures (e.g. ipc tx error -5 for pcm2 HDMI1)."
kernel_snip="$(
journalctl -k -b --no-pager 2>/dev/null | \
grep -nE 'sof-audio|sof_ipc3_pcm_hw_params|ipc tx error|STREAM_PCM_PARAMS|ASoC error|HDMI1|pcm2' 2>/dev/null | \
tail -n 120 || true
)"
if [[ -n "${kernel_snip}" ]]; then
warn "Kernel log shows SOF/ASoC HDMI failures (tail):"
printf '%s\n' "${kernel_snip}"
note "If it mentions: sof_ipc3_pcm_hw_params: pcm2 (HDMI1) ... ipc failed ... -5"
note "then this is a kernel/SOF issue (not UCM/WirePlumber). Capture full: journalctl -k -b | grep -nE 'sof-audio|ipc tx error|pcm2|HDMI1'"
else
note "No matching kernel SOF/HDMI error lines found (or insufficient permission to read kernel journal)."
fi
sec "Quick playback tests (non-destructive)"
echo
echo "Note: if ALSA 'pulse' PCM is missing, do NOT use: speaker-test -D pulse"
echo "Try these instead (they use PipeWire):"
echo
echo "Tip: these are wrapped with a short timeout to avoid hanging."
echo " (uses: timeout -k 1s 5s ... -> TERM then KILL)"
maybe timeout -k 1s 5s speaker-test -D pipewire -c2 -t sine -f 440 -l 1
maybe timeout -k 1s 5s speaker-test -D default -c2 -t sine -f 440 -l 1
maybe timeout -k 1s 5s pw-play /usr/share/sounds/alsa/Front_Center.wav
maybe timeout -k 1s 5s paplay /usr/share/sounds/alsa/Front_Center.wav
sec "Recent logs (journalctl --user, current boot)"
maybe journalctl --user -u wireplumber -b --no-pager -n 200
maybe journalctl --user -u pipewire -b --no-pager -n 200
maybe journalctl --user -u pipewire-pulse -b --no-pager -n 200
sec "Hints"
cat <<'EOF'
- If HDMI ports show "not available", verify cable/monitor input/EDID and re-plug.
- If profile keeps reverting after reboot, compare:
- ~/.local/state/wireplumber/default-profile
- /usr/share/wireplumber/main.lua.d/60-kaisa-ucm.lua (device.profile)
- any *kaisa* snippets under ~/.config/wireplumber/ or /etc/wireplumber/
- If set_hw_params errors appear in logs for a given pcm (2/3/4), test only ONE HDMI at a time and switch sink accordingly.
EOF
hr
echo "Done. Report saved to: $out"