973 lines
32 KiB
Bash
Executable File
973 lines
32 KiB
Bash
Executable File
#!/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
|
|
verify_strict=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] [--verify-strict] [--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
|
|
--verify-strict Implies --verify; after the full report, exit non-zero if VERIFY found no clean sink (for watchdogs/CI)
|
|
|
|
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 (required; no pro-audio fallback)
|
|
- 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 ;;
|
|
--verify-strict) verify=1; verify_strict=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]+$ ]]; }
|
|
|
|
PLAY_TIMEOUT_S="${KAISA_PLAY_TIMEOUT_S:-8}"
|
|
PLAY_KILL_AFTER_S="${KAISA_PLAY_KILL_AFTER_S:-1}"
|
|
|
|
get_kaisa_card_name() {
|
|
pactl list cards short 2>/dev/null | awk '/cml_rt5682_def/ {print $2; exit}'
|
|
}
|
|
|
|
wait_for_card() {
|
|
# Wait for PipeWire/ACP to enumerate the card after service restart.
|
|
local timeout_s="${1:-6}"
|
|
local i
|
|
for i in $(seq 1 "$timeout_s"); do
|
|
local card
|
|
card="$(get_kaisa_card_name || true)"
|
|
if [[ -n "$card" ]]; then
|
|
echo "$card"
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
return 1
|
|
}
|
|
|
|
card_has_profile() {
|
|
# Usage: card_has_profile "<card_name>" "HiFi"
|
|
local card="${1:?card}"
|
|
local prof="${2:?profile}"
|
|
pactl list cards 2>/dev/null | awk -v c="$card" -v p="$prof" '
|
|
$0 ~ ("名称:[ \t]*" c "$") { in_card=1; next }
|
|
in_card && $0 ~ /^名称:[ \t]*/ { in_card=0 }
|
|
in_card && $0 ~ ("^[ \t]*" p ":") { found=1; exit }
|
|
END { exit !found }
|
|
'
|
|
}
|
|
|
|
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 kernel_pcm_is_bad "$pcm"; then
|
|
warn "Kernel shows pcm=${pcm} hw_params failures in this boot; skipping from connected set"
|
|
continue
|
|
fi
|
|
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}"
|
|
# Delivery requirement: must be managed by UCM/HiFi.
|
|
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
|
|
}
|
|
|
|
user_pipewire_tail_since_ts() {
|
|
# PipeWire user-service errors since ts (best-effort).
|
|
local since_ts="${1:?since_ts}"
|
|
journalctl --user -u pipewire -b --since "$since_ts" --no-pager 2>/dev/null | tail -n 120 || true
|
|
}
|
|
|
|
kernel_pcm_is_bad() {
|
|
# Return 0 if kernel has shown SOF/ASoC hw_params failures for this PCM in current boot.
|
|
# This is used as a guardrail to avoid re-probing a PCM that is known to brick HDMI audio in this session.
|
|
local pcm="${1:?pcm}"
|
|
journalctl -k -b --no-pager 2>/dev/null | \
|
|
grep -qE "sof_ipc3_pcm_hw_params: pcm${pcm} \\(HDMI|STREAM_PCM_PARAMS.*pcm${pcm}.*failed|HDMI[0-9]+: ASoC error .*pcm${pcm}" \
|
|
&& return 0
|
|
return 1
|
|
}
|
|
|
|
restart_user_audio_services() {
|
|
maybe systemctl --user restart pipewire pipewire-pulse wireplumber
|
|
maybe sleep 2
|
|
}
|
|
|
|
clamp_wireplumber_default_routes_min_volume() {
|
|
# WirePlumber persists per-port volumes in ~/.local/state/wireplumber/default-routes.
|
|
# On this machine, HDMI ports sometimes get persisted to very low values (e.g. 0.06~0.12),
|
|
# which feels like "no sound" after reboot even when routing is correct.
|
|
#
|
|
# This function is intentionally conservative:
|
|
# - only edits lines containing channelVolumes=
|
|
# - only bumps values that are >0 and < threshold
|
|
local threshold="${1:-0.25}"
|
|
local routes="${HOME}/.local/state/wireplumber/default-routes"
|
|
[[ -f "$routes" ]] || return 0
|
|
|
|
# If awk fails for any reason, do nothing (best-effort).
|
|
local tmp
|
|
tmp="$(mktemp)"
|
|
if awk -v th="$threshold" '
|
|
function should_bump(line, n, rest, v) {
|
|
n = split(line, a, "channelVolumes=")
|
|
if (n < 2) return 0
|
|
rest = a[2]
|
|
sub(/;.*/, "", rest)
|
|
v = rest + 0.0
|
|
if (v > 0 && v < th) return 1
|
|
return 0
|
|
}
|
|
{
|
|
if ($0 ~ /channelVolumes=/ && should_bump($0)) {
|
|
sub(/channelVolumes=[0-9.]+;[0-9.]+;/, "channelVolumes=1.0;1.0;")
|
|
}
|
|
print
|
|
}
|
|
' "$routes" >"$tmp" 2>/dev/null; then
|
|
if ! cmp -s "$routes" "$tmp" 2>/dev/null; then
|
|
mv "$tmp" "$routes"
|
|
note "Clamped low WirePlumber route volumes in default-routes (threshold ${threshold})"
|
|
else
|
|
rm -f "$tmp"
|
|
fi
|
|
else
|
|
rm -f "$tmp"
|
|
fi
|
|
}
|
|
|
|
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}"
|
|
timeout -k "${PLAY_KILL_AFTER_S}s" "${PLAY_TIMEOUT_S}s" pw-play /usr/share/sounds/alsa/Front_Center.wav
|
|
local rc=$?
|
|
if [[ "$rc" -eq 0 ]]; then
|
|
note "pw-play: OK (exit 0)"
|
|
ok=1
|
|
break
|
|
fi
|
|
warn "pw-play: FAILED (exit ${rc})"
|
|
if [[ "$rc" -eq 124 ]]; then
|
|
warn "pw-play timed out (${PLAY_TIMEOUT_S}s). This can indicate a hung open/stream; capture PipeWire errors below."
|
|
fi
|
|
note "PipeWire (user) log tail since $since_ts"
|
|
local u
|
|
u="$(user_pipewire_tail_since_ts "$since_ts")"
|
|
if [[ -n "$u" ]]; then
|
|
printf '%s\n' "$u"
|
|
fi
|
|
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."
|
|
return 0
|
|
fi
|
|
warn "No sink attempt was clean. If windows show ipc tx error -5 / hw_params failures, this points to kernel/SOF."
|
|
return 1
|
|
}
|
|
|
|
persist_wireplumber_default_profile_hifi() {
|
|
# WirePlumber restores card profile from this file on user-session start.
|
|
# If it contains pro-audio, it will override our manual pactl set-card-profile HiFi.
|
|
local f="${HOME}/.local/state/wireplumber/default-profile"
|
|
[[ -f "$f" ]] || return 0
|
|
local tmp
|
|
tmp="$(mktemp)"
|
|
if awk '
|
|
BEGIN { changed=0 }
|
|
{
|
|
if ($0 ~ /^alsa_card\.pci-0000_00_1f\.3-platform-cml_rt5682_def=/) {
|
|
if ($0 !~ /=HiFi$/) { changed=1 }
|
|
print "alsa_card.pci-0000_00_1f.3-platform-cml_rt5682_def=HiFi"
|
|
next
|
|
}
|
|
print
|
|
}
|
|
END {
|
|
# If mapping line does not exist, append it.
|
|
if (NR > 0 && !seen) {}
|
|
}
|
|
' "$f" >"$tmp" 2>/dev/null; then
|
|
# Ensure the mapping line exists (append if missing)
|
|
if ! grep -q '^alsa_card\.pci-0000_00_1f\.3-platform-cml_rt5682_def=' "$tmp" 2>/dev/null; then
|
|
printf '\nalsa_card.pci-0000_00_1f.3-platform-cml_rt5682_def=HiFi\n' >>"$tmp"
|
|
fi
|
|
if ! cmp -s "$f" "$tmp" 2>/dev/null; then
|
|
mv "$tmp" "$f"
|
|
note "Persisted WirePlumber default-profile to HiFi (${f})"
|
|
else
|
|
rm -f "$tmp"
|
|
fi
|
|
else
|
|
rm -f "$tmp"
|
|
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
|
|
|
|
# Preemptively fix the most common "looks routed but silent after reboot" trap.
|
|
clamp_wireplumber_default_routes_min_volume 0.25
|
|
|
|
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="$(wait_for_card 8 || true)"
|
|
if [[ -z "$card" ]]; then
|
|
warn "Could not find card name (expected match: cml_rt5682_def). Abort fix."
|
|
return 1
|
|
fi
|
|
|
|
local target_profile="HiFi"
|
|
if ! card_has_profile "$card" "HiFi"; then
|
|
warn "Card does not expose HiFi profile in pactl. This violates the 'HiFi-only' requirement."
|
|
warn "Fix: ensure UCM overlay is installed and PipeWire enumerates HiFi, then reboot/restart user audio services."
|
|
return 1
|
|
fi
|
|
|
|
# Ensure WirePlumber won't revert profile back to pro-audio on restart.
|
|
persist_wireplumber_default_profile_hifi
|
|
|
|
note "Setting card profile to ${target_profile} on card: $card"
|
|
maybe pactl set-card-profile "$card" "$target_profile"
|
|
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
|
|
if [[ "$pcm" -ne 0 ]] && kernel_pcm_is_bad "$pcm"; then
|
|
warn "Skipping pcm=$pcm due to kernel SOF/ASoC failure signature in this boot"
|
|
continue
|
|
fi
|
|
# 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)"
|
|
else
|
|
note "Trying HDMI pcm=$pcm"
|
|
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
|
|
|
|
verify_rc=0
|
|
if [[ "$verify" -eq 1 ]]; then
|
|
if [[ "$verify_strict" -eq 1 ]]; then
|
|
verify_audio && verify_rc=0 || verify_rc=$?
|
|
else
|
|
verify_audio || true
|
|
fi
|
|
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 "${PLAY_KILL_AFTER_S}s" "${PLAY_TIMEOUT_S}s" speaker-test -D pipewire -c2 -t sine -f 440 -l 1
|
|
maybe timeout -k "${PLAY_KILL_AFTER_S}s" "${PLAY_TIMEOUT_S}s" speaker-test -D default -c2 -t sine -f 440 -l 1
|
|
maybe timeout -k "${PLAY_KILL_AFTER_S}s" "${PLAY_TIMEOUT_S}s" pw-play /usr/share/sounds/alsa/Front_Center.wav
|
|
maybe timeout -k "${PLAY_KILL_AFTER_S}s" "${PLAY_TIMEOUT_S}s" 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"
|
|
if [[ "$verify" -eq 1 && "$verify_strict" -eq 1 ]]; then
|
|
exit "${verify_rc:-1}"
|
|
fi
|