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:
800
scripts/kaisa-audio-doctor.sh
Executable file
800
scripts/kaisa-audio-doctor.sh
Executable 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"
|
||||
Reference in New Issue
Block a user