#!/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