160 lines
4.5 KiB
Bash
Executable File
160 lines
4.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# scripts/manual/run_uac_output_sanity.sh - play a short tone into the Theia UAC sink
|
|
# Manual: Theia UAC output sanity probe; not part of CI.
|
|
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}
|
|
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
|
|
local wav
|
|
log=$(mktemp)
|
|
wav=$(mktemp --suffix=.wav)
|
|
echo "🎧 trying UAC playback device: $candidate" >&2
|
|
python3 - "$wav" "$DURATION_SECONDS" "$TONE_FREQ" "$TONE_VOLUME" <<'PY'
|
|
import math, struct, sys, wave
|
|
|
|
path, duration_s, freq_hz, volume = sys.argv[1], float(sys.argv[2]), float(sys.argv[3]), float(sys.argv[4])
|
|
rate = 48_000
|
|
frames = max(1, int(rate * duration_s))
|
|
amp = max(0.0, min(volume, 1.0)) * 32767.0
|
|
with wave.open(path, "wb") as wf:
|
|
wf.setnchannels(2)
|
|
wf.setsampwidth(2)
|
|
wf.setframerate(rate)
|
|
for index in range(frames):
|
|
sample = int(math.sin((2.0 * math.pi * freq_hz * index) / rate) * amp)
|
|
frame = struct.pack("<hh", sample, sample)
|
|
wf.writeframesraw(frame)
|
|
PY
|
|
set +e
|
|
timeout --signal=INT "$((DURATION_SECONDS + 2))s" \
|
|
aplay -q -D "$candidate" "$wav" \
|
|
>"$log" 2>&1
|
|
local rc=$?
|
|
set -e
|
|
if grep -Eiq 'unknown pcm|invalid argument|no such (device|file)|audio open error|device or resource busy|unable to open slave|unable to install hw params|unable to set hw params|audio open error|no such file or directory' "$log"; then
|
|
echo "⚠️ tone failed on $candidate (sink open error)" >&2
|
|
sed 's/^/ /' "$log" >&2 || true
|
|
rm -f "$log"
|
|
rm -f "$wav"
|
|
return 1
|
|
fi
|
|
if [[ $rc -eq 0 || $rc -eq 124 ]]; then
|
|
echo "✅ UAC tone completed on $candidate" >&2
|
|
rm -f "$log"
|
|
rm -f "$wav"
|
|
return 0
|
|
fi
|
|
echo "⚠️ tone failed on $candidate (rc=$rc)" >&2
|
|
sed 's/^/ /' "$log" >&2 || true
|
|
rm -f "$log"
|
|
rm -f "$wav"
|
|
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
|