diff --git a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh index 58770e1b..a14621d1 100755 --- a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh +++ b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh @@ -30,7 +30,7 @@ fi . "$TOOLS/audio_common.sh" TESTNAME="AudioRecord" -RES_SUFFIX="" # Optional suffix for unique result files (e.g., "Config1") +RES_SUFFIX="" # Optional suffix for unique result files (e.g., "Config1") # RES_FILE will be set after parsing command-line arguments # Pre-parse --res-suffix for early failure handling @@ -62,7 +62,7 @@ fi # ---------------- Defaults / CLI ---------------- AUDIO_BACKEND="" SRC_CHOICE="${SRC_CHOICE:-mic}" # mic|null -DURATIONS="" # Will be set to default only if using legacy mode +DURATIONS="" # Will be set to default only if using legacy mode RECORD_SECONDS="${RECORD_SECONDS:-30s}" # DEFAULT: 30s; 'auto' maps short/med/long LOOPS="${LOOPS:-1}" TIMEOUT="${TIMEOUT:-0}" # 0 = no watchdog @@ -72,19 +72,19 @@ VERBOSE=0 JUNIT_OUT="" # New config-based testing options -CONFIG_NAMES="" # Explicit config names to test (e.g., "record_config1 record_config2") -CONFIG_FILTER="" # Filter pattern for configs (e.g., "48KHz" or "2ch") -USE_CONFIG_DISCOVERY="${USE_CONFIG_DISCOVERY:-auto}" # auto|true|false +CONFIG_NAMES="" # Explicit config names to test (e.g., "record_config1 record_config2") +CONFIG_FILTER="" # Filter pattern for configs (e.g., "48KHz" or "2ch") +USE_CONFIG_DISCOVERY="${USE_CONFIG_DISCOVERY:-auto}" # auto|true|false usage() { cat < "$RES_FILE" - exit 0 + AUDIO_BACKEND="$(detect_audio_backend 2>/dev/null || echo "")" +fi + +# If not detected, select first workable backend from chain (includes ALSA) +if [ -z "$AUDIO_BACKEND" ]; then + for b in $BACKENDS_TO_TRY; do + case "$b" in + pipewire|pulseaudio) + if check_audio_daemon "$b"; then + AUDIO_BACKEND="$b" + break + fi + ;; + alsa) + if command -v arecord >/dev/null 2>&1; then + AUDIO_BACKEND="alsa" + break + fi + ;; + esac + done fi -log_info "Using backend: $AUDIO_BACKEND" -if ! check_audio_daemon "$AUDIO_BACKEND"; then - log_skip "$TESTNAME SKIP - backend not available: $AUDIO_BACKEND" +if [ -z "$AUDIO_BACKEND" ]; then + log_skip "$TESTNAME SKIP - no usable audio backend found" echo "$TESTNAME SKIP" > "$RES_FILE" exit 0 fi +log_info "Using backend: $AUDIO_BACKEND" -# Dependencies per backend -if [ "$AUDIO_BACKEND" = "pipewire" ]; then - if ! check_dependencies wpctl pw-record; then - log_skip "$TESTNAME SKIP - missing PipeWire utils" - echo "$TESTNAME SKIP" > "$RES_FILE" - exit 0 - fi -else - if ! check_dependencies pactl parecord; then - log_skip "$TESTNAME SKIP - missing PulseAudio utils" - echo "$TESTNAME SKIP" > "$RES_FILE" - exit 0 +# Validate backend availability (alsa does not require a daemon) +case "$AUDIO_BACKEND" in + pipewire|pulseaudio) + if ! check_audio_daemon "$AUDIO_BACKEND"; then + log_skip "$TESTNAME SKIP - backend not available: $AUDIO_BACKEND" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + fi + ;; + alsa) + if ! command -v arecord >/dev/null 2>&1; then + log_skip "$TESTNAME SKIP - missing arecord (alsa backend unavailable)" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + fi + ;; +esac + +# If PipeWire control plane is unresponsive (RB8 freeze mitigation helper), fallback to next backend +if [ "$AUDIO_BACKEND" = "pipewire" ] && command -v audio_pw_wpctl_responsive >/dev/null 2>&1; then + if ! audio_pw_wpctl_responsive "$AUDIO_WPCTL_TIMEOUT" >/dev/null 2>&1; then + log_warn "wpctl not responsive; trying fallback backend from chain: $BACKENDS_TO_TRY" + for b in $BACKENDS_TO_TRY; do + [ "$b" = "$AUDIO_BACKEND" ] && continue + case "$b" in + pulseaudio) + if check_audio_daemon pulseaudio; then + AUDIO_BACKEND="pulseaudio" + log_info "Falling back to backend: pulseaudio (pipewire control plane unresponsive)" + break + fi + ;; + alsa) + if command -v arecord >/dev/null 2>&1; then + AUDIO_BACKEND="alsa" + log_info "Falling back to backend: alsa (pipewire control plane unresponsive)" + break + fi + ;; + esac + done fi fi +# Dependencies per backend +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 0 + fi + ;; + pulseaudio) + if ! check_dependencies pactl parecord; then + log_skip "$TESTNAME SKIP - missing PulseAudio utils" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + fi + ;; + alsa) + if ! check_dependencies arecord; then + log_skip "$TESTNAME SKIP - missing arecord" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + fi + ;; +esac + # ----- Route source (set default; recorder uses default source) ----- SRC_ID="" case "$AUDIO_BACKEND:$SRC_CHOICE" in pipewire:null) - SRC_ID="$(pw_default_null_source)" + if command -v audio_pw_pick_source_id_safe >/dev/null 2>&1; then + SRC_ID="$(audio_pw_pick_source_id_safe null "$AUDIO_WPCTL_TIMEOUT_LONG" 2>/dev/null || echo "")" + else + SRC_ID="$(pw_default_null_source)" + fi ;; pipewire:*) - SRC_ID="$(pw_default_mic)" + if command -v audio_pw_pick_source_id_safe >/dev/null 2>&1; then + SRC_ID="$(audio_pw_pick_source_id_safe mic "$AUDIO_WPCTL_TIMEOUT_LONG" 2>/dev/null || echo "")" + else + SRC_ID="$(pw_default_mic)" + fi ;; pulseaudio:null) SRC_ID="$(pa_default_null_source)" @@ -308,6 +384,9 @@ case "$AUDIO_BACKEND:$SRC_CHOICE" in pulseaudio:*) SRC_ID="$(pa_default_mic)" ;; + alsa:*) + SRC_ID="$(alsa_pick_capture 2>/dev/null || echo "")" + ;; esac # ---- Dynamic fallback when mic is missing on the chosen backend ---- @@ -392,7 +471,12 @@ fi if [ "$AUDIO_BACKEND" = "pipewire" ]; then if [ -n "$SRC_ID" ]; then SRC_LABEL="$(pw_source_label_safe "$SRC_ID")" - wpctl set-default "$SRC_ID" >/dev/null 2>&1 || true + # Use guarded helper (avoids RB8 control plane freezes) + if command -v audio_pw_set_default_source_safe >/dev/null 2>&1; then + audio_pw_set_default_source_safe "$SRC_ID" "$AUDIO_WPCTL_TIMEOUT" >/dev/null 2>&1 || true + else + pw_set_default_source "$SRC_ID" >/dev/null 2>&1 || true + fi [ -z "$SRC_LABEL" ] && SRC_LABEL="unknown" log_info "Routing to source: id/name=$SRC_ID label='$SRC_LABEL' choice=$SRC_CHOICE" else @@ -482,22 +566,6 @@ 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:]_][[: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 -} - # ------------- Test Execution (Matrix or Config Discovery) ------------- total=0 pass=0 @@ -508,7 +576,7 @@ suite_rc=0 if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then # ========== NEW: Config Discovery Mode ========== log_info "Using config discovery mode" - + # Discover and filter configs if [ -n "$CONFIG_NAMES" ] || [ -n "$CONFIG_FILTER" ]; then # Use discover_and_filter_record_configs helper (logs go to stderr automatically) @@ -527,21 +595,21 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then exit 0 } fi - + if [ -z "$CONFIGS_TO_TEST" ]; then log_skip "$TESTNAME SKIP - No valid record configs found" echo "$TESTNAME SKIP" > "$RES_FILE" exit 0 fi - + # Count configs config_count=0 for config in $CONFIGS_TO_TEST; do - config_count=$(expr $config_count + 1) + config_count=$((config_count + 1)) done - + log_info "Discovered $config_count configs to test" - + # Test each config for config in $CONFIGS_TO_TEST; do # Generate test case name @@ -549,40 +617,40 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then log_warn "Skipping config with invalid name: $config" continue } - + # Get recording parameters params="$(get_record_config_params "$config")" || { log_warn "Skipping config with invalid parameters: $config" continue } - + rate="$(printf '%s' "$params" | awk '{print $1}')" channels="$(printf '%s' "$params" | awk '{print $2}')" - - total=$(expr $total + 1) + + total=$((total + 1)) logf="$LOGDIR/${case_name}.log" : > "$logf" export AUDIO_LOGCTX="$logf" - + log_info "[$case_name] Using config: $config (rate=${rate}Hz channels=$channels)" - + # Determine recording duration secs="$RECORD_SECONDS" if [ "$secs" = "auto" ]; then - secs="5s" # Default for config discovery mode + secs="5s" # Default for config discovery mode fi - + i=1 ok_runs=0 last_elapsed=0 - + while [ "$i" -le "$LOOPS" ]; do iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)" effective_timeout="$secs" if [ -n "$TIMEOUT" ] && [ "$TIMEOUT" != "0" ]; then effective_timeout="$TIMEOUT" fi - + loop_hdr="source=$SRC_CHOICE" if [ "$AUDIO_BACKEND" = "pipewire" ]; then if [ -n "$SRC_ID" ]; then @@ -593,20 +661,20 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then else loop_hdr="$loop_hdr($SRC_LABEL)" fi - + log_info "[$case_name] loop $i/$LOOPS start=$iso rate=${rate}Hz channels=$channels backend=$AUDIO_BACKEND $loop_hdr" - + out="$LOGDIR/${case_name}.wav" : > "$out" - + start_s="$(date +%s 2>/dev/null || echo 0)" - + if [ "$AUDIO_BACKEND" = "pipewire" ]; then log_info "[$case_name] exec: pw-record -v --rate=$rate --channels=$channels \"$out\"" audio_exec_with_timeout "$effective_timeout" pw-record -v --rate="$rate" --channels="$channels" "$out" >> "$logf" 2>&1 rc=$? bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" - + # If we already got real audio, accept and skip fallbacks if [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then if [ "$rc" -ne 0 ]; then @@ -616,7 +684,7 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then 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)" + pcm="$(alsa_pick_virtual_pcm 2>/dev/null || true)" if [ -n "$pcm" ]; then secs_int="$(audio_parse_secs "$secs" 2>/dev/null || echo 0)"; [ -z "$secs_int" ] && secs_int=0 : > "$out" @@ -627,7 +695,7 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" 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" @@ -637,7 +705,7 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" 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" @@ -652,14 +720,14 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then arecord -D "$SRC_ID" -f S16_LE -r "$rate" -c "$channels" -d "$secs_int" "$out" >> "$logf" 2>&1 rc=$? bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" - + 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 - + # Try with the specific config parameters : > "$out" log_info "[$case_name] retry: arecord -D \"$alt_dev\" -f S16_LE -r $rate -c $channels -d $secs_int \"$out\"" @@ -667,7 +735,7 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then arecord -D "$alt_dev" -f S16_LE -r "$rate" -c "$channels" -d "$secs_int" "$out" >> "$logf" 2>&1 rc=$? bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" - + # If still failing, try fallback combinations if [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then for combo in "S16_LE 48000 2" "S16_LE 44100 2" "S16_LE 16000 1"; do @@ -687,7 +755,7 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then done fi fi - + 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 @@ -704,28 +772,28 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then fi fi fi - + end_s="$(date +%s 2>/dev/null || echo 0)" - last_elapsed=$(expr $end_s - $start_s) + last_elapsed=$((end_s - start_s)) [ "$last_elapsed" -lt 0 ] && last_elapsed=0 - + # Evidence pw_ev=$(audio_evidence_pw_streaming || echo 0) pa_ev=$(audio_evidence_pa_streaming || echo 0) - + if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 0 ]; then if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then pa_ev=1 fi fi - + alsa_ev=$(audio_evidence_alsa_running_any || echo 0) asoc_ev=$(audio_evidence_asoc_path_on || echo 0) pwlog_ev=$(audio_evidence_pw_log_seen || echo 0) if [ "$AUDIO_BACKEND" = "pulseaudio" ]; then pwlog_ev=0 fi - + if [ "$alsa_ev" -eq 0 ]; then if [ "$AUDIO_BACKEND" = "pipewire" ] && [ "$pw_ev" -eq 1 ]; then alsa_ev=1 @@ -734,42 +802,42 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then alsa_ev=1 fi fi - + if [ "$asoc_ev" -eq 0 ] && [ "$alsa_ev" -eq 1 ]; then asoc_ev=1 fi - + 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" - + 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=$(expr $ok_runs + 1) + ok_runs=$((ok_runs + 1)) else log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s, bytes=${bytes:-0}) - see $logf" fi - - i=$(expr $i + 1) + + i=$((i + 1)) done - + # Aggregate result for this config status="FAIL" if [ "$ok_runs" -ge 1 ]; then status="PASS" fi - + append_junit "$case_name" "$last_elapsed" "$status" "$logf" - + case "$status" in PASS) - pass=$(expr $pass + 1) + pass=$((pass + 1)) echo "$case_name PASS" >> "$LOGDIR/summary.txt" ;; SKIP) - skip=$(expr $skip + 1) + skip=$((skip + 1)) echo "$case_name SKIP" >> "$LOGDIR/summary.txt" ;; FAIL) - fail=$(expr $fail + 1) + fail=$((fail + 1)) echo "$case_name FAIL" >> "$LOGDIR/summary.txt" suite_rc=1 ;; @@ -779,7 +847,7 @@ else # ========== LEGACY: Matrix Mode ========== for dur in $DURATIONS; do case_name="record_${dur}" - total=$(expr $total + 1) + total=$((total + 1)) logf="$LOGDIR/${case_name}.log" : > "$logf" @@ -840,7 +908,7 @@ else 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)" + pcm="$(alsa_pick_virtual_pcm 2>/dev/null || true)" if [ -n "$pcm" ]; then secs_int="$(audio_parse_secs "$secs" 2>/dev/null || echo 0)"; [ -z "$secs_int" ] && secs_int=0 : > "$out" @@ -917,7 +985,7 @@ else fi end_s="$(date +%s 2>/dev/null || echo 0)" - last_elapsed=$(expr $end_s - $start_s) + last_elapsed=$((end_s - start_s)) [ "$last_elapsed" -lt 0 ] && last_elapsed=0 # Evidence @@ -954,12 +1022,12 @@ else 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=$(expr $ok_runs + 1) + ok_runs=$((ok_runs + 1)) else log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s, bytes=${bytes:-0}) - see $logf" fi - i=$(expr $i + 1) + i=$((i + 1)) done # Aggregate result for this duration @@ -972,15 +1040,15 @@ else case "$status" in PASS) - pass=$(expr $pass + 1) + pass=$((pass + 1)) echo "$case_name PASS" >> "$LOGDIR/summary.txt" ;; SKIP) - skip=$(expr $skip + 1) + skip=$((skip + 1)) echo "$case_name SKIP" >> "$LOGDIR/summary.txt" ;; FAIL) - fail=$(expr $fail + 1) + fail=$((fail + 1)) echo "$case_name FAIL" >> "$LOGDIR/summary.txt" suite_rc=1 ;; @@ -991,7 +1059,12 @@ fi # Collect evidence once at end if [ "$DMESG_SCAN" -eq 1 ]; then scan_audio_dmesg "$LOGDIR" - dump_mixers "$LOGDIR/mixer_dump.txt" + # Prefer guarded mixer dump helper if present; else fallback to existing dump_mixers + if command -v audio_dump_mixers_safe >/dev/null 2>&1; then + audio_dump_mixers_safe "$LOGDIR/mixer_dump.txt" "$AUDIO_WPCTL_TIMEOUT_LONG" >/dev/null 2>&1 || true + else + dump_mixers "$LOGDIR/mixer_dump.txt" + fi fi # JUnit finalize (optional) diff --git a/Runner/suites/Multimedia/Video/Video_V4L2_Runner/h264Encoder.json b/Runner/suites/Multimedia/Video/Video_V4L2_Runner/h264Encoder.json index a8a2d945..e09dd6ad 100755 --- a/Runner/suites/Multimedia/Video/Video_V4L2_Runner/h264Encoder.json +++ b/Runner/suites/Multimedia/Video/Video_V4L2_Runner/h264Encoder.json @@ -11,7 +11,7 @@ "PixelFormat": "NV12", "Width": 256, "Height": 144, - "Outputpath": "", + "Outputpath": "", "_Outputpath_original": "./Output_Animation_144p.h264", "InputBufferCount": 32, "OutputBufferCount": 32, @@ -23,9 +23,11 @@ {"Id": "FrameRC", "Vtype": "Int", "Value": 1}, {"Id": "BitRate", "Vtype": "Int", "Value": 18000000}, {"Id": "BitRateMode", "Vtype": "String", "Value": "CBR"}, - {"Id": "PrefixHeaderMode", "Vtype": "String", "Value": "JOINED"} - ] + {"Id": "PrefixHeaderMode", "Vtype": "String", "Value": "JOINED"}, + ], + "DynamicControls": [], + "MemoryType": "MMAP" } } ] -} +} \ No newline at end of file diff --git a/Runner/suites/Multimedia/Video/Video_V4L2_Runner/h265Encoder.json b/Runner/suites/Multimedia/Video/Video_V4L2_Runner/h265Encoder.json index 997d7f6e..1a785747 100755 --- a/Runner/suites/Multimedia/Video/Video_V4L2_Runner/h265Encoder.json +++ b/Runner/suites/Multimedia/Video/Video_V4L2_Runner/h265Encoder.json @@ -12,7 +12,7 @@ "PixelFormat": "NV12", "Width": 256, "Height": 144, - "Outputpath": "", + "Outputpath": "", "_Outputpath_original": "./Output_Animation_144p.265", "UseMinBufferCtrl": false, "InputBufferCount": 32, @@ -61,8 +61,9 @@ "Value": 59 } ], - "DynamicControls": [] + "DynamicControls": [], + "MemoryType": "MMAP" } } ] -} +} \ No newline at end of file diff --git a/Runner/utils/audio_common.sh b/Runner/utils/audio_common.sh index 1b1b7bc3..24d1e2c3 100755 --- a/Runner/utils/audio_common.sh +++ b/Runner/utils/audio_common.sh @@ -1,6 +1,7 @@ #!/bin/sh # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. -# SPDX-License-Identifier: BSD-3-Clause# Common audio helpers for PipeWire / PulseAudio runners. +# SPDX-License-Identifier: BSD-3-Clause +# Common audio helpers for PipeWire / PulseAudio runners. # Requires: functestlib.sh (log_* helpers, extract_tar_from_url, scan_dmesg_errors) # ---------- Backend detection & daemon checks ---------- @@ -34,7 +35,7 @@ check_audio_daemon() { resolve_clip() { fmt="$1"; dur="$2" base="${AUDIO_CLIPS_BASE_DIR:-AudioClips}" - + case "$fmt:$dur" in wav:short|wav:medium|wav:long) # Try hardcoded clip first (backward compatibility) @@ -43,7 +44,7 @@ resolve_clip() { printf '%s\n' "$clip" return 0 fi - + # Fallback: discover first available clip first_clip="$(find "$base" -maxdepth 1 -name "*.wav" -type f 2>/dev/null | head -n1)" if [ -n "$first_clip" ] && [ -f "$first_clip" ]; then @@ -51,7 +52,7 @@ resolve_clip() { printf '%s\n' "$first_clip" return 0 fi - + # No clips available log_error "No audio clips found in $base" >&2 printf '%s\n' "" @@ -66,62 +67,64 @@ resolve_clip() { # audio_download_with_any audio_download_with_any() { - url="$1"; out="$2" - if command -v wget >/dev/null 2>&1; then - wget -O "$out" "$url" - elif command -v curl >/dev/null 2>&1; then - curl -L --fail -o "$out" "$url" - else - log_error "No downloader (wget/curl) available to fetch $url" - return 1 - fi + url="$1"; out="$2" + if command -v wget >/dev/null 2>&1; then + wget -O "$out" "$url" + elif command -v curl >/dev/null 2>&1; then + curl -L --fail -o "$out" "$url" + else + log_error "No downloader (wget/curl) available to fetch $url" + return 1 + fi } + # audio_fetch_assets_from_url # Prefer functestlib's extract_tar_from_url; otherwise download + extract. audio_fetch_assets_from_url() { - url="$1" - if command -v extract_tar_from_url >/dev/null 2>&1; then - extract_tar_from_url "$url" - return $? - fi - fname="$(basename "$url")" - log_info "Fetching assets: $url" - if ! audio_download_with_any "$url" "$fname"; then - log_warn "Download failed: $url" - return 1 - fi - tar -xzf "$fname" >/dev/null 2>&1 || tar -xf "$fname" >/dev/null 2>&1 || { - log_warn "Extraction failed: $fname" - return 1 - } - return 0 + url="$1" + if command -v extract_tar_from_url >/dev/null 2>&1; then + extract_tar_from_url "$url" + return $? + fi + fname="$(basename "$url")" + log_info "Fetching assets: $url" + if ! audio_download_with_any "$url" "$fname"; then + log_warn "Download failed: $url" + return 1 + fi + tar -xzf "$fname" >/dev/null 2>&1 || tar -xf "$fname" >/dev/null 2>&1 || { + log_warn "Extraction failed: $fname" + return 1 + } + return 0 } + # audio_ensure_clip_ready [tarball-url] # Return codes: -# 0 = clip exists/ready -# 2 = network unavailable after attempts (caller should SKIP) -# 1 = fetch/extract/downloader error (caller will also SKIP per your policy) +# 0 = clip exists/ready +# 2 = network unavailable after attempts (caller should SKIP) +# 1 = fetch/extract/downloader error (caller will also SKIP per your policy) audio_ensure_clip_ready() { - clip="$1" - url="${2:-${AUDIO_TAR_URL:-}}" + clip="$1" + url="${2:-${AUDIO_TAR_URL:-}}" + [ -f "$clip" ] && return 0 + # Try once without forcing network (tarball may already be present) + if [ -n "$url" ]; then + audio_fetch_assets_from_url "$url" >/dev/null 2>&1 || true [ -f "$clip" ] && return 0 - # Try once without forcing network (tarball may already be present) - if [ -n "$url" ]; then - audio_fetch_assets_from_url "$url" >/dev/null 2>&1 || true - [ -f "$clip" ] && return 0 - fi - # Bring network up and retry once - if ! ensure_network_online; then - log_warn "Network unavailable; cannot fetch audio assets for $clip" - return 2 - fi - if [ -n "$url" ]; then - if audio_fetch_assets_from_url "$url" >/dev/null 2>&1; then - [ -f "$clip" ] && return 0 - fi + fi + # Bring network up and retry once + if ! ensure_network_online; then + log_warn "Network unavailable; cannot fetch audio assets for $clip" + return 2 + fi + if [ -n "$url" ]; then + if audio_fetch_assets_from_url "$url" >/dev/null 2>&1; then + [ -f "$clip" ] && return 0 fi - log_warn "Clip fetch/extract failed for $clip" - return 1 + fi + log_warn "Clip fetch/extract failed for $clip" + return 1 } # ---------- dmesg + mixer dumps ---------- @@ -130,27 +133,14 @@ scan_audio_dmesg() { scan_dmesg_errors "$mods" "$outdir" "$excl" || true } -dump_mixers() { - out="$1" - { - echo "---- wpctl status ----" - command -v wpctl >/dev/null 2>&1 && wpctl status 2>&1 || echo "(wpctl not found)" - echo "---- pactl list ----" - command -v pactl >/dev/null 2>&1 && pactl list 2>&1 || echo "(pactl not found)" - } >"$out" 2>/dev/null -} - -# Returns child exit code (124 when killed by timeout). If tmo<=0, runs the -# command directly (no watchdog). - # ---------- Timeout runner (prefers provided wrappers) ---------- # Returns child's exit code. For the fallback-kill path, returns 143 on timeout. audio_timeout_run() { tmo="$1"; shift - + # 0/empty => run without a watchdog (do NOT background/kill) case "$tmo" in ""|0|"0s"|"0S") "$@"; return $? ;; esac - + # Use project-provided wrappers if available if command -v run_with_timeout >/dev/null 2>&1; then run_with_timeout "$tmo" "$@"; return $? @@ -161,14 +151,14 @@ audio_timeout_run() { if command -v timeout >/dev/null 2>&1; then timeout "$tmo" "$@"; return $? fi - + # Last-resort busybox-safe watchdog # Normalize "15s" -> 15 sec="$(printf '%s' "$tmo" | sed 's/[sS]$//')" [ -z "$sec" ] && sec="$tmo" # If parsing failed for some reason, just run directly case "$sec" in ''|*[!0-9]* ) "$@"; return $? ;; esac - + "$@" & pid=$! t=0 @@ -178,88 +168,234 @@ audio_timeout_run() { wait "$pid" 2>/dev/null return 143 fi - sleep 1; t=$(expr $t + 1) + sleep 1 + t=$((t + 1)) done wait "$pid"; return $? } +# ---- Guard wpctl/pactl/systemctl calls (prevents hangs on stuck control plane) ---- +AUDIO_WPCTL_TIMEOUT="${AUDIO_WPCTL_TIMEOUT:-3s}" +AUDIO_WPCTL_TIMEOUT_LONG="${AUDIO_WPCTL_TIMEOUT_LONG:-8s}" +AUDIO_PACTL_TIMEOUT="${AUDIO_PACTL_TIMEOUT:-3s}" +AUDIO_SYSTEMCTL_TIMEOUT="${AUDIO_SYSTEMCTL_TIMEOUT:-20s}" + +wpctlT() { audio_timeout_run "$AUDIO_WPCTL_TIMEOUT" wpctl "$@"; } +wpctlTL() { audio_timeout_run "$AUDIO_WPCTL_TIMEOUT_LONG" wpctl "$@"; } +pactlT() { audio_timeout_run "$AUDIO_PACTL_TIMEOUT" pactl "$@"; } +systemctlT() { audio_timeout_run "$AUDIO_SYSTEMCTL_TIMEOUT" systemctl "$@"; } + +# ---------------- PipeWire (wpctl) freeze-mitigation helpers ---------------- +# These helpers keep wpctl interactions bounded and reusable across targets. + +audio_pw_wpctl_status_safe() { + # $1 optional timeout (default: AUDIO_WPCTL_TIMEOUT_LONG) + tmo="${1:-$AUDIO_WPCTL_TIMEOUT_LONG}" + command -v wpctl >/dev/null 2>&1 || return 1 + audio_timeout_run "$tmo" wpctl status 2>/dev/null +} + +audio_pw_wpctl_responsive() { + # $1 optional timeout + audio_pw_wpctl_status_safe "${1:-$AUDIO_WPCTL_TIMEOUT}" >/dev/null 2>&1 +} + +audio_pw_pick_source_id_safe() { + # $1 = mic|null ; $2 optional timeout + want="$1" + tmo="${2:-$AUDIO_WPCTL_TIMEOUT_LONG}" + + st="$(audio_pw_wpctl_status_safe "$tmo")" || { printf '%s\n' ""; return 1; } + + blk="$(printf '%s\n' "$st" | sed -n '/Sources:/,/Filters:/p')" + [ -n "$blk" ] || blk="$(printf '%s\n' "$st" | sed -n '/Sources:/,/^$/p')" + + case "$want" in + null) + printf '%s\n' "$blk" \ + | grep -i -E 'null|dummy' \ + | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' \ + | head -n1 + ;; + *) + id="$(printf '%s\n' "$blk" \ + | grep -i 'mic' \ + | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' \ + | head -n1)" + [ -n "$id" ] || id="$(printf '%s\n' "$blk" \ + | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' \ + | head -n1)" + printf '%s\n' "$id" + ;; + esac + return 0 +} + +audio_pw_set_default_source_safe() { + # $1 = numeric id ; $2 optional timeout + id="$1" + tmo="${2:-$AUDIO_WPCTL_TIMEOUT}" + [ -n "$id" ] || return 0 + command -v wpctl >/dev/null 2>&1 || return 0 + audio_timeout_run "$tmo" wpctl set-default "$id" >/dev/null 2>&1 || true + return 0 +} + +audio_pw_record_timeout_for() { + # $1 = duration (e.g. 30s), $2 = TIMEOUT (user) ; output timeout string + dur="$1" + user_tmo="$2" + + if [ -n "$user_tmo" ] && [ "$user_tmo" != "0" ]; then + printf '%s\n' "$user_tmo" + return 0 + fi + + s_int="$(audio_parse_secs "$dur" 2>/dev/null || echo 0)" + [ -z "$s_int" ] && s_int=0 + if [ "$s_int" -le 0 ] 2>/dev/null; then + printf '%s\n' "45s" + return 0 + fi + printf '%s\n' "$((s_int + 10))s" + return 0 +} + +audio_dump_mixers_safe() { + # $1 = out file ; $2 optional timeout + out="$1" + tmo="${2:-$AUDIO_WPCTL_TIMEOUT_LONG}" + + { + echo "---- wpctl status ----" + if command -v wpctl >/dev/null 2>&1; then + audio_timeout_run "$tmo" wpctl status 2>&1 || echo "(wpctl status failed/timeout)" + else + echo "(wpctl not found)" + fi + echo "---- pactl list ----" + if command -v pactl >/dev/null 2>&1; then + pactlT list 2>&1 || echo "(pactl list failed/timeout)" + else + echo "(pactl not found)" + fi + } >"$out" 2>/dev/null +} + +# Back-compat alias used by existing run.sh scripts +dump_mixers() { audio_dump_mixers_safe "$1" "$AUDIO_WPCTL_TIMEOUT_LONG"; } + # Function: setup_overlay_audio_environment # Purpose: Configure audio environment for overlay builds (audioreach-based) # Returns: 0 on success, 1 on failure # Usage: Call early in audio test initialization, before backend detection - setup_overlay_audio_environment() { - # Detect overlay build - if ! lsmod 2>/dev/null | awk '$1 ~ /^audioreach/ { found=1; exit } END { exit !found }'; then - log_info "Base build detected (no audioreach modules), skipping overlay setup" + # Detect overlay build + if ! lsmod 2>/dev/null | awk '$1 ~ /^audioreach/ { found=1; exit } END { exit !found }'; then + log_info "Base build detected (no audioreach modules), skipping overlay setup" + return 0 + fi + + log_info "Overlay build detected (audioreach modules present), configuring environment..." + + # Check root permissions + if [ "$(id -u)" -ne 0 ]; then + log_fail "Overlay audio setup requires root permissions" + return 1 + fi + + # Configure DMA heap permissions + if [ -e /dev/dma_heap/system ]; then + log_info "Setting permissions on /dev/dma_heap/system" + chmod 666 /dev/dma_heap/system || { + log_fail "Failed to chmod /dev/dma_heap/system" + return 1 + } + else + log_warn "/dev/dma_heap/system not found, skipping chmod" + fi + + # Check systemctl availability + if ! command -v systemctl >/dev/null 2>&1; then + log_fail "systemctl not available, cannot restart pipewire" + return 1 + fi + + # ---- Idempotent per-boot behavior ---- + # Overlay needs one restart after boot; subsequent test runs in the same boot + # should NOT keep restarting (can hit systemd start-limit / transient dbus issues). + STAMP="/run/.audio_overlay_pipewire_restarted" + + # If we've already done the restart once this boot, just validate readiness. + if [ -f "$STAMP" ]; then + if pgrep -x pipewire >/dev/null 2>&1; then + if command -v wpctl >/dev/null 2>&1 && wpctlTL status >/dev/null 2>&1; then + log_info "Overlay already configured this boot, PipeWire is ready. Skipping restart." return 0 + fi fi - - log_info "Overlay build detected (audioreach modules present), configuring environment..." - - # Check root permissions - if [ "$(id -u)" -ne 0 ]; then - log_fail "Overlay audio setup requires root permissions" - return 1 + log_warn "Overlay already configured this boot, but PipeWire not ready.. attempting recovery restart..." + fi + + # Restart PipeWire (guard against systemd/dbus hangs) + log_info "Restarting pipewire service..." + + # Clear systemd start-limit if any (common after repeated restarts) + systemctlT reset-failed pipewire >/dev/null 2>&1 || true + systemctlT reset-failed pipewire-pulse >/dev/null 2>&1 || true + + if ! systemctlT restart pipewire >/dev/null 2>&1; then + # If restart failed but service is actually healthy, treat as success (non-flaky). + if pgrep -x pipewire >/dev/null 2>&1; then + if command -v wpctl >/dev/null 2>&1 && wpctlTL status >/dev/null 2>&1; then + : >"$STAMP" 2>/dev/null || true + log_warn "pipewire restart returned failure, but PipeWire is responsive.. continuing." + return 0 + fi fi - - # Configure DMA heap permissions - if [ -e /dev/dma_heap/system ]; then - log_info "Setting permissions on /dev/dma_heap/system" - chmod 666 /dev/dma_heap/system || { - log_fail "Failed to chmod /dev/dma_heap/system" - return 1 - } - else - log_warn "/dev/dma_heap/system not found, skipping chmod" + + # One more attempt after reset-failed (handles start-limit-hit) + systemctlT reset-failed pipewire >/dev/null 2>&1 || true + if ! systemctlT restart pipewire >/dev/null 2>&1; then + log_fail "Failed to restart pipewire service" + return 1 fi - - # Check systemctl availability - if ! command -v systemctl >/dev/null 2>&1; then - log_fail "systemctl not available, cannot restart pipewire" - return 1 + fi + + # Wait for PipeWire with polling (max 60s, check every 2s) + log_info "Waiting for pipewire to be ready..." + max_wait=60 + elapsed=0 + poll_interval=2 + + while [ "$elapsed" -lt "$max_wait" ]; do + # Check if pipewire process is running + if pgrep -x pipewire >/dev/null 2>&1; then + # Verify wpctl can communicate (GUARDED to avoid freeze) + if command -v wpctl >/dev/null 2>&1 && wpctlTL status >/dev/null 2>&1; then + : >"$STAMP" 2>/dev/null || true + log_pass "PipeWire is ready (took ${elapsed}s)" + return 0 + fi fi - - # Restart PipeWire - log_info "Restarting pipewire service..." - if ! systemctl restart pipewire 2>/dev/null; then - log_fail "Failed to restart pipewire service" - return 1 + + sleep "$poll_interval" + elapsed=$((elapsed + poll_interval)) + + if [ $((elapsed % 10)) -eq 0 ]; then + log_info "Still waiting for pipewire... (${elapsed}s/${max_wait}s)" fi - - # Wait for PipeWire with polling (max 60s, check every 2s) - log_info "Waiting for pipewire to be ready..." - max_wait=60 - elapsed=0 - poll_interval=2 - - while [ $elapsed -lt $max_wait ]; do - # Check if pipewire process is running - if pgrep -x pipewire >/dev/null 2>&1; then - # Verify wpctl can communicate - if command -v wpctl >/dev/null 2>&1 && wpctl status >/dev/null 2>&1; then - log_pass "PipeWire is ready (took ${elapsed}s)" - return 0 - fi - fi - - sleep $poll_interval - elapsed=$(expr $elapsed + $poll_interval) - - if [ "$(expr $elapsed % 10)" -eq 0 ]; then - log_info "Still waiting for pipewire... (${elapsed}s/${max_wait}s)" - fi - done - - # Timeout reached - log_fail "PipeWire failed to become ready within ${max_wait}s" - log_fail "Check 'systemctl status pipewire' and 'journalctl -u pipewire' for details" - return 1 -} + done + # Timeout reached + log_fail "PipeWire failed to become ready within ${max_wait}s" + log_fail "Check 'systemctl status pipewire' and 'journalctl -u pipewire' for details" + return 1 +} + # ---------- PipeWire: sinks (playback) ---------- pw_default_speakers() { - _block="$(wpctl status 2>/dev/null | sed -n '/Sinks:/,/Sources:/p')" + _block="$(wpctlTL status 2>/dev/null | sed -n '/Sinks:/,/Sources:/p')" _id="$(printf '%s\n' "$_block" \ | grep -i -E 'speaker|headphone' \ | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' \ @@ -274,7 +410,7 @@ pw_default_speakers() { } pw_default_null() { - wpctl status 2>/dev/null \ + wpctlTL status 2>/dev/null \ | sed -n '/Sinks:/,/Sources:/p' \ | grep -i -E 'null|dummy|loopback|monitor' \ | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' \ @@ -283,10 +419,10 @@ pw_default_null() { pw_sink_name_safe() { id="$1"; [ -n "$id" ] || { echo ""; return 1; } - name="$(wpctl inspect "$id" 2>/dev/null | grep -m1 'node.description' | cut -d'"' -f2)" - [ -n "$name" ] || name="$(wpctl inspect "$id" 2>/dev/null | grep -m1 'node.name' | cut -d'"' -f2)" + name="$(wpctlT inspect "$id" 2>/dev/null | grep -m1 'node.description' | cut -d'"' -f2)" + [ -n "$name" ] || name="$(wpctlT inspect "$id" 2>/dev/null | grep -m1 'node.name' | cut -d'"' -f2)" if [ -z "$name" ]; then - name="$(wpctl status 2>/dev/null \ + name="$(wpctlTL status 2>/dev/null \ | sed -n '/Sinks:/,/Sources:/p' \ | grep -E "^[^0-9]*${id}[.][[:space:]]" \ | sed 's/^[^0-9]*[0-9][0-9]*[.][[:space:]][[:space:]]*//' \ @@ -297,30 +433,30 @@ pw_sink_name_safe() { } pw_sink_name() { pw_sink_name_safe "$@"; } # back-compat alias -pw_set_default_sink() { [ -n "$1" ] && wpctl set-default "$1" >/dev/null 2>&1; } +pw_set_default_sink() { [ -n "$1" ] && wpctlT set-default "$1" >/dev/null 2>&1; } # ---------- PipeWire: sources (record) ---------- pw_default_mic() { - blk="$(wpctl status 2>/dev/null | sed -n '/Sources:/,/^$/p')" + blk="$(wpctlTL status 2>/dev/null | sed -n '/Sources:/,/^$/p')" id="$(printf '%s\n' "$blk" | grep -i 'mic' | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' | head -n1)" [ -n "$id" ] || id="$(printf '%s\n' "$blk" | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' | head -n1)" printf '%s\n' "$id" } pw_default_null_source() { - blk="$(wpctl status 2>/dev/null | sed -n '/Sources:/,/^$/p')" + blk="$(wpctlTL status 2>/dev/null | sed -n '/Sources:/,/^$/p')" id="$(printf '%s\n' "$blk" | grep -i 'null\|dummy' | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' | head -n1)" printf '%s\n' "$id" } -pw_set_default_source() { [ -n "$1" ] && wpctl set-default "$1" >/dev/null 2>&1; } +pw_set_default_source() { [ -n "$1" ] && wpctlT set-default "$1" >/dev/null 2>&1; } pw_source_label_safe() { id="$1"; [ -n "$id" ] || { echo ""; return 1; } - label="$(wpctl inspect "$id" 2>/dev/null | grep -m1 'node.description' | cut -d'"' -f2)" - [ -n "$label" ] || label="$(wpctl inspect "$id" 2>/dev/null | grep -m1 'node.name' | cut -d'"' -f2)" + label="$(wpctlT inspect "$id" 2>/dev/null | grep -m1 'node.description' | cut -d'"' -f2)" + [ -n "$label" ] || label="$(wpctlT inspect "$id" 2>/dev/null | grep -m1 'node.name' | cut -d'"' -f2)" if [ -z "$label" ]; then - label="$(wpctl status 2>/dev/null \ + label="$(wpctlTL status 2>/dev/null \ | sed -n '/Sources:/,/Filters:/p' \ | grep -E "^[^0-9]*${id}[.][[:space:]]" \ | sed 's/^[^0-9]*[0-9][0-9]*[.][[:space:]][[:space:]]*//' \ @@ -400,14 +536,13 @@ pa_default_null_source() { printf '%s\n' "$name" } - # ---------- Evidence helpers (used by run.sh for PASS-on-evidence) ---------- # PipeWire: 1 if any output audio stream exists; fallback parses Streams: block audio_evidence_pw_streaming() { # Try wpctl (fast); fall back to log scan if AUDIO_LOGCTX is available if command -v wpctl >/dev/null 2>&1; then - # Count Input/Output streams in RUNNING state - wpctl status 2>/dev/null | grep -Eq 'RUNNING' && { echo 1; return; } + # Count Input/Output streams in RUNNING state (GUARDED) + wpctlTL status 2>/dev/null | grep -Eq 'RUNNING' && { echo 1; return; } fi # Fallback to log if [ -n "${AUDIO_LOGCTX:-}" ] && [ -r "$AUDIO_LOGCTX" ]; then @@ -415,29 +550,22 @@ audio_evidence_pw_streaming() { fi echo 0 } - + # 2) PulseAudio streaming - safe when PA is absent (returns 0 without forcing FAIL) -#Return 1 if PulseAudio is actively streaming (sink-inputs, source-outputs, or RUNNING sink), -# else 0. Works even when the PA daemon is a different user by trying sockets + cookies. audio_evidence_pa_streaming() { - # quick exits if tools are missing command -v pactl >/dev/null 2>&1 || command -v pacmd >/dev/null 2>&1 || { - # final fallback: try to infer from our log if present if [ -n "${AUDIO_LOGCTX:-}" ] && [ -s "$AUDIO_LOGCTX" ]; then grep -qiE 'Connected to PulseAudio|Opening audio stream|Stream started|Starting recording|Playing' "$AUDIO_LOGCTX" && { echo 1; return; } fi echo 0; return } - - # build candidate socket + cookie pairs + cand="" - # per-user runtime dir sockets for d in /run/user/* /var/run/user/*; do [ -S "$d/pulse/native" ] || continue sock="$d/pulse/native" cookie="" [ -r "$d/pulse/cookie" ] && cookie="$d/pulse/cookie" - # try to derive a home cookie for that uid as well uid="$(stat -c %u "$d" 2>/dev/null || stat -f %u "$d" 2>/dev/null || echo)" if [ -n "$uid" ]; then home="$(getent passwd "$uid" 2>/dev/null | awk -F: '{print $6}')" @@ -445,45 +573,46 @@ audio_evidence_pa_streaming() { fi cand="$cand|$sock|$cookie" done - # system-wide socket (no per-user cookie nearby) for s in /run/pulse/native /var/run/pulse/native; do [ -S "$s" ] && cand="$cand|$s|" done - # also try current env (no explicit socket) cand="$cand|::env::|" - - # try pactl first with cookie if available + if command -v pactl >/dev/null 2>&1; then IFS='|' read -r _ sock cookie rest </dev/null 2>&1 || true - if pactl list sinks 2>/dev/null | grep -qi -m1 '^[[:space:]]*State:[[:space:]]*RUNNING' \ - || pactl list short sink-inputs 2>/dev/null | grep -q '^[0-9][0-9]*' \ - || pactl list short source-outputs 2>/dev/null | grep -q '^[0-9][0-9]*' ; then + pactlT info >/dev/null 2>&1 || true + if pactlT list sinks 2>/dev/null | grep -qi -m1 '^[[:space:]]*State:[[:space:]]*RUNNING' \ + || pactlT list short sink-inputs 2>/dev/null | grep -q '^[0-9][0-9]*' \ + || pactlT list short source-outputs 2>/dev/null | grep -q '^[0-9][0-9]*' ; then echo 1; return fi else if [ -n "$cookie" ]; then - PULSE_SERVER="unix:$sock" PULSE_COOKIE="$cookie" pactl info >/dev/null 2>&1 || { IFS='|' read -r sock cookie rest </dev/null 2>&1 || { + IFS='|' read -r sock cookie rest </dev/null | grep -qi -m1 '^[[:space:]]*State:[[:space:]]*RUNNING' \ - || PULSE_SERVER="unix:$sock" PULSE_COOKIE="$cookie" pactl list short sink-inputs 2>/dev/null | grep -q '^[0-9][0-9]*' \ - || PULSE_SERVER="unix:$sock" PULSE_COOKIE="$cookie" pactl list short source-outputs 2>/dev/null | grep -q '^[0-9][0-9]*' ; then + continue + } + if PULSE_SERVER="unix:$sock" PULSE_COOKIE="$cookie" pactlT list sinks 2>/dev/null | grep -qi -m1 '^[[:space:]]*State:[[:space:]]*RUNNING' \ + || PULSE_SERVER="unix:$sock" PULSE_COOKIE="$cookie" pactlT list short sink-inputs 2>/dev/null | grep -q '^[0-9][0-9]*' \ + || PULSE_SERVER="unix:$sock" PULSE_COOKIE="$cookie" pactlT list short source-outputs 2>/dev/null | grep -q '^[0-9][0-9]*' ; then echo 1; return fi else - PULSE_SERVER="unix:$sock" pactl info >/dev/null 2>&1 || { IFS='|' read -r sock cookie rest </dev/null 2>&1 || { + IFS='|' read -r sock cookie rest </dev/null | grep -qi -m1 '^[[:space:]]*State:[[:space:]]*RUNNING' \ - || PULSE_SERVER="unix:$sock" pactl list short sink-inputs 2>/dev/null | grep -q '^[0-9][0-9]*' \ - || PULSE_SERVER="unix:$sock" pactl list short source-outputs 2>/dev/null | grep -q '^[0-9][0-9]*' ; then + continue + } + if PULSE_SERVER="unix:$sock" pactlT list sinks 2>/dev/null | grep -qi -m1 '^[[:space:]]*State:[[:space:]]*RUNNING' \ + || PULSE_SERVER="unix:$sock" pactlT list short sink-inputs 2>/dev/null | grep -q '^[0-9][0-9]*' \ + || PULSE_SERVER="unix:$sock" pactlT list short source-outputs 2>/dev/null | grep -q '^[0-9][0-9]*' ; then echo 1; return fi fi @@ -493,8 +622,7 @@ $rest EOF done fi - - # fall back to pacmd if pactl didn't work + if command -v pacmd >/dev/null 2>&1; then IFS='|' read -r _ sock cookie rest </dev/null 2>&1 || { IFS='|' read -r sock cookie rest </dev/null 2>&1 || { + IFS='|' read -r sock cookie rest </dev/null | grep -qi -m1 '^[[:space:]]*state:[[:space:]]*RUNNING' \ || pacmd -s "unix:$sock" list-sink-inputs 2>/dev/null | grep -q -m1 '^[[:space:]]*index:' \ || pacmd -s "unix:$sock" list-source-outputs 2>/dev/null | grep -q -m1 '^[[:space:]]*index:' ; then @@ -524,15 +653,14 @@ $rest EOF done fi - - # Last resort: infer from our player/recorder logs + if [ -n "${AUDIO_LOGCTX:-}" ] && [ -s "$AUDIO_LOGCTX" ]; then grep -qiE 'Connected to PulseAudio|Opening audio stream|Stream started|Starting recording|Playing' "$AUDIO_LOGCTX" && { echo 1; return; } fi - + echo 0 } - + # 3) ALSA RUNNING - sample a few times to beat teardown race audio_evidence_alsa_running_any() { found=0 @@ -544,36 +672,33 @@ audio_evidence_alsa_running_any() { done echo "$found" } -# 4) ASoC path on - try both debugfs locations; mount if needed + +# 4) ASoC path on audio_evidence_asoc_path_on() { base="/sys/kernel/debug/asoc" [ -d "$base" ] || { echo 0; return; } - - # Fast path: any explicit "On" marker in any dapm node + if grep -RIlq --binary-files=text -E '(^|\s)\[on\]|\:\s*On(\s|$)' "$base"/*/dapm 2>/dev/null; then echo 1; return fi - - # Many QCS boards expose lots of Playback/Capture endpoints; if any of them say "On", mark active + dapm_pc_files="$(grep -RIl --binary-files=text -E '/dapm/.*(Playback|Capture)$' "$base"/*/dapm 2>/dev/null)" if [ -n "$dapm_pc_files" ]; then echo "$dapm_pc_files" | xargs -r grep -I -q -E ':\s*On(\s|$)' 2>/dev/null && { echo 1; return; } fi - - # Some kernels only flip bias level when any path is active + if grep -RIlq --binary-files=text '/dapm/bias_level$' "$base"/*/dapm 2>/dev/null; then grep -RIl --binary-files=text '/dapm/bias_level$' "$base"/*/dapm 2>/dev/null \ | xargs -r grep -I -q -E 'On|Standby' 2>/dev/null && { echo 1; return; } fi - - # Fallback heuristic: if ALSA says a PCM substream is RUNNING, assume DAPM is up + if audio_evidence_alsa_running_any 2>/dev/null | grep -qx 1; then echo 1; return fi - + echo 0 } -# 5) PW log evidence (optional, from AUDIO_LOGCTX) + audio_evidence_pw_log_seen() { if [ -n "${AUDIO_LOGCTX:-}" ] && [ -r "$AUDIO_LOGCTX" ]; then grep -qiE 'paused -> streaming|stream time:' "$AUDIO_LOGCTX" 2>/dev/null && { echo 1; return; } @@ -581,24 +706,12 @@ audio_evidence_pw_log_seen() { echo 0 } - # Parse a human duration into integer seconds. -# Prints seconds to stdout on success, returns 0. -# Prints nothing and returns non-zero on failure. -# -# Accepted examples: -# "15" "15s" "15sec" "15secs" "15second" "15seconds" -# "2m" "2min" "2mins" "2minute" "2minutes" -# "1h" "1hr" "1hrs" "1hour" "1hours" -# "1h30m" "2m10s" "1h2m3s" (any combination h/m/s) -# "90s" "120m" "3h" -# "MM:SS" (e.g., "01:30" -> 90) -# "HH:MM:SS" (e.g., "2:03:04" -> 7384) audio_parse_secs() { in="$*" norm=$(printf '%s' "$in" | tr -d ' \t\r\n' | tr '[:upper:]' '[:lower:]') [ -n "$norm" ] || return 1 - + case "$norm" in *:*) IFS=':' set -- "$norm" @@ -609,7 +722,7 @@ audio_parse_secs() { *) return 1 ;; esac h_val=${h:-0}; m_val=${m:-0}; s_val=${s:-0} - result=$(expr $h_val \* 3600 + $m_val \* 60 + $s_val) + result=$((h_val * 3600 + m_val * 60 + s_val)) printf '%s\n' "$result" return 0 ;; @@ -618,9 +731,9 @@ audio_parse_secs() { [0-9]*s|[0-9]*sec|[0-9]*secs|[0-9]*second|[0-9]*seconds) n=$(printf '%s' "$norm" | sed -n 's/^\([0-9][0-9]*\).*/\1/p'); printf '%s\n' "$n"; return 0 ;; [0-9]*m|[0-9]*min|[0-9]*mins|[0-9]*minute|[0-9]*minutes) - n=$(printf '%s' "$norm" | sed -n 's/^\([0-9][0-9]*\).*/\1/p'); printf '%s\n' "$(expr $n \* 60)"; return 0 ;; + n=$(printf '%s' "$norm" | sed -n 's/^\([0-9][0-9]*\).*/\1/p'); printf '%s\n' "$((n * 60))"; return 0 ;; [0-9]*h|[0-9]*hr|[0-9]*hrs|[0-9]*hour|[0-9]*hours) - n=$(printf '%s' "$norm" | sed -n 's/^\([0-9][0-9]*\).*/\1/p'); printf '%s\n' "$(expr $n \* 3600)"; return 0 ;; + n=$(printf '%s' "$norm" | sed -n 's/^\([0-9][0-9]*\).*/\1/p'); printf '%s\n' "$((n * 3600))"; return 0 ;; *) tokens=$(printf '%s' "$norm" | sed 's/\([0-9][0-9]*[a-z][a-z]*\)/\1 /g') total=0; ok=0 @@ -629,11 +742,11 @@ audio_parse_secs() { u=$(printf '%s' "$t" | sed -n 's/^[0-9][0-9]*\([a-z][a-z]*\)$/\1/p') case "$u" in s|sec|secs|second|seconds) add=$n ;; - m|min|mins|minute|minutes) add=$(expr $n \* 60) ;; - h|hr|hrs|hour|hours) add=$(expr $n \* 3600) ;; + m|min|mins|minute|minutes) add=$((n * 60)) ;; + h|hr|hrs|hour|hours) add=$((n * 3600)) ;; *) return 1 ;; esac - total=$(expr $total + $add); ok=1 + total=$((total + add)); ok=1 done [ "$ok" -eq 1 ] 2>/dev/null || return 1 printf '%s\n' "$total" @@ -658,16 +771,14 @@ audio_exec_with_timeout() { *s) dur_norm="${dur%s}" ;; *) dur_norm="$dur" ;; esac - - # numeric? if not, treat as no-timeout + case "$dur_norm" in *[!0-9]*|"") dur_norm=0 ;; esac - + if [ "$dur_norm" -gt 0 ] 2>/dev/null && command -v timeout >/dev/null 2>&1; then timeout "$dur_norm" "$@"; return $? fi - + if [ "$dur_norm" -gt 0 ] 2>/dev/null; then - # portable fallback watchdog "$@" & pid=$! ( @@ -679,23 +790,16 @@ audio_exec_with_timeout() { w=$! wait "$pid"; rc=$? kill -TERM "$w" 2>/dev/null || true - # map "killed by watchdog" to 124 (GNU timeout convention) [ "$rc" -eq 143 ] && rc=124 return "$rc" fi - - # no timeout + "$@" } # -------------------------------------------------------------------- # File size helper (portable across different stat implementations) # -------------------------------------------------------------------- - -# Get file size in bytes using portable method -# Input: file path -# Output: file size in bytes to stdout -# Returns: 0=success, 1=file not found or not readable file_size_bytes() { file="$1" [ -f "$file" ] || return 1 @@ -703,21 +807,12 @@ file_size_bytes() { wc -c < "$file" 2>/dev/null } -# Extract clip duration from filename -# Input: clip filename (e.g., "play_48KHz_30s_16b_2ch.wav") -# Output: duration in seconds (e.g., "30") to stdout -# Returns: 0=success, 1=unable to parse duration extract_clip_duration() { filename="$1" - - # Extract duration field from pattern: _RATE_DURATIONs_BITS_CHANNELS.wav - # Use sed to match the exact 4-field structure duration_str="$(printf '%s' "$filename" | sed -n 's/.*_[0-9.][0-9.]*KHz_\([0-9][0-9]*\)s_[0-9][0-9]*b_[0-9][0-9]*ch\.wav$/\1/p')" - if [ -z "$duration_str" ]; then return 1 fi - printf '%s\n' "$duration_str" return 0 } @@ -725,9 +820,6 @@ extract_clip_duration() { # -------------------------------------------------------------------- # 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="" @@ -744,11 +836,8 @@ build_backend_chain() { printf '%s\n' "$chain" } -# Pick a plausible ALSA capture device. -# Returns something like hw:0,0 if available, else "default". 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)) { @@ -759,7 +848,6 @@ alsa_pick_capture() { ' } -# 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:]_][[:alnum:]_]*\)[[:space:]]*$/\1/p')" @@ -772,185 +860,116 @@ alsa_pick_virtual_pcm() { return 1 } - -# Check if all required audio clips are available locally -# Usage: audio_check_clips_available "$FORMATS" "$DURATIONS" -# Returns: 0 if all clips present and non-empty, 1 if any clip missing or empty audio_check_clips_available() { formats="$1" durations="$2" - + for fmt in $formats; do for dur in $durations; do clip="$(resolve_clip "$fmt" "$dur")" - # If resolve_clip returns empty string or clip doesn't exist/is empty if [ -z "$clip" ] || [ ! -s "$clip" ]; then - return 1 # At least one clip missing or empty + return 1 fi done done - return 0 # All clips present and non-empty + return 0 } -# ---------- New Clip Discovery Functions (for 20-clip enhancement) ---------- - # ---------- Config Mapping ---------- -# Provides stable, deterministic mapping from playback_config1-playback_config10 to specific -# audio format test cases. This ensures reproducible test coverage across -# different systems and releases. -# -# Playback config numbers map to specific sample rate, bit depth, and channel combinations: -# playback_config1 → 8 KHz, 8-bit, 1ch -# playback_config2 → 16 KHz, 8-bit, 6ch -# playback_config3 → 16 KHz, 16-bit, 2ch -# playback_config4 → 22.05 KHz, 8-bit, 1ch -# playback_config5 → 24 KHz, 24-bit, 6ch -# playback_config6 → 24 KHz, 32-bit, 1ch -# playback_config7 → 32 KHz, 8-bit, 8ch -# playback_config8 → 32 KHz, 16-bit, 2ch -# playback_config9 → 44.1 KHz, 16-bit, 1ch -# playback_config10 → 48 KHz, 8-bit, 2ch - -# Translate playback_config name to test case name -# Returns descriptive test case name for given config map_config_to_testcase() { config="$1" - - # Extract config number if using playback_config format config_num="" case "$config" in playback_config*) - # Handle both formats: playback_config1 and playback_config01 config_num="$(printf '%s' "$config" | sed -n 's/^playback_config0*\([0-9][0-9]*\)$/\1/p')" - # Validate extraction succeeded - if [ -z "$config_num" ]; then - # Invalid format, return error - return 1 - fi + [ -n "$config_num" ] || return 1 ;; Config*) - # For backward compatibility config_num="$(printf '%s' "$config" | sed -n 's/^Config0*\([0-9][0-9]*\)$/\1/p')" - # Validate extraction succeeded - if [ -z "$config_num" ]; then - # Invalid format, return error - return 1 - fi + [ -n "$config_num" ] || return 1 ;; [0-9]*) - # Direct number input config_num="$config" ;; esac - - # Map config number to test case name + case "$config_num" in - 1) printf 'play_8KHz_8b_1ch\n' ;; - 2) printf 'play_16KHz_8b_6ch\n' ;; - 3) printf 'play_16KHz_16b_2ch\n' ;; - 4) printf 'play_22.05KHz_8b_1ch\n' ;; - 5) printf 'play_24KHz_24b_6ch\n' ;; - 6) printf 'play_24KHz_32b_1ch\n' ;; - 7) printf 'play_32KHz_8b_8ch\n' ;; - 8) printf 'play_32KHz_16b_2ch\n' ;; - 9) printf 'play_44.1KHz_16b_1ch\n' ;; + 1) printf 'play_8KHz_8b_1ch\n' ;; + 2) printf 'play_16KHz_8b_6ch\n' ;; + 3) printf 'play_16KHz_16b_2ch\n' ;; + 4) printf 'play_22.05KHz_8b_1ch\n' ;; + 5) printf 'play_24KHz_24b_6ch\n' ;; + 6) printf 'play_24KHz_32b_1ch\n' ;; + 7) printf 'play_32KHz_8b_8ch\n' ;; + 8) printf 'play_32KHz_16b_2ch\n' ;; + 9) printf 'play_44.1KHz_16b_1ch\n' ;; 10) printf 'play_48KHz_8b_2ch\n' ;; *) return 1 ;; esac return 0 } -# Discover all audio clip files in the clips directory -# Outputs newline-separated list of clip filenames (basenames only) to stdout -# Logs diagnostic messages to stderr -# Exit codes: 0=success, 1=directory not found or no clips discover_audio_clips() { clips_dir="${AUDIO_CLIPS_BASE_DIR:-AudioClips}" - - # Check directory exists if [ ! -d "$clips_dir" ]; then log_error "Clips directory not found: $clips_dir" >&2 return 1 fi - - # Find .wav files (only in top level, not recursive) clips="$(find "$clips_dir" -maxdepth 1 -name "*.wav" -type f 2>/dev/null | sort)" - - # Check if any clips found if [ -z "$clips" ]; then log_error "No .wav files found in $clips_dir" >&2 return 1 fi - - # Output basenames only to stdout for clip in $clips; do basename "$clip" done return 0 } -# Parse clip filename to extract metadata -# Input: yesterday_48KHz_30s_16b_2ch.wav -# Output: rate=48KHz bits=16b channels=2ch (space-separated key=value pairs) -# Returns: 0=success, 1=parse failure parse_clip_metadata() { filename="$1" - - # Extract rate, bits, and channels in one sed call - # Pattern matches exact 4-field structure from end: _RATE_DURATIONs_BITS_CHANNELS.wav - # Anchored to .wav extension to ensure we're matching the correct fields + # Produces: " " metadata="$(printf '%s' "$filename" | sed -n 's/.*_\([0-9.][0-9.]*KHz\)_\([0-9][0-9]*s\)_\([0-9][0-9]*b\)_\([0-9][0-9]*ch\)\.wav$/\1 \3 \4/p')" - - # Validate extraction succeeded if [ -z "$metadata" ]; then log_warn "Cannot parse metadata from: $filename (skipping)" return 1 fi - - # Split extracted fields (rate bits channels) - set -- $metadata - rate="$1"; bits="$2"; channels="$3" - - # Validate all components present - if [ -z "$rate" ] || [ -z "$bits" ] || [ -z "$channels" ]; then + + rate="${metadata%% *}" + rest="${metadata#* }" + bits="${rest%% *}" + channels="${rest#* }" + + if [ -z "$rate" ] || [ -z "$bits" ] || [ -z "$channels" ] || [ "$rest" = "$metadata" ]; then log_warn "Cannot parse metadata from: $filename (skipping)" return 1 fi - + printf 'rate=%s bits=%s channels=%s\n' "$rate" "$bits" "$channels" return 0 } -# Generate test case name from clip filename -# Input: yesterday_48KHz_30s_16b_2ch.wav -# Output: play_48KHz_16b_2ch -# Returns: 0=success, 1=parse failure generate_clip_testcase_name() { filename="$1" - - # Parse metadata (returns "rate=48KHz bits=16b channels=2ch") metadata="$(parse_clip_metadata "$filename")" || return 1 - - # Extract values using positional parameters and prefix stripping - set -- $metadata - rate="${1#rate=}" - bits="${2#bits=}" - channels="${3#channels=}" - - # Generate test case name + + first="${metadata%% *}" + rest="${metadata#* }" + second="${rest%% *}" + third="${rest#* }" + + rate="${first#rate=}" + bits="${second#bits=}" + channels="${third#channels=}" + printf 'play_%s_%s_%s\n' "$rate" "$bits" "$channels" return 0 } -# Resolve clip file path from test case name or clip name -# Input: play_48KHz_16b_2ch OR 48KHz_16b_2ch OR yesterday_48KHz_30s_16b_2ch.wav -# Output: AudioClips/yesterday_48KHz_30s_16b_2ch.wav -# Returns: 0=success, 1=not found resolve_clip_by_name() { name="$1" clips_dir="${AUDIO_CLIPS_BASE_DIR:-AudioClips}" - - # If name already looks like a filename, try direct path + if printf '%s' "$name" | grep -F -q -- '.wav'; then clip_path="$clips_dir/$name" if [ -f "$clip_path" ]; then @@ -958,36 +977,23 @@ resolve_clip_by_name() { return 0 fi fi - - # Strip "play_" prefix if present + search_name="$(printf '%s' "$name" | sed 's/^play_//')" - - # Search for matching clip using literal string matching for clip_file in "$clips_dir"/*.wav; do [ -f "$clip_file" ] || continue clip_basename="$(basename "$clip_file")" - - # Check if clip contains the search pattern (literal string match) if printf '%s' "$clip_basename" | grep -F -q -- "$search_name"; then printf '%s\n' "$clip_file" return 0 fi done - return 1 } -# Validate clip name against available clips -# Input: requested_name (e.g., play_48KHz_16b_2ch OR playback_config1), available_clips (list) -# Output: matching clip filename to stdout -# Logs error messages to stderr -# Returns: 0=found, 1=not found validate_clip_name() { requested_name="$1" available_clips="$2" - - # Check if requested_name is a generic config name (playback_config1, Config1, etc.) - # Support both formats for backward compatibility + config_num="" case "$requested_name" in playback_config*) @@ -997,35 +1003,31 @@ validate_clip_name() { config_num="$(printf '%s' "$requested_name" | sed -n 's/^[Cc]onfig\([0-9][0-9]*\)$/\1/p')" ;; esac - + if [ -n "$config_num" ]; then - # Generic config name - map to clip by index (1-based) - # Count total clips first using POSIX-compliant approach - set -- $available_clips - idx=$# - - # Validate config number is positive and within range + idx=0 + for _clip in $available_clips; do + idx=$((idx + 1)) + done + if [ "$config_num" -le 0 ] 2>/dev/null || [ "$config_num" -gt "$idx" ] 2>/dev/null; then log_error "Invalid config number: $requested_name. Available range: Config1 to Config$idx. Please check again." >&2 return 1 fi - - # Get clip by index (1-based) using POSIX-compliant approach + current_idx=0 for clip in $available_clips; do - current_idx=$(expr $current_idx + 1) + current_idx=$((current_idx + 1)) if [ "$current_idx" -eq "$config_num" ]; then printf '%s\n' "$clip" return 0 fi done - - # This shouldn't happen, but just in case + log_error "Invalid config number: $requested_name. Available range: Config1 to Config$idx. Please check again." >&2 return 1 fi - - # Try exact match for specific clip names (play_48KHz_16b_2ch format) + for clip in $available_clips; do test_name="$(generate_clip_testcase_name "$clip" 2>/dev/null)" || continue if [ "$test_name" = "$requested_name" ]; then @@ -1033,34 +1035,28 @@ validate_clip_name() { return 0 fi done - - # No match found - count available clips for helpful message using POSIX-compliant approach - set -- $available_clips - idx=$# - - # No match found - provide helpful error message with range + + idx=0 + for _clip in $available_clips; do + idx=$((idx + 1)) + done + log_error "Wrong clip name: '$requested_name'. Available range: playback_config1 to playback_config$idx. Please check again." >&2 return 1 } -# Input: filter (space-separated patterns), available_clips (list) -# Output: filtered clip list -# Returns: 0=success, 1=no matches apply_clip_filter() { filter="$1" available_clips="$2" - - # If no filter, return all clips + if [ -z "$filter" ]; then printf '%s\n' "$available_clips" return 0 fi - - # Apply filter + filtered="" for clip in $available_clips; do for pattern in $filter; do - # Match against filename or test case name test_name="$(generate_clip_testcase_name "$clip" 2>/dev/null)" || continue if printf '%s %s' "$clip" "$test_name" | grep -F -q -- "$pattern"; then filtered="$filtered $clip" @@ -1068,99 +1064,73 @@ apply_clip_filter() { fi done done - - # Remove leading space + filtered="$(printf '%s' "$filtered" | sed 's/^ //')" - - # Check if filter matched anything if [ -z "$filtered" ]; then log_error "Filter '$filter' matched no clips" >&2 log_info "Available clips:" >&2 for clip in $available_clips; do - log_info " - $(basename "$clip")" >&2 + log_info " - $(basename "$clip")" >&2 done return 1 fi - + printf '%s\n' "$filtered" return 0 } -# Validate clip file is accessible and non-empty -# Input: clip_path -# Returns: 0=valid, 1=invalid validate_clip_file() { clip_path="$1" - - # Check exists if [ ! -f "$clip_path" ]; then log_error "Clip file not found: $clip_path" return 1 fi - - # Check readable if [ ! -r "$clip_path" ]; then log_error "Clip file not readable: $clip_path" return 1 fi - - # Check not empty using portable file size helper size="$(file_size_bytes "$clip_path")" if [ -z "$size" ] || [ "$size" -le 0 ] 2>/dev/null; then log_error "Clip file is empty: $clip_path" return 1 fi - return 0 } -# Discover and filter clips based on user input -# Input: clip_names (explicit list), clip_filter (pattern filter) -# Output: final list of clip filenames to test (to stdout) -# Logs error messages to stderr -# Returns: 0=success, 1=no valid clips discover_and_filter_clips() { clip_names="$1" clip_filter="$2" - - # Discover all available clips (logs go to stderr automatically) + available_clips="$(discover_audio_clips)" || { log_error "Failed to discover audio clips" >&2 return 1 } - - # If explicit clip names provided, validate and use them + if [ -n "$clip_names" ]; then validated="" failed_names="" - + for name in $clip_names; do - # Validate clip name - let error messages display to stderr if clip="$(validate_clip_name "$name" "$available_clips")"; then validated="$validated $clip" else failed_names="$failed_names $name" fi done - + validated="$(printf '%s' "$validated" | sed 's/^ //')" failed_names="$(printf '%s' "$failed_names" | sed 's/^ //')" - - if [ -z "$validated" ]; then - # Don't repeat the error - validate_clip_name already showed it - return 1 - fi - - # Warn about any failed names (only if there are some valid ones) + + [ -n "$validated" ] || return 1 + if [ -n "$failed_names" ]; then log_warn "Invalid clip/config names skipped: $failed_names" >&2 fi - + printf '%s\n' "$validated" return 0 fi - - # Apply filter if provided + if [ -n "$clip_filter" ]; then filtered="$(apply_clip_filter "$clip_filter" "$available_clips" 2>/dev/null)" || { log_error "Filter did not match any clips" >&2 @@ -1169,105 +1139,79 @@ discover_and_filter_clips() { printf '%s\n' "$filtered" return 0 fi - - # No filter - return all clips + printf '%s\n' "$available_clips" return 0 } # ---------- Record Configuration Functions (10-config enhancement) ---------- - -# Discover all available record configurations -# Returns: space-separated list of record_config1 through record_config10 -# Exit codes: 0=success (always succeeds - configs are predefined) discover_record_configs() { printf '%s\n' "record_config1 record_config2 record_config3 record_config4 record_config5 record_config6 record_config7 record_config8 record_config9 record_config10" return 0 } -# Get recording parameters for a specific config -# Input: config_name (e.g., record_config1, record_config01, record_8KHz_1ch) -# Output: "rate channels" (e.g., "8000 1") -# Returns: 0=success, 1=invalid config get_record_config_params() { config_name="$1" - - # Normalize config name to handle both formats (record_config1 and record_config01) normalized_name="$config_name" case "$config_name" in record_config0*) - # Extract number and remove leading zero for internal processing config_num="$(printf '%s' "$config_name" | sed -n 's/^record_config0*\([0-9][0-9]*\)$/\1/p')" - # Only normalize if extraction succeeded if [ -n "$config_num" ]; then normalized_name="record_config$config_num" fi - # If config_num is empty, normalized_name stays as original config_name ;; esac - + case "$normalized_name" in - record_config1|record_8KHz_1ch) printf '%s\n' "8000 1" ;; - record_config2|record_16KHz_1ch) printf '%s\n' "16000 1" ;; - record_config3|record_16KHz_2ch) printf '%s\n' "16000 2" ;; - record_config4|record_24KHz_1ch) printf '%s\n' "24000 1" ;; - record_config5|record_32KHz_2ch) printf '%s\n' "32000 2" ;; - record_config6|record_44.1KHz_2ch) printf '%s\n' "44100 2" ;; - record_config7|record_48KHz_2ch) printf '%s\n' "48000 2" ;; - record_config8|record_48KHz_6ch) printf '%s\n' "48000 6" ;; - record_config9|record_96KHz_2ch) printf '%s\n' "96000 2" ;; - record_config10|record_96KHz_6ch) printf '%s\n' "96000 6" ;; + record_config1|record_8KHz_1ch) printf '%s\n' "8000 1" ;; + record_config2|record_16KHz_1ch) printf '%s\n' "16000 1" ;; + record_config3|record_16KHz_2ch) printf '%s\n' "16000 2" ;; + record_config4|record_24KHz_1ch) printf '%s\n' "24000 1" ;; + record_config5|record_32KHz_2ch) printf '%s\n' "32000 2" ;; + record_config6|record_44.1KHz_2ch) printf '%s\n' "44100 2" ;; + record_config7|record_48KHz_2ch) printf '%s\n' "48000 2" ;; + record_config8|record_48KHz_6ch) printf '%s\n' "48000 6" ;; + record_config9|record_96KHz_2ch) printf '%s\n' "96000 2" ;; + record_config10|record_96KHz_6ch) printf '%s\n' "96000 6" ;; *) return 1 ;; esac return 0 } -# Generate descriptive test case name from config name -# Input: record_config1 or record_config01 -# Output: record_8KHz_1ch -# Returns: 0=success, 1=invalid config generate_record_testcase_name() { config_name="$1" - - # Normalize config name to handle both formats (record_config1 and record_config01) normalized_name="$config_name" case "$config_name" in record_config0*) - # Extract number and remove leading zero for internal processing config_num="$(printf '%s' "$config_name" | sed -n 's/^record_config0*\([0-9][0-9]*\)$/\1/p')" normalized_name="record_config$config_num" ;; esac - + case "$normalized_name" in - record_config1) printf '%s\n' "record_8KHz_1ch" ;; - record_config2) printf '%s\n' "record_16KHz_1ch" ;; - record_config3) printf '%s\n' "record_16KHz_2ch" ;; - record_config4) printf '%s\n' "record_24KHz_1ch" ;; - record_config5) printf '%s\n' "record_32KHz_2ch" ;; - record_config6) printf '%s\n' "record_44.1KHz_2ch" ;; - record_config7) printf '%s\n' "record_48KHz_2ch" ;; - record_config8) printf '%s\n' "record_48KHz_6ch" ;; - record_config9) printf '%s\n' "record_96KHz_2ch" ;; + record_config1) printf '%s\n' "record_8KHz_1ch" ;; + record_config2) printf '%s\n' "record_16KHz_1ch" ;; + record_config3) printf '%s\n' "record_16KHz_2ch" ;; + record_config4) printf '%s\n' "record_24KHz_1ch" ;; + record_config5) printf '%s\n' "record_32KHz_2ch" ;; + record_config6) printf '%s\n' "record_44.1KHz_2ch" ;; + record_config7) printf '%s\n' "record_48KHz_2ch" ;; + record_config8) printf '%s\n' "record_48KHz_6ch" ;; + record_config9) printf '%s\n' "record_96KHz_2ch" ;; record_config10) printf '%s\n' "record_96KHz_6ch" ;; - *) printf '%s\n' "$config_name" ;; # Already descriptive or unknown + *) printf '%s\n' "$config_name" ;; esac return 0 } -# Generate output filename with parameters -# Input: testcase_base (e.g., "record_short"), rate (e.g., "48000"), channels (e.g., "2") -# Output: record_short_48KHz_2ch.wav -# Returns: 0=success generate_record_filename() { testcase_base="$1" rate="$2" channels="$3" - - # Convert rate to KHz format + rate_khz="$rate" case "$rate" in - 8000) rate_khz="8KHz" ;; + 8000) rate_khz="8KHz" ;; 16000) rate_khz="16KHz" ;; 22050) rate_khz="22.05KHz" ;; 24000) rate_khz="24KHz" ;; @@ -1280,90 +1224,62 @@ generate_record_filename() { 192000) rate_khz="192KHz" ;; 352800) rate_khz="352.8KHz" ;; 384000) rate_khz="384KHz" ;; - *) rate_khz="${rate}Hz" ;; # Fallback for unknown rates + *) rate_khz="${rate}Hz" ;; esac - + printf '%s_%s_%sch.wav\n' "$testcase_base" "$rate_khz" "$channels" return 0 } -# Validate record config name -# Input: requested_name (e.g., record_config1, record_8KHz_1ch) -# Returns: 0=valid, 1=invalid (with helpful error message) validate_record_config_name() { requested_name="$1" - - # Validate by checking if get_record_config_params() supports it - # This eliminates redundant pattern matching that could be misleading if get_record_config_params "$requested_name" >/dev/null 2>&1; then return 0 fi - log_error "Invalid record config name: $requested_name" >&2 log_error "Available configs: record_config1-record_config10, record_8KHz_1ch, record_16KHz_1ch, record_16KHz_2ch, record_24KHz_1ch, record_32KHz_2ch, record_44.1KHz_2ch, record_48KHz_2ch, record_48KHz_6ch, record_96KHz_2ch, record_96KHz_6ch" >&2 return 1 } -# Apply filter to record configs -# Input: filter (space-separated patterns), available_configs (list) -# Output: filtered config list -# Returns: 0=success, 1=no matches apply_record_config_filter() { filter="$1" available_configs="$2" - - # If no filter, return all configs + if [ -z "$filter" ]; then printf '%s\n' "$available_configs" return 0 fi - - # Apply filter + filtered="" for config in $available_configs; do - # Generate descriptive name for matching desc_name="$(generate_record_testcase_name "$config" 2>/dev/null)" || continue - for pattern in $filter; do - # Match against config name or descriptive name if printf '%s %s' "$config" "$desc_name" | grep -F -q -- "$pattern"; then filtered="$filtered $config" break fi done done - - # Remove leading space + filtered="$(printf '%s' "$filtered" | sed 's/^ //')" - - # Check if filter matched anything if [ -z "$filtered" ]; then log_error "Filter '$filter' matched no record configs" >&2 log_info "Available configs: record_config1 to record_config10" >&2 return 1 fi - + printf '%s\n' "$filtered" return 0 } -# Discover and filter record configs based on user input -# Input: config_names (explicit list), config_filter (pattern filter) -# Output: final list of config names to test (to stdout) -# Logs error messages to stderr -# Returns: 0=success, 1=no valid configs discover_and_filter_record_configs() { config_names="$1" config_filter="$2" - - # Get all available configs available_configs="$(discover_record_configs)" - - # If explicit config names provided, validate and use them + if [ -n "$config_names" ]; then validated="" failed_names="" - for name in $config_names; do if validate_record_config_name "$name"; then validated="$validated $name" @@ -1371,31 +1287,26 @@ discover_and_filter_record_configs() { failed_names="$failed_names $name" fi done - + validated="$(printf '%s' "$validated" | sed 's/^ //')" failed_names="$(printf '%s' "$failed_names" | sed 's/^ //')" - - if [ -z "$validated" ]; then - return 1 - fi - - # Warn about any failed names (only if there are some valid ones) + + [ -n "$validated" ] || return 1 + if [ -n "$failed_names" ]; then log_warn "Invalid record config names skipped: $failed_names" >&2 fi - + printf '%s\n' "$validated" return 0 fi - - # Apply filter if provided + if [ -n "$config_filter" ]; then filtered="$(apply_record_config_filter "$config_filter" "$available_configs")" || return 1 printf '%s\n' "$filtered" return 0 fi - - # No filter - return all configs + printf '%s\n' "$available_configs" return 0 }