@@ -140,6 +140,9 @@ log_info "Args: backend=${AUDIO_BACKEND:-auto} source=$SRC_CHOICE loops=$LOOPS d
140140if [ -z " $AUDIO_BACKEND " ]; then
141141 AUDIO_BACKEND=" $( detect_audio_backend) "
142142fi
143+ BACKENDS_TO_TRY=" $( build_backend_chain) "
144+ # Use it for visibility and to satisfy shellcheck usage
145+ log_info " Backend fallback chain: $BACKENDS_TO_TRY "
143146if [ -z " $AUDIO_BACKEND " ]; then
144147 log_skip " $TESTNAME SKIP - no audio backend running"
145148 echo " $TESTNAME SKIP" > " $RES_FILE "
@@ -185,31 +188,126 @@ case "$AUDIO_BACKEND:$SRC_CHOICE" in
185188 ;;
186189esac
187190
188- if [ -z " $SRC_ID " ]; then
189- log_skip " $TESTNAME SKIP - requested source '$SRC_CHOICE ' not found for $AUDIO_BACKEND "
191+ # ---- Dynamic fallback when mic is missing on the chosen backend ----
192+ # Stay on PipeWire even if SRC_ID is empty; pw-record and arecord -D pipewire can use the default source.
193+ if [ -z " $SRC_ID " ] && [ " $SRC_CHOICE " = " mic" ] && [ " $AUDIO_BACKEND " != " pipewire" ]; then
194+ for b in $BACKENDS_TO_TRY ; do
195+ [ " $b " = " $AUDIO_BACKEND " ] && continue
196+ case " $b " in
197+ pipewire)
198+ cand=" $( pw_default_mic) "
199+ if [ -n " $cand " ]; then
200+ AUDIO_BACKEND=" pipewire" ; SRC_ID=" $cand "
201+ log_info " Falling back to backend: pipewire (source id=$SRC_ID )"
202+ break
203+ fi
204+ ;;
205+ pulseaudio)
206+ cand=" $( pa_default_mic) "
207+ if [ -n " $cand " ]; then
208+ AUDIO_BACKEND=" pulseaudio" ; SRC_ID=" $cand "
209+ log_info " Falling back to backend: pulseaudio (source=$SRC_ID )"
210+ break
211+ fi
212+ ;;
213+ alsa)
214+ cand=" $( alsa_pick_capture) "
215+ if [ -n " $cand " ]; then
216+ AUDIO_BACKEND=" alsa" ; SRC_ID=" $cand "
217+ log_info " Falling back to backend: alsa (device=$SRC_ID )"
218+ break
219+ fi
220+ ;;
221+ esac
222+ done
223+ fi
224+
225+ # Only skip if no source AND not on PipeWire.
226+ if [ -z " $SRC_ID " ] && [ " $AUDIO_BACKEND " != " pipewire" ]; then
227+ log_skip " $TESTNAME SKIP - requested source '$SRC_CHOICE ' not available on any backend ($BACKENDS_TO_TRY )"
190228 echo " $TESTNAME SKIP" > " $RES_FILE "
191229 exit 2
192230fi
193231
232+ # ---- Normalize ALSA device id (fix "hw:0 1," → "hw:0,1") ----
233+ if [ " $AUDIO_BACKEND " = " alsa" ]; then
234+ case " $SRC_ID " in
235+ hw:* " " * ,)
236+ SRC_ID=$( printf ' %s' " $SRC_ID " | sed -E ' s/^hw:([0-9]+) ([0-9]+),$/hw:\1,\2/' )
237+ ;;
238+ hw:* " " * )
239+ SRC_ID=$( printf ' %s' " $SRC_ID " | sed -E ' s/^hw:([0-9]+) ([0-9]+)$/hw:\1,\2/' )
240+ ;;
241+ esac
242+ fi
243+
244+ # ---- Validate/auto-pick ALSA device if invalid (prevents "hw:,") ----
245+ if [ " $AUDIO_BACKEND " = " alsa" ]; then
246+ case " $SRC_ID " in
247+ hw:[0-9]* ,[0-9]* |plughw:[0-9]* ,[0-9]* )
248+ : ;;
249+ * )
250+ cand=" $( arecord -l 2> /dev/null | sed -n ' s/^card[[:space:]]*\([0-9][0-9]*\).*device[[:space:]]*\([0-9][0-9]*\).*/hw:\1,\2/p' | head -n 1) "
251+ if [ -z " $cand " ]; then
252+ cand=" $( sed -n ' s/^\([0-9][0-9]*\)-\([0-9][0-9]*\):.*capture.*/hw:\1,\2/p' /proc/asound/pcm 2> /dev/null | head -n 1) "
253+ fi
254+ if [ -z " $cand " ]; then
255+ cand=" $( sed -n ' s/.*\[\s*\([0-9][0-9]*\)-\s*\([0-9][0-9]*\)\]:.*capture.*/hw:\1,\2/p' /proc/asound/devices 2> /dev/null | head -n 1) "
256+ fi
257+ if printf ' %s\n' " $cand " | grep -Eq ' ^hw:[0-9]+,[0-9]+$' ; then
258+ SRC_ID=" $cand "
259+ log_info " ALSA auto-pick: using $SRC_ID "
260+ else
261+ log_skip " $TESTNAME SKIP - no valid ALSA capture device found"
262+ echo " $TESTNAME SKIP" > " $RES_FILE "
263+ exit 2
264+ fi
265+ ;;
266+ esac
267+ fi
268+
269+ # ---- Routing log / defaults per backend ----
194270if [ " $AUDIO_BACKEND " = " pipewire" ]; then
195- SRC_LABEL=" $( pw_source_label_safe " $SRC_ID " ) "
196- wpctl set-default " $SRC_ID " > /dev/null 2>&1 || true
197- if [ -z " $SRC_LABEL " ]; then
198- SRC_LABEL=" unknown"
271+ if [ -n " $SRC_ID " ]; then
272+ SRC_LABEL=" $( pw_source_label_safe " $SRC_ID " ) "
273+ wpctl set-default " $SRC_ID " > /dev/null 2>&1 || true
274+ [ -z " $SRC_LABEL " ] && SRC_LABEL=" unknown"
275+ log_info " Routing to source: id/name=$SRC_ID label='$SRC_LABEL ' choice=$SRC_CHOICE "
276+ else
277+ SRC_LABEL=" default"
278+ log_info " Routing to source: id/name=default label='default' choice=$SRC_CHOICE "
199279 fi
200- log_info " Routing to source: id/name=$SRC_ID label='$SRC_LABEL ' choice=$SRC_CHOICE "
201- else
280+ elif [ " $AUDIO_BACKEND " = " pulseaudio" ]; then
202281 SRC_LABEL=" $( pa_source_name " $SRC_ID " 2> /dev/null || echo " $SRC_ID " ) "
203282 pa_set_default_source " $SRC_ID " > /dev/null 2>&1 || true
204283 log_info " Routing to source: name='$SRC_LABEL ' choice=$SRC_CHOICE "
284+ else # ALSA
285+ SRC_LABEL=" $SRC_ID "
286+ log_info " Routing to source: name='$SRC_LABEL ' choice=$SRC_CHOICE "
205287fi
206288
289+ # If fallback changed backend, ensure deps are present (non-fatal → SKIP)
290+ case " $AUDIO_BACKEND " in
291+ pipewire)
292+ if ! check_dependencies wpctl pw-record; then
293+ log_skip " $TESTNAME SKIP - missing PipeWire utils"
294+ echo " $TESTNAME SKIP" > " $RES_FILE " ; exit 2
295+ fi ;;
296+ pulseaudio)
297+ if ! check_dependencies pactl parecord; then
298+ log_skip " $TESTNAME SKIP - missing PulseAudio utils"
299+ echo " $TESTNAME SKIP" > " $RES_FILE " ; exit 2
300+ fi ;;
301+ alsa)
302+ if ! check_dependencies arecord; then
303+ log_skip " $TESTNAME SKIP - missing arecord"
304+ echo " $TESTNAME SKIP" > " $RES_FILE " ; exit 2
305+ fi ;;
306+ esac
307+
207308# Watchdog info
208309dur_s=" $( duration_to_secs " $TIMEOUT " 2> /dev/null || echo 0) "
209- if [ -z " $dur_s " ]; then
210- dur_s=0
211- fi
212-
310+ [ -z " $dur_s " ] && dur_s=0
213311if [ " $dur_s " -gt 0 ] 2> /dev/null; then
214312 log_info " Watchdog/timeout: ${TIMEOUT} "
215313else
@@ -240,12 +338,8 @@ append_junit() {
240338 {
241339 printf ' <testcase classname="%s" name="%s" time="%s">\n' " Audio.Record" " $name " " $elapsed "
242340 case " $status " in
243- PASS)
244- :
245- ;;
246- SKIP)
247- printf ' <skipped/>\n'
248- ;;
341+ PASS) : ;;
342+ SKIP) printf ' <skipped/>\n' ;;
249343 FAIL)
250344 printf ' <failure message="%s">\n' " failed"
251345 printf ' %s\n' " $safe_msg "
@@ -266,6 +360,22 @@ auto_secs_for() {
266360 esac
267361}
268362
363+ # Prefer virtual capture PCMs (PipeWire/Pulse) over raw hw: when a sound server is present
364+ alsa_pick_virtual_pcm () {
365+ command -v arecord > /dev/null 2>&1 || return 1
366+
367+ pcs=" $( arecord -L 2> /dev/null | sed -n ' s/^[[:space:]]*\([[:alnum:]_]\+\)[[:space:]]*$/\1/p' ) "
368+
369+ for pcm in pipewire pulse default; do
370+ if printf ' %s\n' " $pcs " | grep -m1 -x " $pcm " > /dev/null 2>&1 ; then
371+ printf ' %s\n' " $pcm "
372+ return 0
373+ fi
374+ done
375+
376+ return 1
377+ }
378+
269379# ---------------- Matrix execution ----------------
270380total=0
271381pass=0
@@ -305,7 +415,11 @@ for dur in $DURATIONS; do
305415
306416 loop_hdr=" source=$SRC_CHOICE "
307417 if [ " $AUDIO_BACKEND " = " pipewire" ]; then
308- loop_hdr=" $loop_hdr ($SRC_ID )"
418+ if [ -n " $SRC_ID " ]; then
419+ loop_hdr=" $loop_hdr ($SRC_ID )"
420+ else
421+ loop_hdr=" $loop_hdr (default)"
422+ fi
309423 else
310424 loop_hdr=" $loop_hdr ($SRC_LABEL )"
311425 fi
@@ -321,48 +435,101 @@ for dur in $DURATIONS; do
321435 log_info " [$case_name ] exec: pw-record -v \" $out \" "
322436 audio_exec_with_timeout " $effective_timeout " pw-record -v " $out " >> " $logf " 2>&1
323437 rc=$?
324-
325438 bytes=" $( stat -c ' %s' " $out " 2> /dev/null || wc -c < " $out " ) "
326439
440+ # If we already got real audio, accept and skip fallbacks
441+ if [ " ${bytes:- 0} " -gt 1024 ] 2> /dev/null; then
442+ if [ " $rc " -ne 0 ]; then
443+ log_warn " [$case_name ] nonzero rc=$rc but recording looks valid (bytes=$bytes ) - PASS"
444+ rc=0
445+ fi
446+ else
447+ # Only if output is tiny/empty do we try a virtual PCM (pipewire/pulse/default)
448+ if command -v arecord > /dev/null 2>&1 ; then
449+ pcm=" $( alsa_pick_virtual_pcm || true) "
450+ if [ -n " $pcm " ]; then
451+ secs_int=" $( audio_parse_secs " $secs " 2> /dev/null || echo 0) " ; [ -z " $secs_int " ] && secs_int=0
452+ : > " $out "
453+ log_info " [$case_name ] fallback: arecord -D $pcm -f S16_LE -r 48000 -c 2 -d $secs_int \" $out \" "
454+ audio_exec_with_timeout " $effective_timeout " \
455+ arecord -D " $pcm " -f S16_LE -r 48000 -c 2 -d " $secs_int " " $out " >> " $logf " 2>&1
456+ rc=$?
457+ bytes=" $( stat -c ' %s' " $out " 2> /dev/null || wc -c < " $out " ) "
458+ fi
459+ fi
460+
461+ # As a last resort, retry pw-record with --target (only if we have a source id)
462+ if { [ " $rc " -ne 0 ] || [ " ${bytes:- 0} " -le 1024 ] 2> /dev/null; } && [ -n " $SRC_ID " ]; then
463+ : > " $out "
464+ log_info " [$case_name ] exec: pw-record -v --target \" $SRC_ID \" \" $out \" "
465+ audio_exec_with_timeout " $effective_timeout " pw-record -v --target " $SRC_ID " " $out " >> " $logf " 2>&1
466+ rc=$?
467+ bytes=" $( stat -c ' %s' " $out " 2> /dev/null || wc -c < " $out " ) "
468+ fi
469+ fi
470+
471+ # (Optional safety) If nonzero rc but output is clearly valid, accept.
327472 if [ " $rc " -ne 0 ] && [ " ${bytes:- 0} " -gt 1024 ] 2> /dev/null; then
328- log_warn " [$case_name ] nonzero rc=$rc but recording looks valid (bytes=$bytes ) - PASS"
473+ log_warn " [$case_name ] nonzero rc== $rc but recording looks valid (bytes=$bytes ) - PASS"
329474 rc=0
330475 fi
476+ else
477+ if [ " $AUDIO_BACKEND " = " alsa" ]; then
478+ secs_int=" $( audio_parse_secs " $secs " 2> /dev/null || echo 0) "
479+ [ -z " $secs_int " ] && secs_int=0
480+ log_info " [$case_name ] exec: arecord -D \" $SRC_ID \" -f S16_LE -r 48000 -c 2 -d $secs_int \" $out \" "
481+ audio_exec_with_timeout " $effective_timeout " \
482+ arecord -D " $SRC_ID " -f S16_LE -r 48000 -c 2 -d " $secs_int " " $out " >> " $logf " 2>&1
483+ rc=$?
484+ bytes=" $( stat -c ' %s' " $out " 2> /dev/null || wc -c < " $out " ) "
485+
486+ if [ " $rc " -ne 0 ] || [ " ${bytes:- 0} " -le 1024 ] 2> /dev/null; then
487+ if printf ' %s\n' " $SRC_ID " | grep -q ' ^hw:' ; then
488+ alt_dev=" plughw:${SRC_ID# hw: } "
489+ else
490+ alt_dev=" $SRC_ID "
491+ fi
492+ for combo in " S16_LE 48000 2" " S16_LE 44100 2" " S16_LE 16000 1" ; do
493+ fmt=$( printf ' %s\n' " $combo " | awk ' {print $1}' )
494+ rate=$( printf ' %s\n' " $combo " | awk ' {print $2}' )
495+ ch=$( printf ' %s\n' " $combo " | awk ' {print $3}' )
496+ [ -z " $fmt " ] || [ -z " $rate " ] || [ -z " $ch " ] && continue
497+ : > " $out "
498+ log_info " [$case_name ] retry: arecord -D \" $alt_dev \" -f $fmt -r $rate -c $ch -d $secs_int \" $out \" "
499+ audio_exec_with_timeout " $effective_timeout " \
500+ arecord -D " $alt_dev " -f " $fmt " -r " $rate " -c " $ch " -d " $secs_int " " $out " >> " $logf " 2>&1
501+ rc=$?
502+ bytes=" $( stat -c ' %s' " $out " 2> /dev/null || wc -c < " $out " ) "
503+ if [ " $rc " -eq 0 ] && [ " ${bytes:- 0} " -gt 1024 ] 2> /dev/null; then
504+ break
505+ fi
506+ done
507+ fi
331508
332- if [ " $rc " -ne 0 ] && [ " ${bytes:- 0} " -le 1024 ] 2> /dev/null; then
333- log_warn " [$case_name ] first attempt rc=$rc bytes=$bytes ; retry with --target $SRC_ID "
334- : > " $out "
335- log_info " [$case_name ] exec: pw-record -v --target \" $SRC_ID \" \" $out \" "
336- audio_exec_with_timeout " $effective_timeout " pw-record -v --target " $SRC_ID " " $out " >> " $logf " 2>&1
509+ if [ " $rc " -ne 0 ] && [ " ${bytes:- 0} " -gt 1024 ] 2> /dev/null; then
510+ log_warn " [$case_name ] nonzero rc=$rc but recording looks valid (bytes=$bytes ) - PASS"
511+ rc=0
512+ fi
513+ else
514+ log_info " [$case_name ] exec: parecord --file-format=wav \" $out \" "
515+ audio_exec_with_timeout " $effective_timeout " parecord --file-format=wav " $out " >> " $logf " 2>&1
337516 rc=$?
338517 bytes=" $( stat -c ' %s' " $out " 2> /dev/null || wc -c < " $out " ) "
339518 if [ " $rc " -ne 0 ] && [ " ${bytes:- 0} " -gt 1024 ] 2> /dev/null; then
340- log_warn " [$case_name ] nonzero rc=$rc after retry but recording looks valid (bytes=$bytes ) - PASS"
519+ log_warn " [$case_name ] nonzero rc=$rc but recording looks valid (bytes=$bytes ) - PASS"
341520 rc=0
342521 fi
343522 fi
344- else
345- log_info " [$case_name ] exec: parecord --file-format=wav \" $out \" "
346- audio_exec_with_timeout " $effective_timeout " parecord --file-format=wav " $out " >> " $logf " 2>&1
347- rc=$?
348- bytes=" $( stat -c ' %s' " $out " 2> /dev/null || wc -c < " $out " ) "
349- if [ " $rc " -ne 0 ] && [ " ${bytes:- 0} " -gt 1024 ] 2> /dev/null; then
350- log_warn " [$case_name ] nonzero rc=$rc but recording looks valid (bytes=$bytes ) - PASS"
351- rc=0
352- fi
353523 fi
354524
355525 end_s=" $( date +%s 2> /dev/null || echo 0) "
356526 last_elapsed=$(( end_s - start_s))
357- if [ " $last_elapsed " -lt 0 ]; then
358- last_elapsed=0
359- fi
527+ [ " $last_elapsed " -lt 0 ] && last_elapsed=0
360528
361529 # Evidence
362530 pw_ev=$( audio_evidence_pw_streaming || echo 0)
363531 pa_ev=$( audio_evidence_pa_streaming || echo 0)
364532
365- # ---- minimal PulseAudio fallback so pa_streaming doesn't read as 0 after teardown ----
366533 if [ " $AUDIO_BACKEND " = " pulseaudio" ] && [ " $pa_ev " -eq 0 ]; then
367534 if [ " $rc " -eq 0 ] && [ " ${bytes:- 0} " -gt 1024 ] 2> /dev/null; then
368535 pa_ev=1
@@ -376,7 +543,6 @@ for dur in $DURATIONS; do
376543 pwlog_ev=0
377544 fi
378545
379- # Fast teardown fallback: if user-space stream was active, trust ALSA/ASoC too.
380546 if [ " $alsa_ev " -eq 0 ]; then
381547 if [ " $AUDIO_BACKEND " = " pipewire" ] && [ " $pw_ev " -eq 1 ]; then
382548 alsa_ev=1
@@ -392,7 +558,6 @@ for dur in $DURATIONS; do
392558
393559 log_info " [$case_name ] evidence: pw_streaming=$pw_ev pa_streaming=$pa_ev alsa_running=$alsa_ev asoc_path_on=$asoc_ev bytes=${bytes:- 0} pw_log=$pwlog_ev "
394560
395- # Final PASS/FAIL
396561 if [ " $rc " -eq 0 ] && [ " ${bytes:- 0} " -gt 1024 ] 2> /dev/null; then
397562 log_pass " [$case_name ] loop $i OK (rc=0, ${last_elapsed} s, bytes=$bytes )"
398563 ok_runs=$(( ok_runs + 1 ))
0 commit comments