diff --git a/Runner/suites/Multimedia/Audio/AudioPlayback/AudioPlayback_minimal.yaml b/Runner/suites/Multimedia/Audio/AudioPlayback/AudioPlayback_minimal.yaml new file mode 100644 index 00000000..af65a6e1 --- /dev/null +++ b/Runner/suites/Multimedia/Audio/AudioPlayback/AudioPlayback_minimal.yaml @@ -0,0 +1,24 @@ +metadata: + name: AudioPlayback minimal build + format: "Lava-Test Test Definition 1.0" + description: "AudioPlayback on minimal build using direct ALSA path" + maintainer: + - srikanth kumar + os: + - linux + scope: + - functional + +params: + CLIPS_PATH: "/home/AudioClips" + BACKEND: "alsa" + SINK: "speakers" + LOOPS: "1" + TIMEOUT: "0" + +run: + steps: + - REPO_PATH=$PWD + - cd Runner/suites/Multimedia/Audio/AudioPlayback/ + - ./run.sh --backend "${BACKEND}" --sink "${SINK}" --audio-clips-path "${CLIPS_PATH}" --loops "${LOOPS}" --timeout "${TIMEOUT}" --audio-bootstrap false || true + - ./send-to-lava.sh AudioPlayback.res diff --git a/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh b/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh index e685987a..83fafa9b 100755 --- a/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh +++ b/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh @@ -40,8 +40,13 @@ fi # shellcheck disable=SC1091 . "$TOOLS/lib_video.sh" +SYSTEMD_AVAILABLE=0 +if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then + SYSTEMD_AVAILABLE=1 +fi + TESTNAME="AudioPlayback" -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 @@ -57,28 +62,15 @@ for arg in "$@"; do prev_arg="$arg" done -# Early failure handling with suffix support -if ! setup_overlay_audio_environment; then - log_fail "Overlay audio environment setup failed" - if [ -n "$RES_SUFFIX" ]; then - echo "$TESTNAME FAIL" > "$SCRIPT_DIR/${TESTNAME}_${RES_SUFFIX}.res" - else - echo "$TESTNAME FAIL" > "$SCRIPT_DIR/${TESTNAME}.res" - fi - exit 0 -fi - -# LOGDIR will be set after parsing command-line arguments (to apply RES_SUFFIX correctly) - # ---- Assets ---- -AUDIO_TAR_URL="${AUDIO_TAR_URL:-https://github.com/qualcomm-linux/qcom-linux-testkit/releases/download/Pulse-Audio-Files-v1.0/AudioClips.tar.gz}" +AUDIO_TAR_URL="${AUDIO_TAR_URL:-https://github.com/qualcomm-linux/qcom-linux-testkit/releases/download/AudioClips-v1.1/AudioClips.tar.gz}" export AUDIO_TAR_URL # ------------- Defaults / CLI ------------- AUDIO_BACKEND="" SINK_CHOICE="${SINK_CHOICE:-speakers}" # speakers|null -FORMATS="" # Will be set to default only if using legacy mode -DURATIONS="" # Will be set to default only if using legacy mode +FORMATS="" # Will be set to default only if using legacy mode +DURATIONS="" # Will be set to default only if using legacy mode LOOPS="${LOOPS:-1}" TIMEOUT="${TIMEOUT:-0}" # 0 = no timeout (recommended) STRICT="${STRICT:-0}" @@ -88,10 +80,19 @@ EXTRACT_AUDIO_ASSETS="${EXTRACT_AUDIO_ASSETS:-true}" ENABLE_NETWORK_DOWNLOAD="${ENABLE_NETWORK_DOWNLOAD:-false}" # Default: no network operations AUDIO_CLIPS_BASE_DIR="${AUDIO_CLIPS_BASE_DIR:-}" # Custom path for audio clips (CI use) +AUDIO_BOOTSTRAP_MODE="${AUDIO_BOOTSTRAP_MODE:-auto}" +AUDIO_RUNTIME_DIR="${AUDIO_RUNTIME_DIR:-}" +MINIMAL_RAMDISK_MODE=0 +AUDIO_STARTED_PIDS="" +AUDIO_CREATED_RUNTIME_DIR=0 +AUDIO_SYSTEMD_MANAGED=0 +AUDIO_ALSA_PLAYBACK_DEVICE="" +export AUDIO_BOOTSTRAP_MODE AUDIO_RUNTIME_DIR AUDIO_STARTED_PIDS AUDIO_CREATED_RUNTIME_DIR MINIMAL_RAMDISK_MODE AUDIO_SYSTEMD_MANAGED AUDIO_ALSA_PLAYBACK_DEVICE + # New clip-based testing options -CLIP_NAMES="" # Explicit clip names to test (e.g., "play_48KHz_16b_2ch play_8KHz_8b_1ch") -CLIP_FILTER="" # Filter pattern for clips (e.g., "48KHz" or "16b") -USE_CLIP_DISCOVERY="${USE_CLIP_DISCOVERY:-auto}" # auto|true|false +CLIP_NAMES="" # Explicit clip names to test (e.g., "play_48KHz_16b_2ch play_8KHz_8b_1ch") +CLIP_FILTER="" # Filter pattern for clips (e.g., "48KHz" or "16b") +USE_CLIP_DISCOVERY="${USE_CLIP_DISCOVERY:-auto}" # auto|true|false # Network bring-up knobs (match video behavior) if [ -z "${NET_STABILIZE_SLEEP:-}" ]; then @@ -109,17 +110,19 @@ usage() { Usage: $0 [options] --backend {pipewire|pulseaudio} --sink {speakers|null} - --formats "wav" # Legacy matrix mode only - --durations "short|short medium" # Legacy matrix mode only (not recommended for new tests) - --clip-name "play_48KHz_16b_2ch" # Test specific clip(s) by name (space-separated) + --formats "wav" # Legacy matrix mode only + --durations "short|short medium" # Legacy matrix mode only (not recommended for new tests) + --clip-name "play_48KHz_16b_2ch" # Test specific clip(s) by name (space-separated) # Also supports playback_config1, playback_config2, ..., playback_config10 - --clip-filter "48KHz" # Filter clips by pattern - --res-suffix SUFFIX # Suffix for unique result file (e.g., "Config1") + --clip-filter "48KHz" # Filter clips by pattern + --res-suffix SUFFIX # Suffix for unique result file (e.g., "Config1") # Generates AudioPlayback_SUFFIX.res instead of AudioPlayback.res --loops N --timeout SECS # set 0 to disable watchdog --enable-network-download --audio-clips-path PATH # Custom location for audio clips (CI use) + --audio-bootstrap {auto|true|false} + --runtime-dir PATH --strict --no-dmesg --no-extract-assets @@ -136,8 +139,8 @@ Testing Modes: - Examples: $0 --clip-name "playback_config1 playback_config7" $0 --clip-filter "48KHz" - $0 --clip-name "playback_config1" --res-suffix "Config1" # CI/LAVA use - + $0 --clip-name "playback_config1" --res-suffix "Config1" # CI/LAVA use + Legacy Matrix Mode: - Uses --formats and --durations to generate test matrix - Maintained for backward compatibility @@ -158,12 +161,12 @@ while [ $# -gt 0 ]; do ;; --formats) FORMATS="$2" - USE_CLIP_DISCOVERY=false # Explicit formats = use old matrix mode + USE_CLIP_DISCOVERY=false # Explicit formats = use old matrix mode shift 2 ;; --durations) DURATIONS="$2" - USE_CLIP_DISCOVERY=false # Explicit durations = use old matrix mode + USE_CLIP_DISCOVERY=false # Explicit durations = use old matrix mode shift 2 ;; --clip-name) @@ -188,6 +191,16 @@ while [ $# -gt 0 ]; do TIMEOUT="$2" shift 2 ;; + --audio-bootstrap) + AUDIO_BOOTSTRAP_MODE="$2" + export AUDIO_BOOTSTRAP_MODE + shift 2 + ;; + --runtime-dir) + AUDIO_RUNTIME_DIR="$2" + export AUDIO_RUNTIME_DIR + shift 2 + ;; --strict) case "$2" in --*|"") @@ -271,6 +284,24 @@ mkdir -p "$LOGDIR" # ------------- Mode Detection and Validation ------------- +if [ "$SYSTEMD_AVAILABLE" -eq 1 ]; then + if ! setup_overlay_audio_environment; then + log_warn "Overlay audio environment setup failed; continuing with backend recovery flow" + fi +else + log_info "systemd not available; skipping overlay audio environment setup (minimal ramdisk mode)" +fi + +if [ "$SYSTEMD_AVAILABLE" -eq 0 ]; then + MINIMAL_RAMDISK_MODE=1 + export MINIMAL_RAMDISK_MODE + log_info "Detected minimal ramdisk environment (systemd unavailable)" +else + log_info "Detected standard userspace environment (systemd available)" +fi + +trap 'audio_cleanup_started_daemons' EXIT HUP INT TERM + # Check for conflicting parameters (discovery vs legacy mode) if { [ -n "$CLIP_NAMES" ] || [ -n "$CLIP_FILTER" ]; } && { [ -n "$FORMATS" ] || [ -n "$DURATIONS" ]; }; then log_error "Cannot mix clip discovery parameters (--clip-name, --clip-filter) with legacy matrix parameters (--formats, --durations)" @@ -299,7 +330,7 @@ if [ "$USE_CLIP_DISCOVERY" = "auto" ]; then break fi done - + if [ "$wav_found" = "true" ]; then USE_CLIP_DISCOVERY=true log_info "Auto-detected clip discovery mode (found clips in $clips_dir)" @@ -313,7 +344,6 @@ if [ "$USE_CLIP_DISCOVERY" = "auto" ]; then fi fi - # Validate CLI option conflicts if [ -n "$CLIP_NAMES" ] && [ -n "$CLIP_FILTER" ]; then log_warn "Both --clip-name and --clip-filter specified" @@ -323,7 +353,7 @@ fi # Validate numeric parameters case "$LOOPS" in - ''|*[!0-9]*) + ''|*[!0-9]*) log_error "Invalid --loops value: $LOOPS (must be positive integer)" exit 1 ;; @@ -357,7 +387,7 @@ if [ -n "$AUDIO_CLIPS_BASE_DIR" ]; then log_info "Using custom audio clips path: $AUDIO_CLIPS_BASE_DIR" fi -log_info "Args: backend=${AUDIO_BACKEND:-auto} sink=$SINK_CHOICE loops=$LOOPS timeout=$TIMEOUT formats='$FORMATS' durations='$DURATIONS' strict=$STRICT dmesg=$DMESG_SCAN extract=$EXTRACT_AUDIO_ASSETS network_download=$ENABLE_NETWORK_DOWNLOAD clips_path=${AUDIO_CLIPS_BASE_DIR:-default}" +log_info "Args: backend=${AUDIO_BACKEND:-auto} sink=$SINK_CHOICE loops=$LOOPS timeout=$TIMEOUT formats='$FORMATS' durations='$DURATIONS' strict=$STRICT dmesg=$DMESG_SCAN extract=$EXTRACT_AUDIO_ASSETS network_download=$ENABLE_NETWORK_DOWNLOAD clips_path=${AUDIO_CLIPS_BASE_DIR:-default} bootstrap=$AUDIO_BOOTSTRAP_MODE runtime_dir=${AUDIO_RUNTIME_DIR:-auto}" # --- Rootfs minimum size check (mirror video policy) --- if [ "$TOP_LEVEL_RUN" -eq 1 ]; then @@ -370,9 +400,20 @@ fi if [ "$TOP_LEVEL_RUN" -eq 1 ]; then if [ "${EXTRACT_AUDIO_ASSETS}" = "true" ]; then # First check: Do we have all files we need? - if audio_check_clips_available "$FORMATS" "$DURATIONS"; then - log_info "All required audio clips present locally, skipping all network operations" + clips_ready=1 + if [ "$USE_CLIP_DISCOVERY" = "true" ]; then + if audio_has_runnable_discovery_clips; then + log_info "Runnable discovery clips present locally, skipping all network operations" + clips_ready=0 + fi else + if audio_check_clips_available "$FORMATS" "$DURATIONS"; then + log_info "All required audio clips present locally, skipping all network operations" + clips_ready=0 + fi + fi + + if [ "$clips_ready" -ne 0 ]; then # Files missing - check if network download is enabled if [ "${ENABLE_NETWORK_DOWNLOAD}" = "true" ]; then log_info "Audio clips missing, network download enabled - bringing network online" @@ -393,9 +434,10 @@ if [ "$TOP_LEVEL_RUN" -eq 1 ]; then else sleep "${NET_STABILIZE_SLEEP}" fi - + # Download and extract audio clips tarball log_info "Downloading audio clips from: $AUDIO_TAR_URL" + log_info "exec: audio_fetch_assets_from_url \"$AUDIO_TAR_URL\"" if audio_fetch_assets_from_url "$AUDIO_TAR_URL"; then log_info "Audio clips downloaded and extracted successfully" else @@ -419,33 +461,196 @@ fi # Resolve backend if [ -z "$AUDIO_BACKEND" ]; then - AUDIO_BACKEND="$(detect_audio_backend)" + AUDIO_BACKEND="$(detect_audio_backend 2>/dev/null || echo "")" fi + +AUDIO_SYSTEMD_MANAGED=0 +if [ -n "$AUDIO_BACKEND" ]; then + if audio_backend_is_systemd_managed "$AUDIO_BACKEND"; then + AUDIO_SYSTEMD_MANAGED=1 + fi +fi +export AUDIO_SYSTEMD_MANAGED + if [ -z "$AUDIO_BACKEND" ]; then - log_skip "$TESTNAME SKIP - no audio backend running" - echo "$TESTNAME SKIP" >"$RES_FILE" - exit 0 + if audio_playback_alsa_probe; then + AUDIO_BACKEND="alsa" + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + log_info "Using backend: alsa (direct minimal-build fallback)" + elif audio_bootstrap_backend_if_needed; then + AUDIO_BACKEND="$(detect_audio_backend 2>/dev/null || echo "")" + if [ -z "$AUDIO_BACKEND" ]; then + if audio_playback_alsa_probe; then + AUDIO_BACKEND="alsa" + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + log_info "Using backend: alsa (direct minimal-build fallback)" + else + log_skip "$TESTNAME SKIP - no audio backend running" + echo "$TESTNAME SKIP" >"$RES_FILE" + exit 0 + fi + fi + else + log_skip "$TESTNAME SKIP - no audio backend running" + echo "$TESTNAME SKIP" >"$RES_FILE" + exit 0 + fi fi + log_info "Using backend: $AUDIO_BACKEND" -if ! check_audio_daemon "$AUDIO_BACKEND"; then +backend_ok=0 +if [ "$AUDIO_BACKEND" = "alsa" ]; then + if audio_playback_alsa_probe; then + backend_ok=1 + fi +else + if audio_backend_ready "$AUDIO_BACKEND"; then + backend_ok=1 + else + if check_audio_daemon "$AUDIO_BACKEND"; then + backend_ok=1 + fi + fi +fi + +if [ "$backend_ok" -ne 1 ]; then + if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then + log_warn "$TESTNAME: backend not available ($AUDIO_BACKEND) - attempting restart+retry once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + if audio_backend_ready "$AUDIO_BACKEND"; then + backend_ok=1 + else + if check_audio_daemon "$AUDIO_BACKEND"; then + backend_ok=1 + fi + fi + fi +fi + +if [ "$backend_ok" -ne 1 ] && [ "$AUDIO_BACKEND" != "alsa" ]; then + log_warn "$TESTNAME: backend not available ($AUDIO_BACKEND) - attempting manual bootstrap" + if audio_bootstrap_backend_if_needed; then + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + if audio_backend_ready "$AUDIO_BACKEND"; then + backend_ok=1 + else + if check_audio_daemon "$AUDIO_BACKEND"; then + backend_ok=1 + fi + fi + fi +fi + +if [ "$backend_ok" -ne 1 ] && [ "$AUDIO_BACKEND" != "alsa" ]; then + if audio_playback_alsa_probe; then + log_warn "$TESTNAME: falling back to ALSA direct playback path" + AUDIO_BACKEND="alsa" + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + backend_ok=1 + fi +fi + +if [ "$backend_ok" -ne 1 ]; then log_skip "$TESTNAME SKIP - backend not available: $AUDIO_BACKEND" echo "$TESTNAME SKIP" >"$RES_FILE" exit 0 fi # Dependencies per backend -if [ "$AUDIO_BACKEND" = "pipewire" ]; then - if ! check_dependencies wpctl pw-play; then - log_skip "$TESTNAME SKIP - missing PipeWire utils" +case "$AUDIO_BACKEND" in + pipewire) + if ! check_dependencies pw-play; then + if audio_playback_alsa_probe && check_dependencies aplay; then + log_warn "$TESTNAME: PipeWire playback utility missing - falling back to ALSA" + AUDIO_BACKEND="alsa" + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + else + log_skip "$TESTNAME SKIP - missing PipeWire playback utility" + echo "$TESTNAME SKIP" >"$RES_FILE" + exit 0 + fi + fi + ;; + pulseaudio) + if ! check_dependencies paplay; then + if audio_playback_alsa_probe && check_dependencies aplay; then + log_warn "$TESTNAME: PulseAudio playback utility missing - falling back to ALSA" + AUDIO_BACKEND="alsa" + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + else + log_skip "$TESTNAME SKIP - missing PulseAudio playback utility" + echo "$TESTNAME SKIP" >"$RES_FILE" + exit 0 + fi + fi + ;; + alsa) + if ! check_dependencies aplay; then + log_skip "$TESTNAME SKIP - missing ALSA playback utility" + echo "$TESTNAME SKIP" >"$RES_FILE" + exit 0 + fi + ;; + *) + log_skip "$TESTNAME SKIP - unsupported backend: $AUDIO_BACKEND" echo "$TESTNAME SKIP" >"$RES_FILE" exit 0 + ;; +esac + +if [ "$AUDIO_BACKEND" = "pipewire" ]; then + if ! audio_pw_ctl_ok 2>/dev/null; then + if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then + log_warn "$TESTNAME: wpctl not responsive - attempting restart+retry once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + else + log_warn "$TESTNAME: PipeWire control-plane not responsive - attempting ALSA fallback" + fi + + if ! audio_pw_ctl_ok 2>/dev/null; then + if audio_playback_alsa_probe && check_dependencies aplay; then + log_warn "$TESTNAME: falling back to ALSA direct playback path" + AUDIO_BACKEND="alsa" + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + else + log_skip "$TESTNAME SKIP - PipeWire control-plane not responsive" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + fi + fi fi -else - if ! check_dependencies pactl paplay; then - log_skip "$TESTNAME SKIP - missing PulseAudio utils" - echo "$TESTNAME SKIP" >"$RES_FILE" - exit 0 +elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then + if ! audio_pa_ctl_ok 2>/dev/null; then + if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then + log_warn "$TESTNAME: pactl not responsive - attempting restart+retry once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + else + log_warn "$TESTNAME: PulseAudio control-plane not responsive - attempting ALSA fallback" + fi + + if ! audio_pa_ctl_ok 2>/dev/null; then + if audio_playback_alsa_probe && check_dependencies aplay; then + log_warn "$TESTNAME: falling back to ALSA direct playback path" + AUDIO_BACKEND="alsa" + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + else + log_skip "$TESTNAME SKIP - PulseAudio control-plane not responsive" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + fi + fi fi fi @@ -464,6 +669,17 @@ case "$AUDIO_BACKEND:$SINK_CHOICE" in pulseaudio:*) SINK_ID="$(pa_default_speakers)" ;; + alsa:null) + SINK_ID="null" + ;; + alsa:*) + audio_playback_alsa_prepare >/dev/null 2>&1 || true + if [ -n "${AUDIO_ALSA_PLAYBACK_DEVICE:-}" ]; then + SINK_ID="$AUDIO_ALSA_PLAYBACK_DEVICE" + else + SINK_ID="$(audio_playback_pick_alsa_sink)" + fi + ;; esac if [ -z "$SINK_ID" ]; then @@ -479,13 +695,16 @@ if [ "$AUDIO_BACKEND" = "pipewire" ]; then SINK_NAME="unknown" fi log_info "Routing to sink: id=$SINK_ID name='$SINK_NAME' choice=$SINK_CHOICE" -else +elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then SINK_NAME="$(pa_sink_name "$SINK_ID")" if [ -z "$SINK_NAME" ]; then SINK_NAME="$SINK_ID" fi pa_set_default_sink "$SINK_ID" >/dev/null 2>&1 || true log_info "Routing to sink: name='$SINK_NAME' choice=$SINK_CHOICE" +else + SINK_NAME="$SINK_ID" + log_info "Routing to sink: device='$SINK_NAME' choice=$SINK_CHOICE" fi # Decide minimum ok seconds if timeout>0 @@ -496,7 +715,7 @@ fi min_ok=0 if [ "$dur_s" -gt 0 ] 2>/dev/null; then - min_ok=$(expr $dur_s - 1) + min_ok=$($dur_s - 1) if [ "$min_ok" -lt 1 ]; then min_ok=1 fi @@ -515,62 +734,58 @@ suite_rc=0 if [ "$USE_CLIP_DISCOVERY" = "true" ]; then # ========== NEW: Clip Discovery Mode ========== log_info "Using clip discovery mode" - + # Discover and filter clips clips_dir="${AUDIO_CLIPS_BASE_DIR:-AudioClips}" - + # Get list of clips to test if [ -n "$CLIP_NAMES" ] || [ -n "$CLIP_FILTER" ]; then - # Use discover_and_filter_clips helper (logs go to stderr automatically) CLIPS_TO_TEST="$(discover_and_filter_clips "$CLIP_NAMES" "$CLIP_FILTER")" || { - # Error messages already printed to stderr, just skip log_skip "$TESTNAME SKIP - Invalid clip/config name(s) provided" echo "$TESTNAME SKIP" > "$RES_FILE" exit 0 } else - # Discover all clips (logs go to stderr automatically) CLIPS_TO_TEST="$(discover_audio_clips)" || { - # Error messages already printed to stderr, just skip log_skip "$TESTNAME SKIP - No audio clips found in $clips_dir" echo "$TESTNAME SKIP" > "$RES_FILE" exit 0 } fi - + # Count clips clip_count=0 for clip_file in $CLIPS_TO_TEST; do - clip_count=$(expr $clip_count + 1) + clip_count=$((clip_count + 1)) done - + log_info "Discovered $clip_count clips to test" - + # Test each clip for clip_file in $CLIPS_TO_TEST; do - # Generate test case name from clip filename - case_name="$(generate_clip_testcase_name "$clip_file")" || { - log_warn "Skipping clip with unparseable name: $clip_file" - continue - } - + case_name="$(generate_clip_testcase_name "$clip_file" 2>/dev/null || true)" + if [ -z "$case_name" ]; then + case_name="$(printf '%s' "$clip_file" | sed 's/\.[Ww][Aa][Vv]$//' | tr ' /' '__')" + log_warn "Clip name not in expected format; using generic testcase name: $case_name" + fi + # Resolve full path clip_path="$clips_dir/$clip_file" - + # Validate clip file if ! validate_clip_file "$clip_path"; then log_skip "[$case_name] SKIP: Invalid clip file: $clip_path" echo "$case_name SKIP (invalid file)" >> "$LOGDIR/summary.txt" - skip=$(expr $skip + 1) + skip=$((skip + 1)) continue fi - + # Extract clip duration for accurate timeout handling clip_duration="$(extract_clip_duration "$clip_file" 2>/dev/null || echo 0)" if [ "$clip_duration" -gt 0 ] 2>/dev/null; then # Use clip duration for timeout calculations clip_dur_s="$clip_duration" - clip_min_ok=$(expr $clip_duration - 1) + clip_min_ok=$((clip_duration - 1)) if [ "$clip_min_ok" -lt 1 ]; then clip_min_ok=1 fi @@ -580,30 +795,30 @@ if [ "$USE_CLIP_DISCOVERY" = "true" ]; then clip_dur_s="$dur_s" clip_min_ok="$min_ok" fi - - total=$(expr $total + 1) + + total=$((total + 1)) logf="$LOGDIR/${case_name}.log" : > "$logf" export AUDIO_LOGCTX="$logf" - + CLIP_BYTES="$(file_size_bytes "$clip_path" 2>/dev/null || echo 0)" log_info "[$case_name] Using clip: $clip_file (${CLIP_BYTES} bytes)" - + i=1 ok_runs=0 last_elapsed=0 - + while [ "$i" -le "$LOOPS" ]; do iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - + if [ "$AUDIO_BACKEND" = "pipewire" ]; then loop_hdr="sink=$SINK_CHOICE($SINK_ID)" else loop_hdr="sink=$SINK_CHOICE($SINK_NAME)" fi - + log_info "[$case_name] loop $i/$LOOPS start=$iso clip=$clip_file backend=$AUDIO_BACKEND $loop_hdr" - + # Determine effective timeout: use clip duration when TIMEOUT is disabled effective_timeout="$TIMEOUT" if [ "$TIMEOUT" = "0" ] || [ "$TIMEOUT" = "" ]; then @@ -612,43 +827,47 @@ if [ "$USE_CLIP_DISCOVERY" = "true" ]; then log_info "[$case_name] Using clip duration as timeout: ${effective_timeout}s" fi fi - + start_s="$(date +%s 2>/dev/null || echo 0)" - + if [ "$AUDIO_BACKEND" = "pipewire" ]; then log_info "[$case_name] exec: pw-play -v \"$clip_path\"" audio_exec_with_timeout "$effective_timeout" pw-play -v "$clip_path" >>"$logf" 2>&1 rc=$? - else + elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then log_info "[$case_name] exec: paplay --device=\"$SINK_NAME\" \"$clip_path\"" audio_exec_with_timeout "$effective_timeout" paplay --device="$SINK_NAME" "$clip_path" >>"$logf" 2>&1 rc=$? + else + log_info "[$case_name] exec: aplay -D \"$SINK_NAME\" \"$clip_path\"" + audio_exec_with_timeout "$effective_timeout" aplay -D "$SINK_NAME" "$clip_path" >>"$logf" 2>&1 + rc=$? fi - + end_s="$(date +%s 2>/dev/null || echo 0)" - last_elapsed=$(expr $end_s - $start_s) + last_elapsed=$((end_s - start_s)) if [ "$last_elapsed" -lt 0 ]; then last_elapsed=0 fi - + # Evidence collection pw_ev="$(audio_evidence_pw_streaming || echo 0)" pa_ev="$(audio_evidence_pa_streaming || echo 0)" - + # Minimal PulseAudio fallback if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 0 ]; then if [ "$rc" -eq 0 ] || { [ "$rc" -eq 124 ] && [ "$dur_s" -gt 0 ] 2>/dev/null && [ "$last_elapsed" -ge "$min_ok" ]; }; 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 + if [ "$AUDIO_BACKEND" = "pulseaudio" ] || [ "$AUDIO_BACKEND" = "alsa" ]; then pwlog_ev=0 fi - + # Fast teardown fallback if [ "$alsa_ev" -eq 0 ]; then if [ "$AUDIO_BACKEND" = "pipewire" ] && [ "$pw_ev" -eq 1 ]; then @@ -658,188 +877,192 @@ if [ "$USE_CLIP_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 pw_log=$pwlog_ev" - + # Determine result (use clip-specific timeout thresholds) if [ "$rc" -eq 0 ]; then log_pass "[$case_name] loop $i OK (rc=0, ${last_elapsed}s)" - ok_runs=$(expr $ok_runs + 1) + ok_runs=$((ok_runs + 1)) elif [ "$rc" -eq 124 ] && [ "$clip_dur_s" -gt 0 ] 2>/dev/null && [ "$last_elapsed" -ge "$clip_min_ok" ]; then log_warn "[$case_name] TIMEOUT ($TIMEOUT) - PASS (ran ~${last_elapsed}s, expected ${clip_duration}s)" - ok_runs=$(expr $ok_runs + 1) + ok_runs=$((ok_runs + 1)) elif [ "$rc" -ne 0 ] && { [ "$pw_ev" -eq 1 ] || [ "$pa_ev" -eq 1 ] || [ "$alsa_ev" -eq 1 ] || [ "$asoc_ev" -eq 1 ]; }; then log_warn "[$case_name] nonzero rc=$rc but evidence indicates playback - PASS" - ok_runs=$(expr $ok_runs + 1) + ok_runs=$((ok_runs + 1)) else log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s) - see $logf" fi - - i=$(expr $i + 1) + + i=$((i + 1)) done - + # Aggregate result for this clip if [ "$ok_runs" -ge 1 ]; then - pass=$(expr $pass + 1) + pass=$((pass + 1)) echo "$case_name PASS" >> "$LOGDIR/summary.txt" else - fail=$(expr $fail + 1) + fail=$((fail + 1)) echo "$case_name FAIL" >> "$LOGDIR/summary.txt" suite_rc=1 fi done - - # Collect evidence once at end (not per clip) - if [ "$DMESG_SCAN" -eq 1 ]; then - scan_audio_dmesg "$LOGDIR" - dump_mixers "$LOGDIR/mixer_dump.txt" - fi else # ========== LEGACY: Matrix Mode ========== - + for fmt in $FORMATS; do for dur in $DURATIONS; do - clip="$(resolve_clip "$fmt" "$dur")" - case_name="play_${fmt}_${dur}" - total=$(expr $total + 1) - logf="$LOGDIR/${case_name}.log" - : > "$logf" - export AUDIO_LOGCTX="$logf" - - if [ -z "$clip" ]; then - log_warn "[$case_name] No clip mapping for format=$fmt duration=$dur" - echo "$case_name SKIP (no clip mapping)" >> "$LOGDIR/summary.txt" - skip=$(expr $skip + 1) - continue - fi + clip="$(resolve_clip "$fmt" "$dur")" + case_name="play_${fmt}_${dur}" + total=$((total + 1)) + logf="$LOGDIR/${case_name}.log" + : > "$logf" + export AUDIO_LOGCTX="$logf" + + if [ -z "$clip" ]; then + log_warn "[$case_name] No clip mapping for format=$fmt duration=$dur" + echo "$case_name SKIP (no clip mapping)" >> "$LOGDIR/summary.txt" + skip=$((skip + 1)) + continue + fi - # Check if clip is available (should have been downloaded at top level if needed) - if [ "${EXTRACT_AUDIO_ASSETS}" = "true" ]; then - if [ -s "$clip" ]; then - CLIP_BYTES="$(file_size_bytes "$clip" 2>/dev/null || echo 0)" - log_info "[$case_name] Using clip: $clip (${CLIP_BYTES} bytes)" - else - # Clip missing or empty - this shouldn't happen if top-level download succeeded - log_skip "[$case_name] SKIP: Clip not available: $clip" - if [ "${ENABLE_NETWORK_DOWNLOAD}" = "true" ]; then - log_info "[$case_name] Hint: Clip should have been downloaded at test startup" + # Check if clip is available (should have been downloaded at top level if needed) + if [ "${EXTRACT_AUDIO_ASSETS}" = "true" ]; then + if [ -s "$clip" ]; then + CLIP_BYTES="$(file_size_bytes "$clip" 2>/dev/null || echo 0)" + log_info "[$case_name] Using clip: $clip (${CLIP_BYTES} bytes)" else - log_info "[$case_name] Hint: Run with --enable-network-download to download clips" + # Clip missing or empty - this shouldn't happen if top-level download succeeded + log_skip "[$case_name] SKIP: Clip not available: $clip" + if [ "${ENABLE_NETWORK_DOWNLOAD}" = "true" ]; then + log_info "[$case_name] Hint: Clip should have been downloaded at test startup" + else + log_info "[$case_name] Hint: Run with --enable-network-download to download clips" + fi + echo "$case_name SKIP (clip unavailable)" >> "$LOGDIR/summary.txt" + skip=$((skip + 1)) + continue fi - echo "$case_name SKIP (clip unavailable)" >> "$LOGDIR/summary.txt" - skip=$(expr $skip + 1) - continue fi - fi - i=1 - ok_runs=0 - last_elapsed=0 + i=1 + ok_runs=0 + last_elapsed=0 - while [ "$i" -le "$LOOPS" ]; do - iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + while [ "$i" -le "$LOOPS" ]; do + iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - if [ "$AUDIO_BACKEND" = "pipewire" ]; then - loop_hdr="sink=$SINK_CHOICE($SINK_ID)" - else - loop_hdr="sink=$SINK_CHOICE($SINK_NAME)" - fi + if [ "$AUDIO_BACKEND" = "pipewire" ]; then + loop_hdr="sink=$SINK_CHOICE($SINK_ID)" + else + loop_hdr="sink=$SINK_CHOICE($SINK_NAME)" + fi - log_info "[$case_name] loop $i/$LOOPS start=$iso clip=$clip backend=$AUDIO_BACKEND $loop_hdr" + log_info "[$case_name] loop $i/$LOOPS start=$iso clip=$clip backend=$AUDIO_BACKEND $loop_hdr" - start_s="$(date +%s 2>/dev/null || echo 0)" + start_s="$(date +%s 2>/dev/null || echo 0)" - if [ "$AUDIO_BACKEND" = "pipewire" ]; then - log_info "[$case_name] exec: pw-play -v \"$clip\"" - audio_exec_with_timeout "$TIMEOUT" pw-play -v "$clip" >>"$logf" 2>&1 - rc=$? - else - log_info "[$case_name] exec: paplay --device=\"$SINK_NAME\" \"$clip\"" - audio_exec_with_timeout "$TIMEOUT" paplay --device="$SINK_NAME" "$clip" >>"$logf" 2>&1 - rc=$? - fi + if [ "$AUDIO_BACKEND" = "pipewire" ]; then + log_info "[$case_name] exec: pw-play -v \"$clip\"" + audio_exec_with_timeout "$TIMEOUT" pw-play -v "$clip" >>"$logf" 2>&1 + rc=$? + elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then + log_info "[$case_name] exec: paplay --device=\"$SINK_NAME\" \"$clip\"" + audio_exec_with_timeout "$TIMEOUT" paplay --device="$SINK_NAME" "$clip" >>"$logf" 2>&1 + rc=$? + else + log_info "[$case_name] exec: aplay -D \"$SINK_NAME\" \"$clip\"" + audio_exec_with_timeout "$TIMEOUT" aplay -D "$SINK_NAME" "$clip" >>"$logf" 2>&1 + rc=$? + fi - end_s="$(date +%s 2>/dev/null || echo 0)" - last_elapsed=$(expr $end_s - $start_s) - if [ "$last_elapsed" -lt 0 ]; then - last_elapsed=0 - 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 - # Evidence - pw_ev="$(audio_evidence_pw_streaming || echo 0)" - pa_ev="$(audio_evidence_pa_streaming || echo 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 ] || { [ "$rc" -eq 124 ] && [ "$dur_s" -gt 0 ] 2>/dev/null && [ "$last_elapsed" -ge "$min_ok" ]; }; then - pa_ev=1 + # 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 ] || { [ "$rc" -eq 124 ] && [ "$dur_s" -gt 0 ] 2>/dev/null && [ "$last_elapsed" -ge "$min_ok" ]; }; then + pa_ev=1 + fi 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 + 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" ] || [ "$AUDIO_BACKEND" = "alsa" ]; then + 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 + # 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 + fi + if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 1 ]; then + alsa_ev=1 + fi fi - if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 1 ]; then - alsa_ev=1 + + if [ "$asoc_ev" -eq 0 ] && [ "$alsa_ev" -eq 1 ]; then + asoc_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 pw_log=$pwlog_ev" + + if [ "$rc" -eq 0 ]; then + log_pass "[$case_name] loop $i OK (rc=0, ${last_elapsed}s)" + ok_runs=$((ok_runs + 1)) + elif [ "$rc" -eq 124 ] && [ "$dur_s" -gt 0 ] 2>/dev/null && [ "$last_elapsed" -ge "$min_ok" ]; then + log_warn "[$case_name] TIMEOUT ($TIMEOUT) - PASS (ran ~${last_elapsed}s)" + ok_runs=$((ok_runs + 1)) + elif [ "$rc" -ne 0 ] && { [ "$pw_ev" -eq 1 ] || [ "$pa_ev" -eq 1 ] || [ "$alsa_ev" -eq 1 ] || [ "$asoc_ev" -eq 1 ]; }; then + log_warn "[$case_name] nonzero rc=$rc but evidence indicates playback - PASS" + ok_runs=$((ok_runs + 1)) + else + log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s) - see $logf" + fi - log_info "[$case_name] evidence: pw_streaming=$pw_ev pa_streaming=$pa_ev alsa_running=$alsa_ev asoc_path_on=$asoc_ev pw_log=$pwlog_ev" + i=$((i + 1)) + done - if [ "$rc" -eq 0 ]; then - log_pass "[$case_name] loop $i OK (rc=0, ${last_elapsed}s)" - ok_runs=$(expr $ok_runs + 1) - elif [ "$rc" -eq 124 ] && [ "$dur_s" -gt 0 ] 2>/dev/null && [ "$last_elapsed" -ge "$min_ok" ]; then - log_warn "[$case_name] TIMEOUT ($TIMEOUT) - PASS (ran ~${last_elapsed}s)" - ok_runs=$(expr $ok_runs + 1) - elif [ "$rc" -ne 0 ] && { [ "$pw_ev" -eq 1 ] || [ "$pa_ev" -eq 1 ] || [ "$alsa_ev" -eq 1 ] || [ "$asoc_ev" -eq 1 ]; }; then - log_warn "[$case_name] nonzero rc=$rc but evidence indicates playback - PASS" - ok_runs=$(expr $ok_runs + 1) + if [ "$ok_runs" -ge 1 ]; then + pass=$((pass + 1)) + echo "$case_name PASS" >> "$LOGDIR/summary.txt" else - log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s) - see $logf" + fail=$((fail + 1)) + echo "$case_name FAIL" >> "$LOGDIR/summary.txt" + suite_rc=1 fi - - i=$(expr $i + 1) - done - - if [ "$ok_runs" -ge 1 ]; then - pass=$(expr $pass + 1) - echo "$case_name PASS" >> "$LOGDIR/summary.txt" - else - fail=$(expr $fail + 1) - echo "$case_name FAIL" >> "$LOGDIR/summary.txt" - suite_rc=1 - fi done done - - # Collect evidence once at end (not per test case) - if [ "$DMESG_SCAN" -eq 1 ]; then - scan_audio_dmesg "$LOGDIR" - dump_mixers "$LOGDIR/mixer_dump.txt" - fi +fi + +# Collect evidence once at end (not per test case) +if [ "$DMESG_SCAN" -eq 1 ]; then + scan_audio_dmesg "$LOGDIR" + dump_mixers "$LOGDIR/mixer_dump.txt" fi log_info "Summary: total=$total pass=$pass fail=$fail skip=$skip" +if [ "$total" -eq 0 ] && [ "$pass" -eq 0 ] && [ "$fail" -eq 0 ]; then + log_skip "$TESTNAME SKIP - no runnable playback testcases" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 +fi + # --- Proper exit codes: PASS=0, FAIL=1, SKIP-only=0 --- if [ "$pass" -eq 0 ] && [ "$fail" -eq 0 ] && [ "$skip" -gt 0 ]; then log_skip "$TESTNAME SKIP" diff --git a/Runner/suites/Multimedia/Audio/AudioRecord/AudioRecord_minimal.yaml b/Runner/suites/Multimedia/Audio/AudioRecord/AudioRecord_minimal.yaml new file mode 100644 index 00000000..ad90058c --- /dev/null +++ b/Runner/suites/Multimedia/Audio/AudioRecord/AudioRecord_minimal.yaml @@ -0,0 +1,24 @@ +metadata: + name: AudioRecord minimal build + format: "Lava-Test Test Definition 1.0" + description: "AudioRecord on minimal build using direct ALSA capture probe/path" + maintainer: + - srikanth kumar + os: + - linux + scope: + - functional + +params: + BACKEND: "alsa" + SOURCE: "mic" + LOOPS: "1" + TIMEOUT: "0" + RECORD_SECONDS: "30s" + +run: + steps: + - REPO_PATH=$PWD + - cd Runner/suites/Multimedia/Audio/AudioRecord/ + - ./run.sh --backend "${BACKEND}" --source "${SOURCE}" --loops "${LOOPS}" --timeout "${TIMEOUT}" --record-seconds "${RECORD_SECONDS}" --audio-bootstrap false || true + - ./send-to-lava.sh AudioRecord.res diff --git a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh index 27f37f49..da454088 100755 --- a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh +++ b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh @@ -7,6 +7,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # ---- Source init_env & tools ---- INIT_ENV="" SEARCH="$SCRIPT_DIR" + while [ "$SEARCH" != "/" ]; do if [ -f "$SEARCH/init_env" ]; then INIT_ENV="$SEARCH/init_env" @@ -20,9 +21,11 @@ if [ -z "$INIT_ENV" ]; then exit 1 fi -# shellcheck disable=SC1090 -if [ -z "$__INIT_ENV_LOADED" ]; then +# Only source once (idempotent) +if [ -z "${__INIT_ENV_LOADED:-}" ]; then + # shellcheck disable=SC1090 . "$INIT_ENV" + __INIT_ENV_LOADED=1 fi # shellcheck disable=SC1091 @@ -30,6 +33,11 @@ fi # shellcheck disable=SC1091 . "$TOOLS/audio_common.sh" +SYSTEMD_AVAILABLE=0 +if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then + SYSTEMD_AVAILABLE=1 +fi + TESTNAME="AudioRecord" RES_SUFFIX="" # Optional suffix for unique result files (e.g., "Config1") # RES_FILE will be set after parsing command-line arguments @@ -47,19 +55,6 @@ for arg in "$@"; do prev_arg="$arg" done -# Early failure handling with suffix support -if ! setup_overlay_audio_environment; then - log_fail "Overlay audio environment setup failed" - if [ -n "$RES_SUFFIX" ]; then - echo "$TESTNAME FAIL" > "$SCRIPT_DIR/${TESTNAME}_${RES_SUFFIX}.res" - else - echo "$TESTNAME FAIL" > "$SCRIPT_DIR/${TESTNAME}.res" - fi - exit 0 -fi - -# LOGDIR will be set after parsing command-line arguments (to apply RES_SUFFIX correctly) - # ---------------- Defaults / CLI ---------------- AUDIO_BACKEND="" SRC_CHOICE="${SRC_CHOICE:-mic}" # mic|null @@ -72,6 +67,15 @@ DMESG_SCAN="${DMESG_SCAN:-1}" VERBOSE=0 JUNIT_OUT="" +# Minimal ramdisk audio bootstrap options +AUDIO_BOOTSTRAP_MODE="${AUDIO_BOOTSTRAP_MODE:-auto}" # auto|true|false +AUDIO_RUNTIME_DIR="${AUDIO_RUNTIME_DIR:-}" # optional override +MINIMAL_RAMDISK_MODE=0 +AUDIO_STARTED_PIDS="" +AUDIO_CREATED_RUNTIME_DIR=0 +AUDIO_SYSTEMD_MANAGED=0 +export AUDIO_BOOTSTRAP_MODE AUDIO_RUNTIME_DIR MINIMAL_RAMDISK_MODE AUDIO_STARTED_PIDS AUDIO_CREATED_RUNTIME_DIR AUDIO_SYSTEMD_MANAGED + # 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") @@ -80,15 +84,20 @@ USE_CONFIG_DISCOVERY="${USE_CONFIG_DISCOVERY:-auto}" # auto|true|false usage() { cat < "$LOGDIR/summary.txt" +# Overlay setup must happen after CLI parsing so --help can exit cleanly. +# Make it best-effort; later backend recovery logic will handle retry/bootstrap. +if [ "$SYSTEMD_AVAILABLE" -eq 1 ]; then + if ! setup_overlay_audio_environment; then + log_warn "Overlay audio environment setup failed; continuing with backend recovery flow" + fi +else + log_info "systemd not available; skipping overlay audio environment setup (minimal ramdisk mode)" +fi + +# Minimal ramdisk detection and cleanup trap (kills any manually bootstrapped daemons) +if [ "$SYSTEMD_AVAILABLE" -eq 0 ]; then + MINIMAL_RAMDISK_MODE=1 + export MINIMAL_RAMDISK_MODE + log_info "Detected minimal ramdisk environment (systemd unavailable)" +else + log_info "Detected standard userspace environment (systemd available)" +fi + +trap 'audio_cleanup_started_daemons' EXIT HUP INT TERM + # Ensure we run from the testcase dir test_path="$(find_test_case_by_name "$TESTNAME" 2>/dev/null || echo "$SCRIPT_DIR")" if ! cd "$test_path"; then @@ -249,7 +289,6 @@ if [ "$USE_CONFIG_DISCOVERY" = "auto" ]; then log_info "Auto-detected config discovery mode (testing all 10 record configs)" fi - # Validate CLI option conflicts if [ -n "$CONFIG_NAMES" ] && [ -n "$CONFIG_FILTER" ]; then log_warn "Both --config-name and --config-filter specified" @@ -257,68 +296,180 @@ if [ -n "$CONFIG_NAMES" ] && [ -n "$CONFIG_FILTER" ]; then CONFIG_FILTER="" fi -log_info "Args: backend=${AUDIO_BACKEND:-auto} source=$SRC_CHOICE loops=$LOOPS durations='$DURATIONS' record_seconds=$RECORD_SECONDS timeout=$TIMEOUT strict=$STRICT dmesg=$DMESG_SCAN" +log_info "Args: backend=${AUDIO_BACKEND:-auto} source=$SRC_CHOICE loops=$LOOPS durations='$DURATIONS' record_seconds=$RECORD_SECONDS timeout=$TIMEOUT strict=$STRICT dmesg=$DMESG_SCAN bootstrap=$AUDIO_BOOTSTRAP_MODE runtime_dir=${AUDIO_RUNTIME_DIR:-auto}" -# Resolve backend +# Resolve backend (allow minimal-build ALSA capture fallback) if [ -z "$AUDIO_BACKEND" ]; then - AUDIO_BACKEND="$(detect_audio_backend)" + AUDIO_BACKEND="$(detect_audio_backend 2>/dev/null || echo "")" +fi + +AUDIO_SYSTEMD_MANAGED=0 +if [ -n "$AUDIO_BACKEND" ]; then + if audio_backend_is_systemd_managed "$AUDIO_BACKEND"; then + AUDIO_SYSTEMD_MANAGED=1 + fi fi -BACKENDS_TO_TRY="$(build_backend_chain)" -# Use it for visibility and to satisfy shellcheck usage -log_info "Backend fallback chain: $BACKENDS_TO_TRY" +export AUDIO_SYSTEMD_MANAGED + +BACKENDS_TO_TRY="$(build_backend_chain 2>/dev/null || echo "")" +log_info "Backend fallback chain: ${BACKENDS_TO_TRY:-unknown}" + +ALSA_CAPTURE_PROBED=0 +AUDIO_ALSA_CAPTURE_DEVICE="" +AUDIO_ALSA_CAPTURE_FORMAT="" +AUDIO_ALSA_CAPTURE_RATE="" +AUDIO_ALSA_CAPTURE_CHANNELS="" +AUDIO_ALSA_CAPTURE_REASON="" + if [ -z "$AUDIO_BACKEND" ]; then - log_skip "$TESTNAME SKIP - no audio backend running" - echo "$TESTNAME SKIP" > "$RES_FILE" - exit 0 + if audio_bootstrap_backend_if_needed; then + AUDIO_BACKEND="$(detect_audio_backend 2>/dev/null || echo "")" + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + else + if audio_probe_alsa_capture_profile; then + ALSA_CAPTURE_PROBED=1 + AUDIO_BACKEND="alsa" + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + log_warn "$TESTNAME: no managed audio backend running - using direct ALSA capture path" + else + log_skip "$TESTNAME SKIP - no audio backend running and ALSA capture probe failed: ${AUDIO_ALSA_CAPTURE_REASON:-capture path unavailable}" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + fi + fi fi + log_info "Using backend: $AUDIO_BACKEND" -if ! check_audio_daemon "$AUDIO_BACKEND"; then - log_warn "$TESTNAME: backend not available ($AUDIO_BACKEND) - attempting restart+retry once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true +backend_ok=0 +if [ "$AUDIO_BACKEND" = "alsa" ]; then + if [ "$ALSA_CAPTURE_PROBED" -eq 1 ]; then + backend_ok=1 + else + if audio_probe_alsa_capture_profile; then + ALSA_CAPTURE_PROBED=1 + backend_ok=1 + fi + fi +else + if audio_backend_ready "$AUDIO_BACKEND"; then + backend_ok=1 + else + if check_audio_daemon "$AUDIO_BACKEND"; then + backend_ok=1 + fi + fi +fi - if ! check_audio_daemon "$AUDIO_BACKEND"; then - log_skip "$TESTNAME SKIP - backend not available: $AUDIO_BACKEND" - echo "$TESTNAME SKIP" > "$RES_FILE" - exit 0 +if [ "$backend_ok" -ne 1 ] && [ "$AUDIO_BACKEND" != "alsa" ]; then + if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then + log_warn "$TESTNAME: backend not available ($AUDIO_BACKEND) - attempting restart+retry once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + if audio_backend_ready "$AUDIO_BACKEND"; then + backend_ok=1 + else + if check_audio_daemon "$AUDIO_BACKEND"; then + backend_ok=1 + fi + fi + else + log_warn "$TESTNAME: backend not available ($AUDIO_BACKEND) - attempting manual bootstrap" + if audio_bootstrap_backend_if_needed; then + backend_ok=1 + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + AUDIO_BACKEND="$(detect_audio_backend 2>/dev/null || echo "$AUDIO_BACKEND")" + fi fi fi -# 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 +if [ "$backend_ok" -ne 1 ] && [ "$AUDIO_BACKEND" != "alsa" ]; then + if audio_probe_alsa_capture_profile; then + ALSA_CAPTURE_PROBED=1 + AUDIO_BACKEND="alsa" + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + backend_ok=1 + log_warn "$TESTNAME: falling back to ALSA direct capture path" fi -else - if ! check_dependencies pactl parecord; then - log_skip "$TESTNAME SKIP - missing PulseAudio utils" - echo "$TESTNAME SKIP" > "$RES_FILE" - exit 0 +fi + +if [ "$backend_ok" -ne 1 ]; then + if [ "$AUDIO_BACKEND" = "alsa" ] || [ "$ALSA_CAPTURE_PROBED" -eq 1 ]; then + log_skip "$TESTNAME SKIP - ALSA capture path unavailable: ${AUDIO_ALSA_CAPTURE_REASON:-capture device could not be opened}" + else + log_skip "$TESTNAME SKIP - backend not available: $AUDIO_BACKEND" fi + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 fi +# Dependencies per backend (include ALSA) +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 + ;; + *) + log_skip "$TESTNAME SKIP - unsupported backend: $AUDIO_BACKEND" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + ;; +esac + # ----- Control-plane sanity (prevents wpctl/pactl hangs during source selection) ----- if [ "$AUDIO_BACKEND" = "pipewire" ]; then if ! audio_pw_ctl_ok 2>/dev/null; then - log_warn "$TESTNAME: wpctl not responsive - attempting restart+retry once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true + if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then + log_warn "$TESTNAME: wpctl not responsive - attempting restart+retry once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + else + log_warn "$TESTNAME: wpctl not responsive - attempting manual bootstrap" + audio_bootstrap_backend_if_needed >/dev/null 2>&1 || true + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + fi if ! audio_pw_ctl_ok 2>/dev/null; then - log_skip "$TESTNAME SKIP - PipeWire control-plane not responsive after restart" + log_skip "$TESTNAME SKIP - PipeWire control-plane not responsive" echo "$TESTNAME SKIP" > "$RES_FILE" exit 0 fi fi elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then if ! audio_pa_ctl_ok 2>/dev/null; then - log_warn "$TESTNAME: pactl not responsive - attempting restart+retry once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true + if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then + log_warn "$TESTNAME: pactl not responsive - attempting restart+retry once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + else + log_warn "$TESTNAME: pactl not responsive - attempting manual bootstrap" + audio_bootstrap_backend_if_needed >/dev/null 2>&1 || true + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + fi if ! audio_pa_ctl_ok 2>/dev/null; then - log_skip "$TESTNAME SKIP - PulseAudio control-plane not responsive after restart" + log_skip "$TESTNAME SKIP - PulseAudio control-plane not responsive" echo "$TESTNAME SKIP" > "$RES_FILE" exit 0 fi @@ -340,10 +491,20 @@ case "$AUDIO_BACKEND:$SRC_CHOICE" in pulseaudio:*) SRC_ID="$(pa_default_mic)" ;; + alsa:null) + SRC_ID="" + ;; + alsa:*) + if [ "$ALSA_CAPTURE_PROBED" -eq 1 ] && [ -n "$AUDIO_ALSA_CAPTURE_DEVICE" ]; then + SRC_ID="$AUDIO_ALSA_CAPTURE_DEVICE" + else + SRC_ID="$(alsa_pick_capture)" + fi + ;; esac # ---- 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. +# Stay on PipeWire even if SRC_ID is empty; pw-record 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 @@ -351,7 +512,8 @@ if [ -z "$SRC_ID" ] && [ "$SRC_CHOICE" = "mic" ] && [ "$AUDIO_BACKEND" != "pipew pipewire) cand="$(pw_default_mic)" if [ -n "$cand" ]; then - AUDIO_BACKEND="pipewire"; SRC_ID="$cand" + AUDIO_BACKEND="pipewire" + SRC_ID="$cand" log_info "Falling back to backend: pipewire (source id=$SRC_ID)" break fi @@ -359,7 +521,8 @@ if [ -z "$SRC_ID" ] && [ "$SRC_CHOICE" = "mic" ] && [ "$AUDIO_BACKEND" != "pipew pulseaudio) cand="$(pa_default_mic)" if [ -n "$cand" ]; then - AUDIO_BACKEND="pulseaudio"; SRC_ID="$cand" + AUDIO_BACKEND="pulseaudio" + SRC_ID="$cand" log_info "Falling back to backend: pulseaudio (source=$SRC_ID)" break fi @@ -367,7 +530,8 @@ if [ -z "$SRC_ID" ] && [ "$SRC_CHOICE" = "mic" ] && [ "$AUDIO_BACKEND" != "pipew alsa) cand="$(alsa_pick_capture)" if [ -n "$cand" ]; then - AUDIO_BACKEND="alsa"; SRC_ID="$cand" + AUDIO_BACKEND="alsa" + SRC_ID="$cand" log_info "Falling back to backend: alsa (device=$SRC_ID)" break fi @@ -378,7 +542,7 @@ 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)" + log_skip "$TESTNAME SKIP - requested source '$SRC_CHOICE' not available on any backend (${BACKENDS_TO_TRY:-unknown})" echo "$TESTNAME SKIP" > "$RES_FILE" exit 0 fi @@ -405,10 +569,7 @@ if [ "$AUDIO_BACKEND" = "alsa" ]; then 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]*,[0-9][0-9]*$'; then + if [ -n "$cand" ] && printf '%s\n' "$cand" | grep -Eq '^hw:[0-9][0-9]*,[0-9][0-9]*$'; then SRC_ID="$cand" log_info "ALSA auto-pick: using $SRC_ID" else @@ -436,7 +597,7 @@ elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then 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" + SRC_LABEL="${SRC_ID:-default}" log_info "Routing to source: name='$SRC_LABEL' choice=$SRC_CHOICE" fi @@ -445,18 +606,24 @@ 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 ;; + 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 ;; + 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 ;; + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + fi + ;; esac # Watchdog info @@ -524,81 +691,79 @@ 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) CONFIGS_TO_TEST="$(discover_and_filter_record_configs "$CONFIG_NAMES" "$CONFIG_FILTER")" || { - # Error messages already printed to stderr, just skip log_skip "$TESTNAME SKIP - Invalid config name(s) provided" echo "$TESTNAME SKIP" > "$RES_FILE" exit 0 } else - # Discover all configs (logs go to stderr automatically) CONFIGS_TO_TEST="$(discover_record_configs)" || { - # Error messages already printed to stderr, just skip log_skip "$TESTNAME SKIP - No record configs found" echo "$TESTNAME SKIP" > "$RES_FILE" 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 case_name="$(generate_record_testcase_name "$config")" || { log_warn "Skipping config with invalid name: $config" + echo "$config SKIP (invalid config name)" >> "$LOGDIR/summary.txt" + skip=$((skip + 1)) continue } - - # Get recording parameters + params="$(get_record_config_params "$config")" || { log_warn "Skipping config with invalid parameters: $config" + echo "$config SKIP (invalid config parameters)" >> "$LOGDIR/summary.txt" + skip=$((skip + 1)) 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 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 @@ -609,33 +774,40 @@ 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 pw-record failed AND PipeWire control-plane is broken, restart+retry once + # If pw-record failed AND PipeWire control-plane is broken, restart/bootstrap and retry once if [ "$rc" -ne 0 ] && ! audio_pw_ctl_ok 2>/dev/null; then - log_warn "[$case_name] pw-record rc=$rc and wpctl not responsive - restarting and retrying once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true - out="$LOGDIR/${case_name}.wav" + if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then + log_warn "[$case_name] pw-record rc=$rc and wpctl not responsive - restarting and retrying once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + else + log_warn "[$case_name] pw-record rc=$rc and wpctl not responsive - attempting bootstrap and retrying once" + audio_bootstrap_backend_if_needed >/dev/null 2>&1 || true + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + fi + + out="$LOGDIR/${case_name}.wav" : > "$out" - log_info "[$case_name] retry after restart: pw-record -v --rate=$rate --channels=$channels \"$out\"" + log_info "[$case_name] retry: 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)" fi - + # If we already got real audio, accept and skip fallbacks if [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then if [ "$rc" -ne 0 ]; then @@ -643,11 +815,12 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then rc=0 fi else - # Only if output is tiny/empty do we try a virtual PCM (pipewire/pulse/default) + # Only if output is tiny/empty do we try a virtual PCM 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 + 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 $rate -c $channels -d $secs_int \"$out\"" audio_exec_with_timeout "$effective_timeout" \ @@ -656,9 +829,17 @@ 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 + retry_target=0 + if [ "$rc" -ne 0 ]; then + retry_target=1 + else + if [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then + retry_target=1 + fi + fi + if [ "$retry_target" -eq 1 ] && [ -n "$SRC_ID" ]; then : > "$out" log_info "[$case_name] exec: pw-record -v --rate=$rate --channels=$channels --target \"$SRC_ID\" \"$out\"" audio_exec_with_timeout "$effective_timeout" pw-record -v --rate="$rate" --channels="$channels" --target "$SRC_ID" "$out" >> "$logf" 2>&1 @@ -666,12 +847,13 @@ 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. + + # 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)" @@ -681,29 +863,51 @@ 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 + + retry_alsa=0 + if [ "$rc" -ne 0 ]; then + retry_alsa=1 + else + if [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then + retry_alsa=1 + fi + fi + + if [ "$retry_alsa" -eq 1 ]; 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\"" audio_exec_with_timeout "$effective_timeout" \ 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 - fmt=$(printf '%s\n' "$combo" | awk '{print $1}') - fallback_rate=$(printf '%s\n' "$combo" | awk '{print $2}') - fallback_ch=$(printf '%s\n' "$combo" | awk '{print $3}') + + retry_fallback=0 + if [ "$rc" -ne 0 ]; then + retry_fallback=1 + else + if [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then + retry_fallback=1 + fi + fi + + if [ "$retry_fallback" -eq 1 ]; then + for combo in \ + "${AUDIO_ALSA_CAPTURE_FORMAT:-S16_LE} ${AUDIO_ALSA_CAPTURE_RATE:-48000} ${AUDIO_ALSA_CAPTURE_CHANNELS:-2}" \ + "S16_LE 48000 2" \ + "S16_LE 44100 2" \ + "S16_LE 16000 1" + do + fmt="$(printf '%s\n' "$combo" | awk '{print $1}')" + fallback_rate="$(printf '%s\n' "$combo" | awk '{print $2}')" + fallback_ch="$(printf '%s\n' "$combo" | awk '{print $3}')" [ -z "$fmt" ] || [ -z "$fallback_rate" ] || [ -z "$fallback_ch" ] && continue + : > "$out" log_info "[$case_name] fallback: arecord -D \"$alt_dev\" -f $fmt -r $fallback_rate -c $fallback_ch -d $secs_int \"$out\"" audio_exec_with_timeout "$effective_timeout" \ @@ -716,11 +920,12 @@ 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 fi + else # PulseAudio log_info "[$case_name] exec: parecord --rate=$rate --channels=$channels --file-format=wav \"$out\"" @@ -728,47 +933,53 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then rc=$? bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" - # If parecord failed AND PulseAudio control-plane is broken, restart+retry once + # If parecord failed AND PulseAudio control-plane is broken, restart/bootstrap and retry once if [ "$rc" -ne 0 ] && ! audio_pa_ctl_ok 2>/dev/null; then - log_warn "[$case_name] parecord rc=$rc and pactl not responsive - restarting and retrying once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true - out="$LOGDIR/${case_name}.wav" + if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then + log_warn "[$case_name] parecord rc=$rc and pactl not responsive - restarting and retrying once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + else + log_warn "[$case_name] parecord rc=$rc and pactl not responsive - attempting bootstrap and retrying once" + audio_bootstrap_backend_if_needed >/dev/null 2>&1 || true + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + fi + + out="$LOGDIR/${case_name}.wav" : > "$out" - log_info "[$case_name] retry after restart: parecord --rate=$rate --channels=$channels --file-format=wav \"$out\"" + log_info "[$case_name] retry: parecord --rate=$rate --channels=$channels --file-format=wav \"$out\"" audio_exec_with_timeout "$effective_timeout" parecord --rate="$rate" --channels="$channels" --file-format=wav "$out" >> "$logf" 2>&1 rc=$? bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" 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 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) - + 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) + 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 @@ -777,53 +988,53 @@ 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 ;; esac done + 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" export AUDIO_LOGCTX="$logf" @@ -865,7 +1076,6 @@ else out="$LOGDIR/${case_name}.wav" : > "$out" - start_s="$(date +%s 2>/dev/null || echo 0)" if [ "$AUDIO_BACKEND" = "pipewire" ]; then @@ -874,14 +1084,22 @@ else rc=$? bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" - # If pw-record failed AND PipeWire control-plane is broken, restart+retry once + # If pw-record failed AND PipeWire control-plane is broken, restart/bootstrap and retry once if [ "$rc" -ne 0 ] && ! audio_pw_ctl_ok 2>/dev/null; then - log_warn "[$case_name] pw-record rc=$rc and wpctl not responsive - restarting and retrying once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true - out="$LOGDIR/${case_name}.wav" + if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then + log_warn "[$case_name] pw-record rc=$rc and wpctl not responsive - restarting and retrying once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + else + log_warn "[$case_name] pw-record rc=$rc and wpctl not responsive - attempting bootstrap and retrying once" + audio_bootstrap_backend_if_needed >/dev/null 2>&1 || true + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + fi + + out="$LOGDIR/${case_name}.wav" : > "$out" - log_info "[$case_name] retry after restart: pw-record -v \"$out\"" + log_info "[$case_name] retry: pw-record -v \"$out\"" audio_exec_with_timeout "$effective_timeout" pw-record -v "$out" >> "$logf" 2>&1 rc=$? bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" @@ -894,11 +1112,12 @@ else rc=0 fi else - # Only if output is tiny/empty do we try a virtual PCM (pipewire/pulse/default) + # Only if output is tiny/empty do we try a virtual PCM 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 + 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" \ @@ -909,7 +1128,15 @@ else 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 + retry_target=0 + if [ "$rc" -ne 0 ]; then + retry_target=1 + else + if [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then + retry_target=1 + fi + fi + if [ "$retry_target" -eq 1 ] && [ -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 @@ -918,11 +1145,12 @@ else fi fi - # (Optional safety) If nonzero rc but output is clearly valid, accept. + # 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)" @@ -933,17 +1161,33 @@ else rc=$? bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" - if [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then + retry_alsa=0 + if [ "$rc" -ne 0 ]; then + retry_alsa=1 + else + if [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then + retry_alsa=1 + fi + fi + + if [ "$retry_alsa" -eq 1 ]; 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}') + + for combo in \ + "${AUDIO_ALSA_CAPTURE_FORMAT:-S16_LE} ${AUDIO_ALSA_CAPTURE_RATE:-48000} ${AUDIO_ALSA_CAPTURE_CHANNELS:-2}" \ + "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" \ @@ -960,20 +1204,30 @@ else log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" rc=0 fi + else + # PulseAudio 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="$(file_size_bytes "$out" 2>/dev/null || echo 0)" - # If parecord failed AND PulseAudio control-plane is broken, restart+retry once + # If parecord failed AND PulseAudio control-plane is broken, restart/bootstrap and retry once if [ "$rc" -ne 0 ] && ! audio_pa_ctl_ok 2>/dev/null; then - log_warn "[$case_name] parecord rc=$rc and pactl not responsive - restarting and retrying once" - audio_restart_services_best_effort >/dev/null 2>&1 || true - audio_wait_audio_ready 20 >/dev/null 2>&1 || true - out="$LOGDIR/${case_name}.wav" + if [ "$SYSTEMD_AVAILABLE" -eq 1 ] && [ "${AUDIO_SYSTEMD_MANAGED:-0}" -eq 1 ]; then + log_warn "[$case_name] parecord rc=$rc and pactl not responsive - restarting and retrying once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + else + log_warn "[$case_name] parecord rc=$rc and pactl not responsive - attempting bootstrap and retrying once" + audio_bootstrap_backend_if_needed >/dev/null 2>&1 || true + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + fi + + out="$LOGDIR/${case_name}.wav" : > "$out" - log_info "[$case_name] retry after restart: parecord --file-format=wav \"$out\"" + log_info "[$case_name] retry: parecord --file-format=wav \"$out\"" audio_exec_with_timeout "$effective_timeout" parecord --file-format=wav "$out" >> "$logf" 2>&1 rc=$? bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" @@ -987,22 +1241,20 @@ 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 - pw_ev=$(audio_evidence_pw_streaming || echo 0) - pa_ev=$(audio_evidence_pa_streaming || echo 0) - + 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) + 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 @@ -1024,12 +1276,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 @@ -1042,15 +1294,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 ;; @@ -1080,6 +1332,12 @@ fi log_info "Summary: total=$total pass=$pass fail=$fail skip=$skip" # --- Proper exit codes: PASS=0, FAIL=1, SKIP-only=0 --- +if [ "$total" -eq 0 ] && [ "$pass" -eq 0 ] && [ "$fail" -eq 0 ]; then + log_skip "$TESTNAME SKIP - no runnable record testcases" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 +fi + if [ "$pass" -eq 0 ] && [ "$fail" -eq 0 ] && [ "$skip" -gt 0 ]; then log_skip "$TESTNAME SKIP" echo "$TESTNAME SKIP" > "$RES_FILE" diff --git a/Runner/utils/audio_common.sh b/Runner/utils/audio_common.sh index 154dcb54..a3b6e4a7 100755 --- a/Runner/utils/audio_common.sh +++ b/Runner/utils/audio_common.sh @@ -4,6 +4,12 @@ # Common audio helpers for PipeWire / PulseAudio runners. # Requires: functestlib.sh (log_* helpers, extract_tar_from_url, scan_dmesg_errors) +# Check whether a command exists in PATH. +# Used by bootstrap helpers before attempting backend startup. +have_cmd() { + command -v "$1" >/dev/null 2>&1 +} + # ---------- Backend detection & daemon checks ---------- detect_audio_backend() { if pgrep -x pipewire >/dev/null 2>&1 && command -v wpctl >/dev/null 2>&1; then @@ -93,26 +99,162 @@ audio_download_with_any() { return 1 fi } + +audio_has_runnable_discovery_clips() { + clips_dir="${AUDIO_CLIPS_BASE_DIR:-AudioClips}" + found_any=0 + + if [ ! -d "$clips_dir" ]; then + return 1 + fi + + for audio_clip_path in "$clips_dir"/*.wav; do + if [ ! -f "$audio_clip_path" ]; then + continue + fi + + found_any=1 + audio_clip_file="$(basename "$audio_clip_path")" + if generate_clip_testcase_name "$audio_clip_file" >/dev/null 2>&1; then + return 0 + fi + done + + if [ "$found_any" -eq 1 ]; then + return 1 + fi + + return 1 +} + # 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 + url="$1" + clips_dir="${AUDIO_CLIPS_BASE_DIR:-AudioClips}" + marker_file="${AUDIO_EXTRACT_MARKER:-$clips_dir/.audioclips_extracted}" + work_dir="${SCRIPT_DIR:-$(pwd)}" + ts="$(date +%s 2>/dev/null || echo 0)" + archive_path="$work_dir/AudioClips.$$.${ts}.tar.gz" + fetch_log="$work_dir/AudioClips_fetch.$$.${ts}.log" + fetch_attempts="${AUDIO_FETCH_RETRIES:-2}" + fetch_retry_delay="${AUDIO_FETCH_RETRY_DELAY:-3}" + fetch_attempt=1 + + if [ -z "$url" ]; then + log_error "audio_fetch_assets_from_url: URL is empty" + return 1 + fi + + if [ ! -d "$clips_dir" ]; then + if ! mkdir -p "$clips_dir"; then + log_error "Failed to create clips directory: $clips_dir" + 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 + fi + + if [ -f "$marker_file" ]; then + if audio_has_runnable_discovery_clips; then + log_pass "AudioClips.tar.gz has already been extracted (marker present, runnable clips available)." + log_info "Already extracted. Skipping download." + return 0 + fi + log_warn "Extraction marker present but runnable clips not found; continuing with download/re-extract path" + fi + + while [ "$fetch_attempt" -le "$fetch_attempts" ]; do + rm -f "$archive_path" >/dev/null 2>&1 || true + rm -f "$fetch_log" >/dev/null 2>&1 || true + + download_ok=0 + + if command -v curl >/dev/null 2>&1; then + log_info "exec: curl -fL --retry 3 --retry-delay 2 --connect-timeout 20 -o \"$archive_path\" \"$url\" (attempt ${fetch_attempt}/${fetch_attempts})" + if curl -fL --retry 3 --retry-delay 2 --connect-timeout 20 -o "$archive_path" "$url" >"$fetch_log" 2>&1; then + download_ok=1 + else + log_warn "curl download failed on attempt ${fetch_attempt}/${fetch_attempts}; showing last lines from $fetch_log" + tail -n 20 "$fetch_log" 2>/dev/null || true + fi + fi + + if [ "$download_ok" -ne 1 ]; then + if command -v wget >/dev/null 2>&1; then + log_info "exec: wget --tries=3 --timeout=20 -O \"$archive_path\" \"$url\" (attempt ${fetch_attempt}/${fetch_attempts})" + if wget --tries=3 --timeout=20 -O "$archive_path" "$url" >"$fetch_log" 2>&1; then + download_ok=1 + else + log_warn "wget download failed on attempt ${fetch_attempt}/${fetch_attempts}; showing last lines from $fetch_log" + tail -n 20 "$fetch_log" 2>/dev/null || true + fi + fi + fi + + if [ "$download_ok" -eq 1 ]; then + break + fi + + if [ "$fetch_attempt" -lt "$fetch_attempts" ]; then + log_warn "Download attempt ${fetch_attempt}/${fetch_attempts} failed; retrying after ${fetch_retry_delay}s" + sleep "$fetch_retry_delay" + fi + + fetch_attempt=$((fetch_attempt + 1)) + done + + if [ "$download_ok" -ne 1 ]; then + log_error "Failed to download audio clips using available download tools" + log_error "Fetch log preserved at: $fetch_log" + rm -f "$archive_path" >/dev/null 2>&1 || true + return 1 + fi + + if [ ! -s "$archive_path" ]; then + log_error "Downloaded archive is missing or empty: $archive_path" + log_error "Fetch log preserved at: $fetch_log" + rm -f "$archive_path" >/dev/null 2>&1 || true + return 1 + fi + + log_info "exec: tar -xzf \"$archive_path\" -C \"$clips_dir\"" + if ! tar -xzf "$archive_path" -C "$clips_dir" >>"$fetch_log" 2>&1; then + log_error "Failed to extract audio clips archive into $clips_dir" + log_error "Fetch log preserved at: $fetch_log" + rm -f "$archive_path" >/dev/null 2>&1 || true + return 1 + fi + + rm -f "$archive_path" >/dev/null 2>&1 || true + + # Normalize nested archive layout like AudioClips/AudioClips/*.wav -> AudioClips/*.wav + if [ -d "$clips_dir/AudioClips" ]; then + log_warn "Detected nested AudioClips directory after extraction.. normalizing layout" + for nested_item in "$clips_dir/AudioClips"/*; do + [ -e "$nested_item" ] || continue + nested_name=$(basename "$nested_item") + if [ ! -e "$clips_dir/$nested_name" ]; then + if ! mv "$nested_item" "$clips_dir/"; then + log_error "Failed to normalize extracted clips layout" + log_error "Fetch log preserved at: $fetch_log" + return 1 + fi + fi + done + rmdir "$clips_dir/AudioClips" >/dev/null 2>&1 || true + fi + + if ! audio_has_runnable_discovery_clips; then + log_error "Extraction completed, but no runnable discovery clips were found in $clips_dir" + log_error "Fetch log preserved at: $fetch_log" + return 1 + fi + + : > "$marker_file" || true + log_info "Audio clips download/extract validation completed" + log_info "Fetch log saved at: $fetch_log" + return 0 } + # audio_ensure_clip_ready [tarball-url] # Return codes: # 0 = clip exists/ready @@ -277,78 +419,151 @@ audio_restart_services_best_effort() { return 0 } + +# Restart PipeWire through systemd without blocking forever in `systemctl restart`. +# Polls the unit state until the restart job settles or times out. +audio_restart_pipewire_service() { + aprs_label="$1" + aprs_timeout="${PIPEWIRE_SYSTEMCTL_TIMEOUT:-180}" + aprs_start_s="$(date +%s 2>/dev/null || echo 0)" + aprs_next_log=10 + + if [ -z "$aprs_label" ]; then + aprs_label="1/1" + fi + + if ! command -v systemctl >/dev/null 2>&1; then + log_fail "systemctl not available, cannot restart pipewire" + return 1 + fi + + log_info "exec: systemctl restart pipewire (attempt ${aprs_label})" + if ! systemctl restart pipewire >/dev/null 2>&1; then + log_warn "Failed to queue pipewire restart job on attempt ${aprs_label}" + return 1 + fi + + while :; do + aprs_now_s="$(date +%s 2>/dev/null || echo 0)" + aprs_elapsed=$((aprs_now_s - aprs_start_s)) + if [ "$aprs_elapsed" -lt 0 ]; then + aprs_elapsed=0 + fi + + if [ "$aprs_elapsed" -ge "$aprs_timeout" ]; then + aprs_active_state="$(systemctl show -p ActiveState --value pipewire 2>/dev/null || echo unknown)" + aprs_sub_state="$(systemctl show -p SubState --value pipewire 2>/dev/null || echo unknown)" + aprs_job_state="$(systemctl show -p Job --value pipewire 2>/dev/null || echo unknown)" + log_warn "PipeWire restart attempt ${aprs_label} timed out after ${aprs_timeout}s (state=${aprs_active_state}/${aprs_sub_state}, job=${aprs_job_state})" + return 1 + fi + + aprs_active_state="$(systemctl show -p ActiveState --value pipewire 2>/dev/null || echo unknown)" + aprs_sub_state="$(systemctl show -p SubState --value pipewire 2>/dev/null || echo unknown)" + aprs_result_state="$(systemctl show -p Result --value pipewire 2>/dev/null || echo unknown)" + aprs_job_state="$(systemctl show -p Job --value pipewire 2>/dev/null || echo unknown)" + + case "$aprs_job_state" in + ""|0) + aprs_job_done=1 + ;; + *) + aprs_job_done=0 + ;; + esac + + if [ "$aprs_active_state" = "active" ] && [ "$aprs_sub_state" = "running" ] && [ "$aprs_job_done" -eq 1 ]; then + return 0 + fi + + if [ "$aprs_active_state" = "failed" ]; then + log_warn "PipeWire entered failed state on attempt ${aprs_label} (state=${aprs_active_state}/${aprs_sub_state}, result=${aprs_result_state})" + return 1 + fi + + if [ "$aprs_result_state" = "failed" ]; then + log_warn "PipeWire restart job failed on attempt ${aprs_label} (state=${aprs_active_state}/${aprs_sub_state}, result=${aprs_result_state})" + return 1 + fi + + if [ "$aprs_elapsed" -ge "$aprs_next_log" ]; then + log_info "Still waiting for pipewire restart job... (state=${aprs_active_state}/${aprs_sub_state} job=${aprs_job_state} ${aprs_elapsed}s/${aprs_timeout}s)" + aprs_next_log=$((aprs_next_log + 10)) + fi + + sleep 1 + done +} + # 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 - +# Configure overlay-specific audio prerequisites and restart PipeWire. +# Restart and readiness share the same centralized timeout to avoid drift. 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" - 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" + PIPEWIRE_SYSTEMCTL_TIMEOUT="${PIPEWIRE_SYSTEMCTL_TIMEOUT:-180}" + PIPEWIRE_READY_TIMEOUT="${PIPEWIRE_READY_TIMEOUT:-120}" + PIPEWIRE_RESTART_RETRIES="${PIPEWIRE_RESTART_RETRIES:-3}" + + sao_attempt=1 + + 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..." + + if [ "$(id -u)" -ne 0 ]; then + log_fail "Overlay audio setup requires root permissions" + return 1 + fi + + if [ -e /dev/dma_heap/system ]; then + log_info "Setting permissions on /dev/dma_heap/system" + if ! chmod 666 /dev/dma_heap/system; then + log_fail "Failed to chmod /dev/dma_heap/system" + 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 + else + log_warn "/dev/dma_heap/system not found, skipping chmod" + fi + + if ! audio_should_use_service_recovery "pipewire"; then + log_info "pipewire service unit not available; skipping service restart and leaving backend recovery to manual bootstrap" + return 0 + fi + + while [ "$sao_attempt" -le "$PIPEWIRE_RESTART_RETRIES" ]; do + sao_label="${sao_attempt}/${PIPEWIRE_RESTART_RETRIES}" + + if audio_restart_pipewire_service "$sao_label"; then + log_info "Waiting for pipewire to be ready..." + if audio_wait_audio_ready "$PIPEWIRE_READY_TIMEOUT" pipewire; then + log_pass "PipeWire is ready" + return 0 + fi + + sao_active_state="$(systemctl show -p ActiveState --value pipewire 2>/dev/null || echo unknown)" + sao_sub_state="$(systemctl show -p SubState --value pipewire 2>/dev/null || echo unknown)" + log_warn "PipeWire restart attempt ${sao_label} reached active state but backend was not ready within ${PIPEWIRE_READY_TIMEOUT}s (state=${sao_active_state}/${sao_sub_state})" fi - - # Restart PipeWire - log_info "Restarting pipewire service..." - if ! audio_exec_with_timeout 30s systemctl restart pipewire 2>/dev/null; then - log_fail "Failed to restart pipewire service" - return 1 + + if [ "$sao_attempt" -lt "$PIPEWIRE_RESTART_RETRIES" ]; then + log_warn "Retrying full pipewire restart flow after short delay" + sleep 2 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 audio_pw_ctl_ok >/dev/null 2>&1 && audio_pw_ctl_ok; 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 + + sao_attempt=$((sao_attempt + 1)) + done + + sao_active_state="$(systemctl show -p ActiveState --value pipewire 2>/dev/null || echo unknown)" + sao_sub_state="$(systemctl show -p SubState --value pipewire 2>/dev/null || echo unknown)" + log_fail "PipeWire failed to become ready after ${PIPEWIRE_RESTART_RETRIES} attempts (state=${sao_active_state}/${sao_sub_state})" + log_fail "This usually means the sound card is still not online" + log_fail "Check 'systemctl status pipewire' and 'journalctl -u pipewire' for details" + return 1 } # ---------- PipeWire control helpers (bounded; never hang) ---------- @@ -844,26 +1059,63 @@ audio_exec_with_timeout() { return $? } +# Wait until the requested audio backend becomes usable. +# Uses real elapsed time, not loop count, so slow ctl probes do not skew timeout logs. audio_wait_audio_ready() { - max_s="${1:-20}" - i=0 - - while [ "$i" -lt "$max_s" ] 2>/dev/null; do - if command -v wpctl >/dev/null 2>&1 && audio_pw_ctl_ok; then - return 0 + max_s="${1:-${PIPEWIRE_READY_TIMEOUT:-120}}" + backend_name="${2:-auto}" + start_s="$(date +%s 2>/dev/null || echo 0)" + next_log=10 + + while :; do + now_s="$(date +%s 2>/dev/null || echo 0)" + elapsed=$((now_s - start_s)) + if [ "$elapsed" -lt 0 ]; then + elapsed=0 fi - if command -v pactl >/dev/null 2>&1 && audio_pa_ctl_ok; then - return 0 + + if [ "$elapsed" -ge "$max_s" ]; then + break fi - if [ -d /dev/snd ] || [ -e /proc/asound/cards ]; then - return 0 + + case "$backend_name" in + pipewire) + if audio_backend_ready pipewire; then + return 0 + fi + ;; + pulseaudio) + if audio_backend_ready pulseaudio; then + return 0 + fi + ;; + auto|"") + if audio_backend_ready pipewire; then + return 0 + fi + if audio_backend_ready pulseaudio; then + return 0 + fi + if [ -d /dev/snd ] || [ -e /proc/asound/cards ]; then + return 0 + fi + ;; + *) + return 1 + ;; + esac + + if [ "$elapsed" -ge "$next_log" ]; then + log_info "Still waiting for ${backend_name:-audio}... (${elapsed}s/${max_s}s)" + next_log=$((next_log + 10)) fi + sleep 1 - i="$(expr "$i" + 1)" done return 1 } + # --- bounded wpctl helpers (never hang) --- pwctl_status_safe() { # Prints wpctl status to stdout on success, returns nonzero on failure/timeout. @@ -979,17 +1231,21 @@ alsa_pick_virtual_pcm() { audio_check_clips_available() { formats="$1" durations="$2" - + + if [ -z "$formats" ] || [ -z "$durations" ]; then + return 1 + fi + 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) ---------- @@ -1599,3 +1855,696 @@ discover_and_filter_record_configs() { printf '%s\n' "$available_configs" return 0 } + +# Generic backend readiness wrapper used by run.sh. +# Reuses existing daemon/control-plane helpers instead of duplicating probe logic. +audio_backend_ready() { + case "$1" in + pipewire) + if check_audio_daemon pipewire >/dev/null 2>&1; then + if audio_pw_ctl_ok >/dev/null 2>&1; then + return 0 + fi + fi + return 1 + ;; + pulseaudio) + if check_audio_daemon pulseaudio >/dev/null 2>&1; then + if audio_pa_ctl_ok >/dev/null 2>&1; then + return 0 + fi + fi + return 1 + ;; + alsa) + if command -v arecord >/dev/null 2>&1; then + return 0 + fi + return 1 + ;; + *) + return 1 + ;; + esac +} + +# Track background daemon PIDs started by this script. +# These PIDs are later cleaned up on exit. +audio_add_started_pid() { + if [ -n "$1" ]; then + if [ -n "${AUDIO_STARTED_PIDS:-}" ]; then + AUDIO_STARTED_PIDS="$AUDIO_STARTED_PIDS $1" + else + AUDIO_STARTED_PIDS="$1" + fi + export AUDIO_STARTED_PIDS + fi +} + +# Stop any audio daemons started by manual bootstrap. +# Also removes the temporary runtime directory if this script created it. +audio_cleanup_started_daemons() { + for pid in ${AUDIO_STARTED_PIDS:-}; do + if kill -0 "$pid" >/dev/null 2>&1; then + kill "$pid" >/dev/null 2>&1 || true + fi + done + + if [ "${AUDIO_CREATED_RUNTIME_DIR:-0}" -eq 1 ] 2>/dev/null; then + if [ -n "${XDG_RUNTIME_DIR:-}" ]; then + rmdir "$XDG_RUNTIME_DIR" >/dev/null 2>&1 || true + fi + fi +} + +# Ensure XDG_RUNTIME_DIR is available for PipeWire/PulseAudio in minimal userspace. +# Reuses an existing writable runtime dir when possible, otherwise creates one under /tmp. +audio_ensure_runtime_dir() { + uid_now="$(id -u 2>/dev/null || echo 0)" + + if [ -n "${AUDIO_RUNTIME_DIR:-}" ]; then + run_dir="$AUDIO_RUNTIME_DIR" + elif [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -d "$XDG_RUNTIME_DIR" ] && [ -w "$XDG_RUNTIME_DIR" ]; then + return 0 + elif [ -d "/run/user/$uid_now" ] && [ -w "/run/user/$uid_now" ]; then + run_dir="/run/user/$uid_now" + else + run_dir="/tmp/audio-runtime-$uid_now" + AUDIO_CREATED_RUNTIME_DIR=1 + export AUDIO_CREATED_RUNTIME_DIR + fi + + if [ ! -d "$run_dir" ]; then + mkdir -p "$run_dir" || return 1 + fi + + chmod 700 "$run_dir" >/dev/null 2>&1 || true + XDG_RUNTIME_DIR="$run_dir" + export XDG_RUNTIME_DIR + return 0 +} + +# Start a background process and redirect its output to a log file. +# Returns the spawned PID so the caller can track and clean it up later. +audio_start_bg_logged() { + bg_log="$1" + shift + "$@" >>"$bg_log" 2>&1 & + echo "$!" +} + +# Manually start PipeWire and its session manager in minimal ramdisk userspace. +# Reuses existing daemon/control-plane helpers to validate readiness. +audio_manual_start_pipewire() { + pipewire_log="$LOGDIR/pipewire-bootstrap.log" + session_log="$LOGDIR/pipewire-session.log" + pulse_log="$LOGDIR/pipewire-pulse.log" + + if check_audio_daemon pipewire >/dev/null 2>&1; then + if audio_pw_ctl_ok >/dev/null 2>&1; then + return 0 + fi + fi + + if ! have_cmd pipewire; then + return 1 + fi + + if ! audio_ensure_runtime_dir; then + log_error "Failed to prepare XDG_RUNTIME_DIR for PipeWire" + return 1 + fi + + export HOME="${HOME:-/tmp}" + + pw_pid="$(audio_start_bg_logged "$pipewire_log" pipewire)" + audio_add_started_pid "$pw_pid" + sleep 2 + + if have_cmd pipewire-media-session; then + sm_pid="$(audio_start_bg_logged "$session_log" pipewire-media-session)" + audio_add_started_pid "$sm_pid" + elif have_cmd wireplumber; then + sm_pid="$(audio_start_bg_logged "$session_log" wireplumber)" + audio_add_started_pid "$sm_pid" + else + log_warn "No PipeWire session manager found (wireplumber / pipewire-media-session)" + fi + + if have_cmd pipewire-pulse; then + pp_pid="$(audio_start_bg_logged "$pulse_log" pipewire-pulse)" + audio_add_started_pid "$pp_pid" + fi + + AUDIO_BACKEND="pipewire" + export AUDIO_BACKEND + + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + + if check_audio_daemon pipewire >/dev/null 2>&1; then + if audio_pw_ctl_ok >/dev/null 2>&1; then + return 0 + fi + fi + + return 1 +} + +# Manually start PulseAudio in minimal ramdisk userspace. +# Reuses existing daemon/control-plane helpers to validate readiness. +audio_manual_start_pulseaudio() { + pulseaudio_log="$LOGDIR/pulseaudio-bootstrap.log" + uid_now="$(id -u 2>/dev/null || echo 0)" + + if check_audio_daemon pulseaudio >/dev/null 2>&1; then + if audio_pa_ctl_ok >/dev/null 2>&1; then + return 0 + fi + fi + + if ! have_cmd pulseaudio; then + return 1 + fi + + if ! audio_ensure_runtime_dir; then + log_error "Failed to prepare XDG_RUNTIME_DIR for PulseAudio" + return 1 + fi + + export HOME="${HOME:-/tmp}" + + if [ "$uid_now" -eq 0 ] 2>/dev/null; then + pa_pid="$(audio_start_bg_logged "$pulseaudio_log" pulseaudio --system --daemonize=no --disallow-exit --exit-idle-time=-1)" + else + pa_pid="$(audio_start_bg_logged "$pulseaudio_log" pulseaudio --daemonize=no --exit-idle-time=-1)" + fi + audio_add_started_pid "$pa_pid" + + AUDIO_BACKEND="pulseaudio" + export AUDIO_BACKEND + + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + + if check_audio_daemon pulseaudio >/dev/null 2>&1; then + if [ -z "${PULSE_SERVER:-}" ]; then + if [ -S "$XDG_RUNTIME_DIR/pulse/native" ]; then + PULSE_SERVER="unix:$XDG_RUNTIME_DIR/pulse/native" + export PULSE_SERVER + elif [ -S /run/pulse/native ]; then + PULSE_SERVER="unix:/run/pulse/native" + export PULSE_SERVER + elif [ -S /var/run/pulse/native ]; then + PULSE_SERVER="unix:/var/run/pulse/native" + export PULSE_SERVER + fi + fi + + if audio_pa_ctl_ok >/dev/null 2>&1; then + return 0 + fi + fi + + return 1 +} + +# Choose which backend to bootstrap when none is explicitly running yet. +# Prefers PipeWire first, then PulseAudio, based on available binaries/tools. +audio_choose_bootstrap_backend() { + if [ -n "${AUDIO_BACKEND:-}" ]; then + echo "$AUDIO_BACKEND" + return 0 + fi + + if have_cmd pipewire; then + if have_cmd pw-play || have_cmd pw-record || have_cmd wpctl || have_cmd pw-cli; then + echo "pipewire" + return 0 + fi + fi + + if have_cmd pulseaudio; then + if have_cmd paplay || have_cmd parecord || have_cmd pactl; then + echo "pulseaudio" + return 0 + fi + fi + + echo "" + return 1 +} + +# Return success only when the given systemd unit actually exists on this target. +audio_systemd_unit_exists() { + unit_name="$1" + + if ! command -v systemctl >/dev/null 2>&1; then + return 1 + fi + + if systemctl list-unit-files "$unit_name" --no-legend 2>/dev/null | awk 'NF { found=1 } END { exit !found }'; then + return 0 + fi + + return 1 +} + +# Return success only when the requested backend is genuinely managed by systemd here. +# This avoids assuming that "systemctl exists" means "pipewire/pulseaudio service exists". +audio_backend_is_systemd_managed() { + backend_name="$1" + + case "$backend_name" in + pipewire) + if audio_systemd_unit_exists "pipewire.service" \ + || audio_systemd_unit_exists "pipewire.socket" \ + || audio_systemd_unit_exists "pipewire-pulse.service" \ + || audio_systemd_unit_exists "pipewire-pulse.socket"; then + return 0 + fi + return 1 + ;; + pulseaudio) + if audio_systemd_unit_exists "pulseaudio.service" \ + || audio_systemd_unit_exists "pulseaudio.socket"; then + return 0 + fi + return 1 + ;; + *) + return 1 + ;; + esac +} + +# Decide whether manual bootstrap is allowed, then start the best available backend. +# In auto mode, bootstrap is allowed when: +# 1) there is no normal systemd userspace, or +# 2) the chosen backend is not systemd-managed on this target. +audio_bootstrap_backend_if_needed() { + start_allowed=0 + requested_backend="${AUDIO_BACKEND:-}" + chosen_backend="" + backend_probe="" + + case "${AUDIO_BOOTSTRAP_MODE:-auto}" in + true|1|yes) + start_allowed=1 + ;; + auto) + backend_probe="$requested_backend" + if [ -z "$backend_probe" ]; then + backend_probe="$(audio_choose_bootstrap_backend 2>/dev/null || echo "")" + fi + + if [ -n "$backend_probe" ]; then + if ! audio_should_use_service_recovery "$backend_probe"; then + start_allowed=1 + fi + fi + ;; + false|0|no) + start_allowed=0 + ;; + *) + log_warn "Unknown AUDIO_BOOTSTRAP_MODE='${AUDIO_BOOTSTRAP_MODE:-}', treating as auto" + backend_probe="$requested_backend" + if [ -z "$backend_probe" ]; then + backend_probe="$(audio_choose_bootstrap_backend 2>/dev/null || echo "")" + fi + if [ -n "$backend_probe" ]; then + if ! audio_should_use_service_recovery "$backend_probe"; then + start_allowed=1 + fi + fi + ;; + esac + + if [ "$start_allowed" -ne 1 ]; then + return 1 + fi + + chosen_backend="$(audio_choose_bootstrap_backend)" + if [ -z "$chosen_backend" ]; then + log_warn "No backend binaries available for manual bootstrap" + return 1 + fi + + log_info "Attempting manual audio backend bootstrap: $chosen_backend" + + if [ "$chosen_backend" = "pipewire" ]; then + if audio_manual_start_pipewire; then + AUDIO_BACKEND="pipewire" + export AUDIO_BACKEND + return 0 + fi + + if [ -z "$requested_backend" ]; then + if have_cmd pulseaudio && have_cmd paplay; then + log_warn "PipeWire bootstrap failed, trying PulseAudio fallback" + if audio_manual_start_pulseaudio; then + AUDIO_BACKEND="pulseaudio" + export AUDIO_BACKEND + return 0 + fi + fi + fi + elif [ "$chosen_backend" = "pulseaudio" ]; then + if audio_manual_start_pulseaudio; then + AUDIO_BACKEND="pulseaudio" + export AUDIO_BACKEND + return 0 + fi + fi + + return 1 +} + +audio_backend_has_service_unit() { + case "$1" in + pipewire) + if audio_systemd_unit_exists "pipewire.service"; then + return 0 + fi + return 1 + ;; + pulseaudio) + if audio_systemd_unit_exists "pulseaudio.service"; then + return 0 + fi + if audio_systemd_unit_exists "pulseaudio.socket"; then + return 0 + fi + return 1 + ;; + *) + return 1 + ;; + esac +} + +audio_should_use_service_recovery() { + backend_name="$1" + + if [ ! -d /run/systemd/system ]; then + return 1 + fi + + if ! command -v systemctl >/dev/null 2>&1; then + return 1 + fi + + if audio_backend_has_service_unit "$backend_name"; then + return 0 + fi + + return 1 +} + +audio_playback_alsa_prepare() { + ap_ucm_card="" + + if [ "${SINK_CHOICE:-speakers}" = "null" ]; then + return 0 + fi + + if command -v alsaucm >/dev/null 2>&1; then + ap_ucm_card="$(alsaucm listcards 2>/dev/null | awk 'NR==2 {sub(/^[[:space:]]+/, "", $0); print; exit}')" + if [ -n "$ap_ucm_card" ]; then + alsaucm -n -b - </dev/null 2>&1 +open $ap_ucm_card +reset +set _verb HiFi +set _enadev Speaker +EOF + fi + fi + + if command -v amixer >/dev/null 2>&1; then + if amixer -c 0 scontrols 2>/dev/null | grep -F "PRIMARY_MI2S_RX Audio Mixer MultiMedia1" >/dev/null 2>&1; then + amixer -c 0 cset name='PRIMARY_MI2S_RX Audio Mixer MultiMedia1' 1 >/dev/null 2>&1 || true + fi + if amixer -c 0 scontrols 2>/dev/null | grep -F "stream0.vol_ctrl0 MultiMedia1 Playback Volu" >/dev/null 2>&1; then + amixer -c 0 cset name='stream0.vol_ctrl0 MultiMedia1 Playback Volu' 65535 >/dev/null 2>&1 || true + fi + fi + + return 0 +} + +audio_playback_pick_alsa_sink() { + ap_dev="" + + if [ "${SINK_CHOICE:-speakers}" = "null" ]; then + echo "null" + return 0 + fi + + if command -v aplay >/dev/null 2>&1; then + ap_dev="$(aplay -L 2>/dev/null | awk '/^default:CARD=/{print $1; exit}')" + if [ -n "$ap_dev" ]; then + echo "$ap_dev" + return 0 + fi + + ap_dev="$(aplay -L 2>/dev/null | awk '/^sysdefault:CARD=/{print $1; exit}')" + if [ -n "$ap_dev" ]; then + echo "$ap_dev" + return 0 + fi + + ap_dev="$(aplay -l 2>/dev/null | sed -n 's/^card[[:space:]]*\([0-9][0-9]*\):.*device[[:space:]]*\([0-9][0-9]*\):.*/plughw:\1,\2/p' | head -n 1)" + if [ -n "$ap_dev" ]; then + echo "$ap_dev" + return 0 + fi + fi + + echo "" + return 1 +} + +audio_playback_alsa_probe() { + ap_probe_dev="$(audio_playback_pick_alsa_sink)" + if [ -z "$ap_probe_dev" ]; then + return 1 + fi + + audio_playback_alsa_prepare >/dev/null 2>&1 || true + + if audio_exec_with_timeout 5s aplay -D "$ap_probe_dev" -t raw -f S16_LE -r 48000 -c 2 -d 1 /dev/zero >/dev/null 2>&1; then + AUDIO_ALSA_PLAYBACK_DEVICE="$ap_probe_dev" + export AUDIO_ALSA_PLAYBACK_DEVICE + return 0 + fi + + return 1 +} + +audio_record_alsa_prepare_capture() { + ar_ucm_card="" + + if command -v alsaucm >/dev/null 2>&1; then + ar_ucm_card="$(alsaucm listcards 2>/dev/null | awk 'NR==2 {sub(/^[[:space:]]+/, "", $0); print; exit}')" + if [ -n "$ar_ucm_card" ]; then + alsaucm -n -b - </dev/null 2>&1 +open $ar_ucm_card +reset +set _verb HiFi +set _enadev Mic +EOF + fi + fi + + if command -v amixer >/dev/null 2>&1; then + if amixer -c 0 scontrols 2>/dev/null | grep -F "MultiMedia2 Mixer TERTIARY_MI2S_TX" >/dev/null 2>&1; then + amixer -c 0 cset name='MultiMedia2 Mixer TERTIARY_MI2S_TX' 1 >/dev/null 2>&1 || true + fi + fi + + return 0 +} + +audio_record_pick_alsa_capture() { + ar_dev="" + + if command -v arecord >/dev/null 2>&1; then + ar_dev="$(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 [ -n "$ar_dev" ]; then + echo "$ar_dev" + return 0 + fi + fi + + ar_dev="$(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)" + if [ -n "$ar_dev" ]; then + echo "$ar_dev" + return 0 + fi + + echo "" + return 1 +} + +audio_record_alsa_capture_probe() { + ar_probe_dev="$(audio_record_pick_alsa_capture)" + if [ -z "$ar_probe_dev" ]; then + return 1 + fi + + audio_record_alsa_prepare_capture >/dev/null 2>&1 || true + + ar_probe_out="$(mktemp /tmp/audio_record_probe.XXXXXX.wav 2>/dev/null || echo /tmp/audio_record_probe.$$)" + rm -f "$ar_probe_out" >/dev/null 2>&1 || true + + for ar_probe_combo in "S16_LE 16000 1" "S16_LE 48000 1" "S16_LE 48000 2"; do + ar_fmt="$(printf '%s\n' "$ar_probe_combo" | awk '{print $1}')" + ar_rate="$(printf '%s\n' "$ar_probe_combo" | awk '{print $2}')" + ar_ch="$(printf '%s\n' "$ar_probe_combo" | awk '{print $3}')" + + if audio_exec_with_timeout 5s arecord -D "$ar_probe_dev" -f "$ar_fmt" -r "$ar_rate" -c "$ar_ch" -d 1 "$ar_probe_out" >/dev/null 2>&1; then + if [ -s "$ar_probe_out" ]; then + AUDIO_ALSA_CAPTURE_DEVICE="$ar_probe_dev" + export AUDIO_ALSA_CAPTURE_DEVICE + rm -f "$ar_probe_out" >/dev/null 2>&1 || true + return 0 + fi + fi + rm -f "$ar_probe_out" >/dev/null 2>&1 || true + + case "$ar_probe_dev" in + hw:*) + ar_alt_dev="plughw:${ar_probe_dev#hw:}" + if audio_exec_with_timeout 5s arecord -D "$ar_alt_dev" -f "$ar_fmt" -r "$ar_rate" -c "$ar_ch" -d 1 "$ar_probe_out" >/dev/null 2>&1; then + if [ -s "$ar_probe_out" ]; then + AUDIO_ALSA_CAPTURE_DEVICE="$ar_alt_dev" + export AUDIO_ALSA_CAPTURE_DEVICE + rm -f "$ar_probe_out" >/dev/null 2>&1 || true + return 0 + fi + fi + rm -f "$ar_probe_out" >/dev/null 2>&1 || true + ;; + esac + done + + rm -f "$ar_probe_out" >/dev/null 2>&1 || true + return 1 +} + +audio_probe_alsa_capture_profile() { + # shellcheck disable=SC2034 + AUDIO_ALSA_CAPTURE_DEVICE="" + # shellcheck disable=SC2034 + AUDIO_ALSA_CAPTURE_FORMAT="" + # shellcheck disable=SC2034 + AUDIO_ALSA_CAPTURE_RATE="" + # shellcheck disable=SC2034 + AUDIO_ALSA_CAPTURE_CHANNELS="" + # shellcheck disable=SC2034 + AUDIO_ALSA_CAPTURE_REASON="" + + if ! command -v arecord >/dev/null 2>&1; then + # shellcheck disable=SC2034 + AUDIO_ALSA_CAPTURE_REASON="arecord not available" + return 1 + fi + + probe_tmp="" + if command -v mktemp >/dev/null 2>&1; then + probe_tmp="$(mktemp /tmp/audio_record_probe.XXXXXX.wav 2>/dev/null || true)" + fi + if [ -z "$probe_tmp" ]; then + probe_tmp="/tmp/audio_record_probe.$$.$(date +%s 2>/dev/null || echo 0).wav" + fi + + probe_cleanup() { + if [ -n "$probe_tmp" ] && [ -f "$probe_tmp" ]; then + rm -f "$probe_tmp" >/dev/null 2>&1 || true + fi + } + + probe_devices="" + cand="$(alsa_pick_capture 2>/dev/null || true)" + if [ -n "$cand" ]; then + probe_devices="$cand" + case "$cand" in + hw:*) + probe_devices="$probe_devices plughw:${cand#hw:}" + ;; + plughw:*) + probe_devices="$probe_devices hw:${cand#plughw:}" + ;; + esac + fi + + extra_devices="$(sed -n 's/^\([0-9][0-9]*\)-\([0-9][0-9]*\):.*capture.*/hw:\1,\2/p' /proc/asound/pcm 2>/dev/null)" + if [ -n "$extra_devices" ]; then + for dev in $extra_devices; do + seen=0 + for existing in $probe_devices; do + if [ "$existing" = "$dev" ]; then + seen=1 + break + fi + done + if [ "$seen" -eq 0 ]; then + probe_devices="$probe_devices $dev" + case "$dev" in + hw:*) + probe_devices="$probe_devices plughw:${dev#hw:}" + ;; + esac + fi + done + fi + + if [ -z "$probe_devices" ]; then + # shellcheck disable=SC2034 + AUDIO_ALSA_CAPTURE_REASON="no ALSA capture device candidates found" + probe_cleanup + return 1 + fi + + for dev in $probe_devices; do + for combo in \ + "S16_LE 48000 1" \ + "S16_LE 16000 1" \ + "S16_LE 48000 2" \ + "S16_LE 16000 2" \ + "S24_LE 48000 2" + 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}')" + + : > "$probe_tmp" + + if audio_exec_with_timeout 5s \ + arecord -q -D "$dev" -f "$fmt" -r "$rate" -c "$ch" -d 1 "$probe_tmp" >/dev/null 2>&1 + then + bytes="$(file_size_bytes "$probe_tmp" 2>/dev/null || echo 0)" + if [ "${bytes:-0}" -gt 44 ] 2>/dev/null; then + # Used later by sourced run.sh + # shellcheck disable=SC2034 + AUDIO_ALSA_CAPTURE_DEVICE="$dev" + # shellcheck disable=SC2034 + AUDIO_ALSA_CAPTURE_FORMAT="$fmt" + # shellcheck disable=SC2034 + AUDIO_ALSA_CAPTURE_RATE="$rate" + # shellcheck disable=SC2034 + AUDIO_ALSA_CAPTURE_CHANNELS="$ch" + # shellcheck disable=SC2034 + AUDIO_ALSA_CAPTURE_REASON="" + probe_cleanup + return 0 + fi + fi + done + done + + # Used later by sourced run.sh + # shellcheck disable=SC2034 + AUDIO_ALSA_CAPTURE_REASON="no ALSA capture profile could be opened" + probe_cleanup + return 1 +}