lesavka/scripts/manual/run_uac_output_sanity.sh

152 lines
4.2 KiB
Bash

#!/usr/bin/env bash
# scripts/manual/run_uac_output_sanity.sh - play a short tone into the Theia UAC sink
set -euo pipefail
if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
echo "❌ run this as root so it uses the same UAC leg as lesavka-server." >&2
exit 1
fi
SERVER_ENV=${LESAVKA_SERVER_ENV:-/etc/lesavka/server.env}
if [[ -r $SERVER_ENV ]]; then
set -a
# shellcheck disable=SC1090
source "$SERVER_ENV"
set +a
fi
DURATION_SECONDS=${LESAVKA_UAC_SANITY_SECONDS:-4}
TONE_FREQ=${LESAVKA_UAC_SANITY_FREQ:-880}
TONE_VOLUME=${LESAVKA_UAC_SANITY_VOLUME:-0.45}
BUFFER_TIME_US=${LESAVKA_UAC_BUFFER_TIME_US:-20000}
LATENCY_TIME_US=${LESAVKA_UAC_LATENCY_TIME_US:-5000}
REQUESTED_DEV=${LESAVKA_UAC_SANITY_DEV:-${LESAVKA_UAC_DEV:-hw:UAC2Gadget,0}}
declare -A SEEN_CANDIDATES=()
CANDIDATES=()
append_candidate() {
local candidate="${1:-}"
candidate=${candidate//[$'\r\n']/}
candidate=${candidate#"${candidate%%[![:space:]]*}"}
candidate=${candidate%"${candidate##*[![:space:]]}"}
[[ -n $candidate ]] || return 0
if [[ -z ${SEEN_CANDIDATES[$candidate]+x} ]]; then
CANDIDATES+=("$candidate")
SEEN_CANDIDATES["$candidate"]=1
fi
}
append_candidate_family() {
local candidate="${1:-}"
[[ -n ${candidate//[[:space:]]/} ]] || return 0
append_candidate "$candidate"
if [[ $candidate == hw:* ]]; then
append_candidate "plughw:${candidate#hw:}"
elif [[ $candidate == plughw:* ]]; then
append_candidate "hw:${candidate#plughw:}"
fi
}
discover_uac_numeric_candidates() {
python3 - <<'PY'
import re
import subprocess
try:
output = subprocess.check_output(["aplay", "-l"], text=True, stderr=subprocess.DEVNULL)
except Exception:
raise SystemExit(0)
seen = set()
for line in output.splitlines():
match = re.search(r"^card\s+(\d+):.*device\s+(\d+):", line)
if not match:
continue
lowered = line.lower()
if not any(token in lowered for token in ("uac", "lesavka", "composite", "usb audio")):
continue
candidate = f"hw:{match.group(1)},{match.group(2)}"
if candidate not in seen:
print(candidate)
seen.add(candidate)
PY
}
build_candidates() {
while IFS= read -r candidate; do
append_candidate_family "$candidate"
done < <(discover_uac_numeric_candidates)
append_candidate_family "$REQUESTED_DEV"
for alias in \
"hw:UAC2Gadget,0" \
"hw:UAC2_Gadget,0" \
"hw:Composite,0" \
"hw:Lesavka,0"
do
append_candidate_family "$alias"
done
}
run_tone() {
local candidate="$1"
local log
log=$(mktemp)
echo "🎧 trying UAC playback device: $candidate" >&2
set +e
timeout --signal=INT "${DURATION_SECONDS}s" \
gst-launch-1.0 -q \
audiotestsrc wave=sine freq="$TONE_FREQ" volume="$TONE_VOLUME" is-live=true \
! audio/x-raw,format=S16LE,channels=2,rate=48000 \
! audioconvert \
! audioresample \
! alsasink \
device="$candidate" \
sync=false \
async=false \
provide-clock=false \
enable-last-sample=false \
buffer-time="$BUFFER_TIME_US" \
latency-time="$LATENCY_TIME_US" \
2>"$log"
local rc=$?
set -e
if grep -Eiq 'unknown pcm|playback open error|invalid argument|no such (device|file)|^error:|could not open|failed to change state|not-negotiated' "$log"; then
echo "⚠️ tone failed on $candidate (sink open error)" >&2
sed 's/^/ /' "$log" >&2 || true
rm -f "$log"
return 1
fi
if [[ $rc -eq 0 || $rc -eq 124 ]]; then
echo "✅ UAC tone completed on $candidate" >&2
rm -f "$log"
return 0
fi
echo "⚠️ tone failed on $candidate (rc=$rc)" >&2
sed 's/^/ /' "$log" >&2 || true
rm -f "$log"
return "$rc"
}
build_candidates
if [[ ${#CANDIDATES[@]} -eq 0 ]]; then
echo "❌ no UAC playback candidates found. Check 'aplay -l' and /etc/lesavka/server.env." >&2
exit 1
fi
echo "🎵 running Theia UAC sanity tone for ${DURATION_SECONDS}s at ${TONE_FREQ} Hz" >&2
printf ' candidates: %s\n' "${CANDIDATES[*]}" >&2
for candidate in "${CANDIDATES[@]}"; do
if run_tone "$candidate"; then
exit 0
fi
done
echo "❌ none of the UAC playback candidates accepted the sanity tone." >&2
echo " requested device: $REQUESTED_DEV" >&2
echo " current aplay -l:" >&2
aplay -l >&2 || true
exit 1