2026-04-24 21:27:19 -03:00
#!/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 "
2026-04-24 21:49:29 -03:00
local log
log = $( mktemp)
2026-04-24 21:27:19 -03:00
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 " \
2026-04-24 21:49:29 -03:00
latency-time= " $LATENCY_TIME_US " \
2>" $log "
2026-04-24 21:27:19 -03:00
local rc = $?
set -e
2026-04-24 21:49:29 -03:00
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
2026-04-24 21:27:19 -03:00
if [ [ $rc -eq 0 || $rc -eq 124 ] ] ; then
echo " ✅ UAC tone completed on $candidate " >& 2
2026-04-24 21:49:29 -03:00
rm -f " $log "
2026-04-24 21:27:19 -03:00
return 0
fi
echo " ⚠️ tone failed on $candidate (rc= $rc ) " >& 2
2026-04-24 21:49:29 -03:00
sed 's/^/ /' " $log " >& 2 || true
rm -f " $log "
2026-04-24 21:27:19 -03:00
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