From 4b996ee77c6a2efbd78f370d9b3be4dae978bef6 Mon Sep 17 00:00:00 2001 From: Srikanth Muppandam Date: Wed, 22 Oct 2025 17:18:23 +0530 Subject: [PATCH 1/2] audio_common.sh: backend/mic discovery & capture utilities - Introduce ALSA capture utilities: - alsa_pick_capture(): pick a valid hw:, using with fallbacks to /proc/asound/{pcm,devices} Signed-off-by: Srikanth Muppandam --- Runner/utils/audio_common.sh | 68 ++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/Runner/utils/audio_common.sh b/Runner/utils/audio_common.sh index 9393cac0..b94118d4 100755 --- a/Runner/utils/audio_common.sh +++ b/Runner/utils/audio_common.sh @@ -262,7 +262,11 @@ pa_default_source() { printf '%s\n' "$s" } -pa_set_default_source() { [ -n "$1" ] && pactl set-default-source "$1" >/dev/null 2>&1 || true; } +pa_set_default_source() { + if [ -n "$1" ]; then + pactl set-default-source "$1" >/dev/null 2>&1 || true + fi +} pa_source_name() { id="$1"; [ -n "$id" ] || return 1 @@ -596,4 +600,64 @@ audio_exec_with_timeout() { # no timeout "$@" } - + +# -------------------------------------------------------------------- +# Backend chain + minimal ALSA capture picker (for fallback in run.sh) +# -------------------------------------------------------------------- + +# Prefer: currently selected (or detected) backend, then pipewire, pulseaudio, alsa. +# We keep it simple: we don't filter by daemon state here; the caller tries each. +build_backend_chain() { + preferred="${AUDIO_BACKEND:-$(detect_audio_backend)}" + chain="" + add_unique() { + case " $chain " in + *" $1 "*) : ;; + *) chain="${chain:+$chain }$1" ;; + esac + } + [ -n "$preferred" ] && add_unique "$preferred" + for b in pipewire pulseaudio alsa; do + add_unique "$b" + done + printf '%s\n' "$chain" +} + +# Pick a plausible ALSA capture device. +# Returns something like hw:0,0 if available, else "default". +alsa_pick_capture() { + line="$(arecord -l 2>/dev/null | sed -n 's/^card \([0-9][0-9]*\):.*device \([0-9][0-9]*\):.*/\1 \2/p' | head -n1)" + if [ -n "$line" ]; then + set -- "$line" + printf 'hw:%s,%s\n' "$1" "$2" + return 0 + fi + printf '%s\n' "default" + return 0 +} + +alsa_pick_capture() { + command -v arecord >/dev/null 2>&1 || return 1 + # Prefer the first real capture device from `arecord -l` + arecord -l 2>/dev/null | awk ' + /card [0-9]+: .*device [0-9]+:/ { + if (match($0, /card ([0-9]+):/, c) && match($0, /device ([0-9]+):/, d)) { + printf("hw:%s,%s\n", c[1], d[1]); + exit 0; + } + } + ' +} + +# Prefer virtual capture PCMs (PipeWire/Pulse) over raw hw: when a sound server is present +alsa_pick_virtual_pcm() { + command -v arecord >/dev/null 2>&1 || return 1 + pcs="$(arecord -L 2>/dev/null | sed -n 's/^[[:space:]]*\([[:alnum:]_]\+\)[[:space:]]*$/\1/p')" + for pcm in pipewire pulse default; do + if printf '%s\n' "$pcs" | grep -m1 -x "$pcm" >/dev/null 2>&1; then + printf '%s\n' "$pcm" + return 0 + fi + done + return 1 +} From 64c575f6e2dc748e442f8a128faa7e1fb2f07988 Mon Sep 17 00:00:00 2001 From: Srikanth Muppandam Date: Wed, 22 Oct 2025 17:20:43 +0530 Subject: [PATCH 2/2] AudioRecord/run.sh: prefer PipeWire default; accept valid audio before fallback; smart virtual-PCM fallback - Do NOT skip when PipeWire is selected but SRC_ID is empty; try against the PipeWire default source first - Immediately accept a recording if the output WAV is non-trivial (bytes > 1024), even when pw-record returns a non-zero rc (observed on RB3) Signed-off-by: Srikanth Muppandam --- .../Multimedia/Audio/AudioRecord/run.sh | 249 +++++++++++++++--- 1 file changed, 207 insertions(+), 42 deletions(-) diff --git a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh index a4ef5181..db4cc9b7 100755 --- a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh +++ b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh @@ -140,6 +140,9 @@ log_info "Args: backend=${AUDIO_BACKEND:-auto} source=$SRC_CHOICE loops=$LOOPS d if [ -z "$AUDIO_BACKEND" ]; then AUDIO_BACKEND="$(detect_audio_backend)" fi +BACKENDS_TO_TRY="$(build_backend_chain)" +# Use it for visibility and to satisfy shellcheck usage +log_info "Backend fallback chain: $BACKENDS_TO_TRY" if [ -z "$AUDIO_BACKEND" ]; then log_skip "$TESTNAME SKIP - no audio backend running" echo "$TESTNAME SKIP" > "$RES_FILE" @@ -185,31 +188,126 @@ case "$AUDIO_BACKEND:$SRC_CHOICE" in ;; esac -if [ -z "$SRC_ID" ]; then - log_skip "$TESTNAME SKIP - requested source '$SRC_CHOICE' not found for $AUDIO_BACKEND" +# ---- Dynamic fallback when mic is missing on the chosen backend ---- +# Stay on PipeWire even if SRC_ID is empty; pw-record and arecord -D pipewire can use the default source. +if [ -z "$SRC_ID" ] && [ "$SRC_CHOICE" = "mic" ] && [ "$AUDIO_BACKEND" != "pipewire" ]; then + for b in $BACKENDS_TO_TRY; do + [ "$b" = "$AUDIO_BACKEND" ] && continue + case "$b" in + pipewire) + cand="$(pw_default_mic)" + if [ -n "$cand" ]; then + AUDIO_BACKEND="pipewire"; SRC_ID="$cand" + log_info "Falling back to backend: pipewire (source id=$SRC_ID)" + break + fi + ;; + pulseaudio) + cand="$(pa_default_mic)" + if [ -n "$cand" ]; then + AUDIO_BACKEND="pulseaudio"; SRC_ID="$cand" + log_info "Falling back to backend: pulseaudio (source=$SRC_ID)" + break + fi + ;; + alsa) + cand="$(alsa_pick_capture)" + if [ -n "$cand" ]; then + AUDIO_BACKEND="alsa"; SRC_ID="$cand" + log_info "Falling back to backend: alsa (device=$SRC_ID)" + break + fi + ;; + esac + done +fi + +# Only skip if no source AND not on PipeWire. +if [ -z "$SRC_ID" ] && [ "$AUDIO_BACKEND" != "pipewire" ]; then + log_skip "$TESTNAME SKIP - requested source '$SRC_CHOICE' not available on any backend ($BACKENDS_TO_TRY)" echo "$TESTNAME SKIP" > "$RES_FILE" exit 2 fi +# ---- Normalize ALSA device id (fix "hw:0 1," → "hw:0,1") ---- +if [ "$AUDIO_BACKEND" = "alsa" ]; then + case "$SRC_ID" in + hw:*" "*,) + SRC_ID=$(printf '%s' "$SRC_ID" | sed -E 's/^hw:([0-9]+) ([0-9]+),$/hw:\1,\2/') + ;; + hw:*" "*) + SRC_ID=$(printf '%s' "$SRC_ID" | sed -E 's/^hw:([0-9]+) ([0-9]+)$/hw:\1,\2/') + ;; + esac +fi + +# ---- Validate/auto-pick ALSA device if invalid (prevents "hw:,") ---- +if [ "$AUDIO_BACKEND" = "alsa" ]; then + case "$SRC_ID" in + hw:[0-9]*,[0-9]*|plughw:[0-9]*,[0-9]*) + : ;; + *) + 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)" + if [ -z "$cand" ]; then + 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)" + fi + if [ -z "$cand" ]; then + 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)" + fi + if printf '%s\n' "$cand" | grep -Eq '^hw:[0-9]+,[0-9]+$'; then + SRC_ID="$cand" + log_info "ALSA auto-pick: using $SRC_ID" + else + log_skip "$TESTNAME SKIP - no valid ALSA capture device found" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 2 + fi + ;; + esac +fi + +# ---- Routing log / defaults per backend ---- if [ "$AUDIO_BACKEND" = "pipewire" ]; then - SRC_LABEL="$(pw_source_label_safe "$SRC_ID")" - wpctl set-default "$SRC_ID" >/dev/null 2>&1 || true - if [ -z "$SRC_LABEL" ]; then - SRC_LABEL="unknown" + if [ -n "$SRC_ID" ]; then + SRC_LABEL="$(pw_source_label_safe "$SRC_ID")" + wpctl set-default "$SRC_ID" >/dev/null 2>&1 || true + [ -z "$SRC_LABEL" ] && SRC_LABEL="unknown" + log_info "Routing to source: id/name=$SRC_ID label='$SRC_LABEL' choice=$SRC_CHOICE" + else + SRC_LABEL="default" + log_info "Routing to source: id/name=default label='default' choice=$SRC_CHOICE" fi - log_info "Routing to source: id/name=$SRC_ID label='$SRC_LABEL' choice=$SRC_CHOICE" -else +elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then SRC_LABEL="$(pa_source_name "$SRC_ID" 2>/dev/null || echo "$SRC_ID")" pa_set_default_source "$SRC_ID" >/dev/null 2>&1 || true log_info "Routing to source: name='$SRC_LABEL' choice=$SRC_CHOICE" +else # ALSA + SRC_LABEL="$SRC_ID" + log_info "Routing to source: name='$SRC_LABEL' choice=$SRC_CHOICE" fi +# If fallback changed backend, ensure deps are present (non-fatal → SKIP) +case "$AUDIO_BACKEND" in + pipewire) + if ! check_dependencies wpctl pw-record; then + log_skip "$TESTNAME SKIP - missing PipeWire utils" + echo "$TESTNAME SKIP" > "$RES_FILE"; exit 2 + fi ;; + pulseaudio) + if ! check_dependencies pactl parecord; then + log_skip "$TESTNAME SKIP - missing PulseAudio utils" + echo "$TESTNAME SKIP" > "$RES_FILE"; exit 2 + fi ;; + alsa) + if ! check_dependencies arecord; then + log_skip "$TESTNAME SKIP - missing arecord" + echo "$TESTNAME SKIP" > "$RES_FILE"; exit 2 + fi ;; +esac + # Watchdog info dur_s="$(duration_to_secs "$TIMEOUT" 2>/dev/null || echo 0)" -if [ -z "$dur_s" ]; then - dur_s=0 -fi - +[ -z "$dur_s" ] && dur_s=0 if [ "$dur_s" -gt 0 ] 2>/dev/null; then log_info "Watchdog/timeout: ${TIMEOUT}" else @@ -240,12 +338,8 @@ append_junit() { { printf ' \n' "Audio.Record" "$name" "$elapsed" case "$status" in - PASS) - : - ;; - SKIP) - printf ' \n' - ;; + PASS) : ;; + SKIP) printf ' \n' ;; FAIL) printf ' \n' "failed" printf '%s\n' "$safe_msg" @@ -266,6 +360,22 @@ auto_secs_for() { esac } +# Prefer virtual capture PCMs (PipeWire/Pulse) over raw hw: when a sound server is present +alsa_pick_virtual_pcm() { + command -v arecord >/dev/null 2>&1 || return 1 + + pcs="$(arecord -L 2>/dev/null | sed -n 's/^[[:space:]]*\([[:alnum:]_]\+\)[[:space:]]*$/\1/p')" + + for pcm in pipewire pulse default; do + if printf '%s\n' "$pcs" | grep -m1 -x "$pcm" >/dev/null 2>&1; then + printf '%s\n' "$pcm" + return 0 + fi + done + + return 1 +} + # ---------------- Matrix execution ---------------- total=0 pass=0 @@ -305,7 +415,11 @@ for dur in $DURATIONS; do loop_hdr="source=$SRC_CHOICE" if [ "$AUDIO_BACKEND" = "pipewire" ]; then - loop_hdr="$loop_hdr($SRC_ID)" + if [ -n "$SRC_ID" ]; then + loop_hdr="$loop_hdr($SRC_ID)" + else + loop_hdr="$loop_hdr(default)" + fi else loop_hdr="$loop_hdr($SRC_LABEL)" fi @@ -321,48 +435,101 @@ for dur in $DURATIONS; do log_info "[$case_name] exec: pw-record -v \"$out\"" audio_exec_with_timeout "$effective_timeout" pw-record -v "$out" >> "$logf" 2>&1 rc=$? - bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + # If we already got real audio, accept and skip fallbacks + if [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + if [ "$rc" -ne 0 ]; then + log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" + rc=0 + fi + else + # Only if output is tiny/empty do we try a virtual PCM (pipewire/pulse/default) + if command -v arecord >/dev/null 2>&1; then + pcm="$(alsa_pick_virtual_pcm || true)" + if [ -n "$pcm" ]; then + secs_int="$(audio_parse_secs "$secs" 2>/dev/null || echo 0)"; [ -z "$secs_int" ] && secs_int=0 + : > "$out" + log_info "[$case_name] fallback: arecord -D $pcm -f S16_LE -r 48000 -c 2 -d $secs_int \"$out\"" + audio_exec_with_timeout "$effective_timeout" \ + arecord -D "$pcm" -f S16_LE -r 48000 -c 2 -d "$secs_int" "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + fi + fi + + # As a last resort, retry pw-record with --target (only if we have a source id) + if { [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; } && [ -n "$SRC_ID" ]; then + : > "$out" + log_info "[$case_name] exec: pw-record -v --target \"$SRC_ID\" \"$out\"" + audio_exec_with_timeout "$effective_timeout" pw-record -v --target "$SRC_ID" "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + fi + fi + + # (Optional safety) If nonzero rc but output is clearly valid, accept. if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" + log_warn "[$case_name] nonzero rc==$rc but recording looks valid (bytes=$bytes) - PASS" rc=0 fi + else + if [ "$AUDIO_BACKEND" = "alsa" ]; then + secs_int="$(audio_parse_secs "$secs" 2>/dev/null || echo 0)" + [ -z "$secs_int" ] && secs_int=0 + log_info "[$case_name] exec: arecord -D \"$SRC_ID\" -f S16_LE -r 48000 -c 2 -d $secs_int \"$out\"" + audio_exec_with_timeout "$effective_timeout" \ + arecord -D "$SRC_ID" -f S16_LE -r 48000 -c 2 -d "$secs_int" "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + + if [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then + if printf '%s\n' "$SRC_ID" | grep -q '^hw:'; then + alt_dev="plughw:${SRC_ID#hw:}" + else + alt_dev="$SRC_ID" + fi + for combo in "S16_LE 48000 2" "S16_LE 44100 2" "S16_LE 16000 1"; do + fmt=$(printf '%s\n' "$combo" | awk '{print $1}') + rate=$(printf '%s\n' "$combo" | awk '{print $2}') + ch=$(printf '%s\n' "$combo" | awk '{print $3}') + [ -z "$fmt" ] || [ -z "$rate" ] || [ -z "$ch" ] && continue + : > "$out" + log_info "[$case_name] retry: arecord -D \"$alt_dev\" -f $fmt -r $rate -c $ch -d $secs_int \"$out\"" + audio_exec_with_timeout "$effective_timeout" \ + arecord -D "$alt_dev" -f "$fmt" -r "$rate" -c "$ch" -d "$secs_int" "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + break + fi + done + fi - if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then - log_warn "[$case_name] first attempt rc=$rc bytes=$bytes; retry with --target $SRC_ID" - : > "$out" - log_info "[$case_name] exec: pw-record -v --target \"$SRC_ID\" \"$out\"" - audio_exec_with_timeout "$effective_timeout" pw-record -v --target "$SRC_ID" "$out" >> "$logf" 2>&1 + if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" + rc=0 + fi + else + log_info "[$case_name] exec: parecord --file-format=wav \"$out\"" + audio_exec_with_timeout "$effective_timeout" parecord --file-format=wav "$out" >> "$logf" 2>&1 rc=$? bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - log_warn "[$case_name] nonzero rc=$rc after retry but recording looks valid (bytes=$bytes) - PASS" + log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" rc=0 fi fi - else - log_info "[$case_name] exec: parecord --file-format=wav \"$out\"" - audio_exec_with_timeout "$effective_timeout" parecord --file-format=wav "$out" >> "$logf" 2>&1 - rc=$? - bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" - if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" - rc=0 - fi fi end_s="$(date +%s 2>/dev/null || echo 0)" last_elapsed=$((end_s - start_s)) - if [ "$last_elapsed" -lt 0 ]; then - last_elapsed=0 - fi + [ "$last_elapsed" -lt 0 ] && last_elapsed=0 # Evidence pw_ev=$(audio_evidence_pw_streaming || echo 0) pa_ev=$(audio_evidence_pa_streaming || echo 0) - # ---- minimal PulseAudio fallback so pa_streaming doesn't read as 0 after teardown ---- if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 0 ]; then if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then pa_ev=1 @@ -376,7 +543,6 @@ for dur in $DURATIONS; do pwlog_ev=0 fi - # Fast teardown fallback: if user-space stream was active, trust ALSA/ASoC too. if [ "$alsa_ev" -eq 0 ]; then if [ "$AUDIO_BACKEND" = "pipewire" ] && [ "$pw_ev" -eq 1 ]; then alsa_ev=1 @@ -392,7 +558,6 @@ for dur in $DURATIONS; do 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" - # Final PASS/FAIL if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then log_pass "[$case_name] loop $i OK (rc=0, ${last_elapsed}s, bytes=$bytes)" ok_runs=$((ok_runs + 1))