diff --git a/Runner/suites/Multimedia/Audio/AudioLoopback/README.md b/Runner/suites/Multimedia/Audio/AudioLoopback/README.md index cd77ae19..77636352 100644 --- a/Runner/suites/Multimedia/Audio/AudioLoopback/README.md +++ b/Runner/suites/Multimedia/Audio/AudioLoopback/README.md @@ -4,11 +4,29 @@ media. It generates a deterministic 48 kHz, unsigned 8-bit stereo WAV, starts capture, plays the reference, and validates the resulting WAV. -Automatic selection uses an active PipeWire or PulseAudio session and its -physical speaker and microphone routes. PipeWire requires `wpctl`, `pw-record`, -and `pw-play` or `pw-cat`. PulseAudio requires `pactl`, `parecord`, and `paplay`. +Automatic selection uses an active PipeWire or PulseAudio session and discovers +playback and capture independently. Playback tries speakers and then headphones. +Capture tries mic and then headset-mic. PipeWire requires `wpctl`, `pw-record`, +and `pw-play` or `pw-cat`. +PulseAudio requires `pactl`, `parecord`, and `paplay`. +Playback aliases `speaker`/`speakers` and +`headphone`/`headphones`/`headset` map to the same canonical routes. Capture +aliases `mic`/`microphone` and +`headset-mic`/`headset_mic`/quoted `headset mic` are normalized before runtime +route discovery. The effective selections and targets are emitted to standard +output in an `AUDIO_ROUTE` record. If no managed backend is usable, the suite falls back to image-provided `aplay` -and `arecord`. The suite never installs runtime packages. +and `arecord`. Direct ALSA mode retrieves `PlaybackPCM` and `CapturePCM` from +the matching UCM HiFi devices instead of encoding card or PCM numbers. It +enables both devices in one UCM session when they share a card and verifies +both through `_enadevs`. Private `_ucmNNNN.` prefixes are removed only from +standard `hw:` or `plughw:` PCM names. UCM-only private virtual names are not +used as standalone direct PCM routes. Auto-discovered raw `hw:` playback PCMs +use the corresponding `plughw:` target so ALSA can convert the generated U8 +reference to the hardware PCM capabilities. Capture keeps its probed hardware +format and device. Legacy speaker/microphone discovery +remains available for images without usable UCM routes. The suite never +installs runtime packages. On Debian and CentOS, a root invocation prepares the regular desktop audio user and re-launches the complete test as that user. Automatic selection also @@ -20,9 +38,9 @@ execution behavior. The selected output must reach the selected input through an acoustic or electrical fixture. Automatic selection prefers the active managed backend and -its physical speaker and microphone endpoints. Supplying either device option -selects direct ALSA mode, where the unspecified side is discovered. Explicit -ALSA devices are recommended for fixtures that require fixed PCM endpoints: +its requested physical endpoints. Supplying either device option selects direct +ALSA mode, where the unspecified side is discovered. Explicit ALSA devices are +recommended for fixtures that require fixed PCM endpoints: ```sh ./run.sh \ @@ -31,10 +49,19 @@ ALSA devices are recommended for fixtures that require fixed PCM endpoints: --duration 5 ``` -When no backend or device inventory exists, the suite reports SKIP with the -missing prerequisite. When a managed backend is active but its physical routes -are unusable and direct ALSA fallback also fails, the suite reports FAIL so the -image or routing regression remains visible. An all-zero capture reports FAIL. +Route-only examples use runtime endpoint and UCM discovery: + +```sh +./run.sh --sink speakers --source mic --duration 5 +./run.sh --sink headphones --source headset-mic --duration 5 +``` + +An explicit semantic route or PCM device is used only after runtime discovery +and an open probe succeed. Explicit unavailable or malformed choices report +FAIL with the requested route/device and retained probe evidence. Automatic +discovery with no applicable route pair reports SKIP. A ready backend with +missing required clients, a broken applicable route, or an all-zero capture +reports FAIL. The default policy does not claim waveform correlation when the input contains some other non-zero signal, so retained metrics and the fixture setup remain important evidence. @@ -46,17 +73,28 @@ WAV integrity: corrupt or empty files, header-only payloads, all-zero audio, and materially short captures fail. RMS, peak, clipping, digital-silence runs, DC offset, large sample transitions, and silent-channel counts are emitted in `AUDIO_VALIDATION` records as diagnostic metrics. They do not fail the default -policy. +policy. The generated two-level square-wave reference is validated with its +known signal shape, so it does not produce a near-constant diagnostic. Complete +RIFF chunks after a WAV `data` chunk are reported as metadata rather than as +trailing audio. ## Options - `--duration SECONDS` selects 2 to 30 seconds. -- `--playback-device DEVICE` overrides playback discovery. -- `--capture-device DEVICE` overrides capture discovery and probes a supported +- `--sink auto|speaker|speakers|headphone|headphones|headset` selects playback. +- `--source auto|mic|microphone|headset-mic|headset_mic|"headset mic"` selects + capture. +- `--playback-device DEVICE|auto` overrides playback discovery. `auto` and an + empty LAVA parameter retain discovery. +- `--capture-device DEVICE|auto` overrides capture discovery and probes a supported format on that exact device. - `--dmesg-scan 0|1` controls retained kernel-log evidence. - `--no-dmesg` disables the kernel-log scan. +An explicit `--playback-device` value is used exactly as supplied. Use +`plughw:CARD,DEVICE` when the fixture's hardware PCM does not natively accept +the generated U8 stereo reference. + Artifacts are retained below `results/AudioLoopback/run--/`. ## Yocto CI diff --git a/Runner/suites/Multimedia/Audio/AudioLoopback/run.sh b/Runner/suites/Multimedia/Audio/AudioLoopback/run.sh index 290bdd50..6789f47d 100755 --- a/Runner/suites/Multimedia/Audio/AudioLoopback/run.sh +++ b/Runner/suites/Multimedia/Audio/AudioLoopback/run.sh @@ -42,7 +42,12 @@ TESTNAME="AudioLoopback" RES_FILE="$SCRIPT_DIR/${TESTNAME}.res" DURATION=5 +PLAYBACK_ROUTE="auto" +CAPTURE_ROUTE="auto" +PLAYBACK_ROUTE_EXPLICIT=0 +CAPTURE_ROUTE_EXPLICIT=0 PLAYBACK_DEVICE="" +PLAYBACK_DEVICE_SELECTION="auto" CAPTURE_DEVICE="" DMESG_SCAN="${DMESG_SCAN:-1}" ARG_ERROR="" @@ -54,6 +59,7 @@ CAPTURE_TARGET="" PLAYBACK_CLIENT="" CAPTURE_CLIENT="" MANAGED_BACKEND_FAILURE="" +MANAGED_BACKEND_APPLICABLE_FAILURE=0 # Print the audio loopback CLI and its environment-variable equivalents. usage() { @@ -62,6 +68,10 @@ Usage: $0 [options] Options: --duration SECONDS Reference playback duration, default: 5 + --sink {auto|speaker|speakers|headphone|headphones|headset} + Semantic playback route, default: auto + --source {auto|mic|microphone|headset-mic|headset_mic|"headset mic"} + Semantic capture route, default: auto --playback-device DEVICE Explicit ALSA playback device, default: auto --capture-device DEVICE Explicit ALSA capture device, default: auto --dmesg-scan {0|1} Capture audio-related kernel evidence, default: 1 @@ -70,7 +80,7 @@ Options: The test requires a real acoustic or electrical path from the selected output to the selected input. Automatic mode uses an active PipeWire or PulseAudio -speaker and microphone route before falling back to direct ALSA. It generates +requested physical routes before falling back to direct ALSA. It generates a deterministic local reference, starts capture, plays the reference, and validates the recorded WAV. EOF_USAGE @@ -93,7 +103,25 @@ parse_args() { ARG_ERROR="--playback-device requires a value" return 1 fi - PLAYBACK_DEVICE="$2" + case "$2" in + ""|auto) + PLAYBACK_DEVICE="" + PLAYBACK_DEVICE_SELECTION="auto" + ;; + *) + PLAYBACK_DEVICE="$2" + PLAYBACK_DEVICE_SELECTION="explicit" + ;; + esac + shift 2 + ;; + --sink) + if [ "$#" -lt 2 ]; then + ARG_ERROR="--sink requires a value" + return 1 + fi + PLAYBACK_ROUTE="$2" + PLAYBACK_ROUTE_EXPLICIT=1 shift 2 ;; --capture-device) @@ -101,7 +129,23 @@ parse_args() { ARG_ERROR="--capture-device requires a value" return 1 fi - CAPTURE_DEVICE="$2" + case "$2" in + ""|auto) + CAPTURE_DEVICE="" + ;; + *) + CAPTURE_DEVICE="$2" + ;; + esac + shift 2 + ;; + --source) + if [ "$#" -lt 2 ]; then + ARG_ERROR="--source requires a value" + return 1 + fi + CAPTURE_ROUTE="$2" + CAPTURE_ROUTE_EXPLICIT=1 shift 2 ;; --dmesg-scan) @@ -164,6 +208,46 @@ if [ "$DURATION" -lt 2 ] || [ "$DURATION" -gt 30 ]; then test_result_finish fi +PLAYBACK_ROUTE_REQUESTED="$PLAYBACK_ROUTE" +PLAYBACK_ROUTE="$( + audio_normalize_playback_route "$PLAYBACK_ROUTE_REQUESTED" 2>/dev/null +)" || { + test_result_record "FAIL" \ + "$TESTNAME FAIL - invalid sink '$PLAYBACK_ROUTE_REQUESTED', expected auto, speaker, speakers, headphone, headphones, or headset" + test_result_finish +} +if [ "$PLAYBACK_ROUTE" = "null" ]; then + test_result_record "FAIL" \ + "$TESTNAME FAIL - loopback requires a physical playback route, use speaker or headset" + test_result_finish +fi +if [ "$PLAYBACK_ROUTE" = "auto" ]; then + PLAYBACK_ROUTE_EXPLICIT=0 +fi + +CAPTURE_ROUTE_REQUESTED="$CAPTURE_ROUTE" +CAPTURE_ROUTE="$( + audio_normalize_capture_route "$CAPTURE_ROUTE_REQUESTED" 2>/dev/null +)" || { + test_result_record "FAIL" \ + "$TESTNAME FAIL - invalid source '$CAPTURE_ROUTE_REQUESTED', expected auto, mic, microphone, or headset-mic" + test_result_finish +} +if [ "$CAPTURE_ROUTE" = "null" ]; then + test_result_record "FAIL" \ + "$TESTNAME FAIL - loopback requires a physical capture route, use mic or headset-mic" + test_result_finish +fi +if [ "$CAPTURE_ROUTE" = "auto" ]; then + CAPTURE_ROUTE_EXPLICIT=0 +fi + +PLAYBACK_ROUTE_POLICY="$PLAYBACK_ROUTE" +CAPTURE_ROUTE_POLICY="$CAPTURE_ROUTE" +SINK_CHOICE="$PLAYBACK_ROUTE" +SRC_CHOICE="$CAPTURE_ROUTE" +export SINK_CHOICE SRC_CHOICE PLAYBACK_ROUTE_EXPLICIT CAPTURE_ROUTE_EXPLICIT + case "$DMESG_SCAN" in 0|1) ;; @@ -221,11 +305,13 @@ else audio_loopback_os_id="unknown" fi -AUDIO_USE_DESKTOP_SESSION=0 -if [ "$audio_loopback_os_id" = "ubuntu" ] && - [ "$(id -u 2>/dev/null || echo 1)" -eq 0 ]; then - AUDIO_USE_DESKTOP_SESSION=1 +if ! command -v audio_configure_ubuntu_desktop_session >/dev/null 2>&1; then + test_result_record "FAIL" \ + "$TESTNAME FAIL - required helper is unavailable: audio_configure_ubuntu_desktop_session" + test_result_finish fi + +audio_configure_ubuntu_desktop_session "$audio_loopback_os_id" export AUDIO_USE_DESKTOP_SESSION AUDIO_ALSA_PLAYBACK_PROBE_LOG="$LOGDIR/alsa_playback_probe.log" @@ -256,7 +342,8 @@ if [ -n "$missing_commands" ]; then test_result_finish fi -log_info "Starting $TESTNAME, duration=${DURATION}s playback_device=${PLAYBACK_DEVICE:-auto} capture_device=${CAPTURE_DEVICE:-auto}" +log_info "Starting $TESTNAME, duration=${DURATION}s sink=$PLAYBACK_ROUTE source=$CAPTURE_ROUTE playback_device=${PLAYBACK_DEVICE:-auto} capture_device=${CAPTURE_DEVICE:-auto}" +log_info "AUDIO_ROUTE_DISCOVERY scope=loopback playback_requested='$PLAYBACK_ROUTE_REQUESTED' playback_explicit=$PLAYBACK_ROUTE_EXPLICIT playback_policy=$PLAYBACK_ROUTE automatic_playback_order='speakers headphones' capture_requested='$CAPTURE_ROUTE_REQUESTED' capture_explicit=$CAPTURE_ROUTE_EXPLICIT capture_policy=$CAPTURE_ROUTE automatic_capture_order='mic headset-mic'" log_info "Retaining loopback artifacts in $LOGDIR" if [ -n "$PLAYBACK_DEVICE" ] || [ -n "$CAPTURE_DEVICE" ]; then @@ -286,22 +373,46 @@ case "$AUDIO_BACKEND" in [ -z "$CAPTURE_CLIENT" ] || ! command -v wpctl >/dev/null 2>&1; then MANAGED_BACKEND_FAILURE="PipeWire is active but its playback, recording, or control client is unavailable" + MANAGED_BACKEND_APPLICABLE_FAILURE=1 AUDIO_BACKEND="" else - PLAYBACK_TARGET="$( + playback_route_record="$( audio_run_helper_as_test_user \ --require-session \ - pw_default_speakers 2>/dev/null || true + audio_select_managed_playback_route \ + pipewire "$PLAYBACK_ROUTE" 2>/dev/null || true )" - CAPTURE_TARGET="$( + capture_route_record="$( audio_run_helper_as_test_user \ --require-session \ - pw_default_mic 2>/dev/null || true + audio_select_managed_capture_route \ + pipewire "$CAPTURE_ROUTE" 2>/dev/null || true + )" + PLAYBACK_ROUTE="$( + printf '%s\n' "$playback_route_record" | cut -d'|' -f1 + )" + PLAYBACK_TARGET="$( + printf '%s\n' "$playback_route_record" | cut -d'|' -f2- + )" + CAPTURE_ROUTE="$( + printf '%s\n' "$capture_route_record" | cut -d'|' -f1 + )" + CAPTURE_TARGET="$( + printf '%s\n' "$capture_route_record" | cut -d'|' -f2- )" if [ -z "$PLAYBACK_TARGET" ] || [ -z "$CAPTURE_TARGET" ]; then - MANAGED_BACKEND_FAILURE="PipeWire is active but a physical speaker sink or microphone source was not discovered" + PLAYBACK_ROUTE="$PLAYBACK_ROUTE_POLICY" + CAPTURE_ROUTE="$CAPTURE_ROUTE_POLICY" + SINK_CHOICE="$PLAYBACK_ROUTE" + SRC_CHOICE="$CAPTURE_ROUTE" + export SINK_CHOICE SRC_CHOICE + MANAGED_BACKEND_FAILURE="PipeWire is active but no usable requested or automatically selected playback/capture route pair was discovered" AUDIO_BACKEND="" + else + SINK_CHOICE="$PLAYBACK_ROUTE" + SRC_CHOICE="$CAPTURE_ROUTE" + export SINK_CHOICE SRC_CHOICE fi fi ;; @@ -311,23 +422,47 @@ case "$AUDIO_BACKEND" in command -v pactl >/dev/null 2>&1; then PLAYBACK_CLIENT="paplay" CAPTURE_CLIENT="parecord" - PLAYBACK_TARGET="$( + playback_route_record="$( audio_run_helper_as_test_user \ --require-session \ - pa_default_speakers 2>/dev/null || true + audio_select_managed_playback_route \ + pulseaudio "$PLAYBACK_ROUTE" 2>/dev/null || true )" - CAPTURE_TARGET="$( + capture_route_record="$( audio_run_helper_as_test_user \ --require-session \ - pa_default_mic 2>/dev/null || true + audio_select_managed_capture_route \ + pulseaudio "$CAPTURE_ROUTE" 2>/dev/null || true + )" + PLAYBACK_ROUTE="$( + printf '%s\n' "$playback_route_record" | cut -d'|' -f1 + )" + PLAYBACK_TARGET="$( + printf '%s\n' "$playback_route_record" | cut -d'|' -f2- + )" + CAPTURE_ROUTE="$( + printf '%s\n' "$capture_route_record" | cut -d'|' -f1 + )" + CAPTURE_TARGET="$( + printf '%s\n' "$capture_route_record" | cut -d'|' -f2- )" if [ -z "$PLAYBACK_TARGET" ] || [ -z "$CAPTURE_TARGET" ]; then - MANAGED_BACKEND_FAILURE="PulseAudio is active but a speaker sink or microphone source was not discovered" + PLAYBACK_ROUTE="$PLAYBACK_ROUTE_POLICY" + CAPTURE_ROUTE="$CAPTURE_ROUTE_POLICY" + SINK_CHOICE="$PLAYBACK_ROUTE" + SRC_CHOICE="$CAPTURE_ROUTE" + export SINK_CHOICE SRC_CHOICE + MANAGED_BACKEND_FAILURE="PulseAudio is active but no usable requested or automatically selected playback/capture route pair was discovered" AUDIO_BACKEND="" + else + SINK_CHOICE="$PLAYBACK_ROUTE" + SRC_CHOICE="$CAPTURE_ROUTE" + export SINK_CHOICE SRC_CHOICE fi else MANAGED_BACKEND_FAILURE="PulseAudio is active but paplay, parecord, or pactl is unavailable" + MANAGED_BACKEND_APPLICABLE_FAILURE=1 AUDIO_BACKEND="" fi ;; @@ -335,6 +470,7 @@ case "$AUDIO_BACKEND" in ;; *) MANAGED_BACKEND_FAILURE="unsupported detected audio backend '$AUDIO_BACKEND'" + MANAGED_BACKEND_APPLICABLE_FAILURE=1 AUDIO_BACKEND="" ;; esac @@ -346,20 +482,26 @@ if [ -z "$AUDIO_BACKEND" ]; then if command -v aplay >/dev/null 2>&1 && command -v arecord >/dev/null 2>&1 && - audio_playback_probe_alsa_with_recovery; then + audio_playback_probe_selected_alsa_with_recovery \ + "$PLAYBACK_DEVICE"; then + PLAYBACK_ROUTE="$SINK_CHOICE" AUDIO_BACKEND="alsa" fi fi if [ -z "$AUDIO_BACKEND" ]; then - if [ -n "$MANAGED_BACKEND_FAILURE" ]; then + if [ "$PLAYBACK_ROUTE_EXPLICIT" -eq 1 ] || + [ "$CAPTURE_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$PLAYBACK_DEVICE" ] || + [ -n "$CAPTURE_DEVICE" ] || + [ "$MANAGED_BACKEND_APPLICABLE_FAILURE" -eq 1 ]; then log_file_with_label \ "ALSA-PROBE" "$AUDIO_ALSA_PLAYBACK_PROBE_LOG" 40 test_result_record "FAIL" \ - "$TESTNAME FAIL - $MANAGED_BACKEND_FAILURE and no direct ALSA loopback path is usable, verify sound-card registration, topology, UCM, mixer routing, and image audio clients" + "$TESTNAME FAIL - requested or applicable loopback routes are unavailable, playback_requested='$PLAYBACK_ROUTE_REQUESTED' capture_requested='$CAPTURE_ROUTE_REQUESTED' playback_device='${PLAYBACK_DEVICE:-auto}' capture_device='${CAPTURE_DEVICE:-auto}' managed_reason='${MANAGED_BACKEND_FAILURE:-none}' ALSA_reason='${AUDIO_ALSA_PLAYBACK_REASON:-playback probe failed}', verify sound-card registration, topology, UCM, mixer routing, and image audio clients" else test_result_record "SKIP" \ - "$TESTNAME SKIP - no active managed audio backend or usable direct ALSA loopback path was discovered" + "$TESTNAME SKIP - automatic discovery found no usable speaker/headphones and mic/headset-mic loopback route pair on managed audio or direct ALSA" fi test_result_finish fi @@ -372,37 +514,117 @@ if [ "$AUDIO_BACKEND" = "alsa" ]; then test_result_finish fi - if [ -z "$PLAYBACK_DEVICE" ]; then - if [ -z "${AUDIO_ALSA_PLAYBACK_DEVICE:-}" ] && - ! audio_playback_probe_alsa_with_recovery; then - if [ -n "$MANAGED_BACKEND_FAILURE" ]; then + if [ -z "${AUDIO_ALSA_PLAYBACK_DEVICE:-}" ] || + [ -n "$PLAYBACK_DEVICE" ]; then + if ! audio_playback_probe_selected_alsa_with_recovery \ + "$PLAYBACK_DEVICE"; then + if [ "$PLAYBACK_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$PLAYBACK_DEVICE" ] || + [ "$MANAGED_BACKEND_APPLICABLE_FAILURE" -eq 1 ]; then log_file_with_label \ "ALSA-PROBE" "$AUDIO_ALSA_PLAYBACK_PROBE_LOG" 40 test_result_record "FAIL" \ - "$TESTNAME FAIL - $MANAGED_BACKEND_FAILURE and no direct ALSA playback device could be opened, verify sound-card registration, topology, UCM, mixer routing, and image audio clients" + "$TESTNAME FAIL - requested playback route/device is unavailable, route='$PLAYBACK_ROUTE_REQUESTED' device='${PLAYBACK_DEVICE:-auto}' reason=${AUDIO_ALSA_PLAYBACK_REASON:-probe failed}, verify sound-card registration, topology, UCM, mixer routing, and image audio clients" else test_result_record "SKIP" \ - "$TESTNAME SKIP - no managed audio backend or direct ALSA playback device is available" + "$TESTNAME SKIP - automatic playback discovery found no usable speakers or headphones ALSA route" fi test_result_finish fi - PLAYBACK_DEVICE="$AUDIO_ALSA_PLAYBACK_DEVICE" fi + PLAYBACK_ROUTE="$SINK_CHOICE" + PLAYBACK_DEVICE="$AUDIO_ALSA_PLAYBACK_DEVICE" requested_capture_device="$CAPTURE_DEVICE" - if ! audio_record_probe_alsa_capture_profile "$requested_capture_device"; then - if [ -n "$MANAGED_BACKEND_FAILURE" ]; then + if ! audio_record_probe_selected_alsa_capture_profile \ + "$requested_capture_device"; then + if [ "$CAPTURE_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$requested_capture_device" ] || + [ "$MANAGED_BACKEND_APPLICABLE_FAILURE" -eq 1 ]; then test_result_record "FAIL" \ - "$TESTNAME FAIL - $MANAGED_BACKEND_FAILURE and no direct ALSA capture profile could be opened, reason=${AUDIO_ALSA_CAPTURE_REASON:-unknown}, verify the microphone route and image audio clients" + "$TESTNAME FAIL - requested capture route/device is unavailable, route='$CAPTURE_ROUTE_REQUESTED' device='${requested_capture_device:-auto}' reason=${AUDIO_ALSA_CAPTURE_REASON:-unknown}, verify the microphone route, UCM data, and image audio clients" else test_result_record "SKIP" \ - "$TESTNAME SKIP - no ALSA capture profile could be opened, device=${requested_capture_device:-auto} reason=${AUDIO_ALSA_CAPTURE_REASON:-unknown}, connect a loopback fixture or pass --capture-device" + "$TESTNAME SKIP - automatic capture discovery found no usable mic or headset-mic ALSA route, reason=${AUDIO_ALSA_CAPTURE_REASON:-unknown}, connect a loopback fixture or pass an available explicit route/device" fi test_result_finish fi + CAPTURE_ROUTE="$SRC_CHOICE" + + playback_route_record="$( + audio_playback_resolve_alsa_route "$PLAYBACK_ROUTE" 2>/dev/null || true + )" + playback_ucm_pcm="$( + printf '%s\n' "$playback_route_record" | cut -d'|' -f1 + )" + playback_ucm_card="$( + printf '%s\n' "$playback_route_record" | cut -d'|' -f2 + )" + playback_ucm_device="$( + printf '%s\n' "$playback_route_record" | cut -d'|' -f3 + )" + capture_route_record="$( + audio_record_resolve_alsa_route "$CAPTURE_ROUTE" 2>/dev/null || true + )" + capture_ucm_card="$( + printf '%s\n' "$capture_route_record" | cut -d'|' -f2 + )" + capture_ucm_device="$( + printf '%s\n' "$capture_route_record" | cut -d'|' -f3 + )" + + if [ -n "$playback_ucm_card" ] && + [ "$playback_ucm_card" = "$capture_ucm_card" ] && + [ -n "$playback_ucm_device" ] && + [ -n "$capture_ucm_device" ]; then + if ! audio_alsa_enable_ucm_hifi_devices \ + "$playback_ucm_card" \ + "$playback_ucm_device" \ + "$capture_ucm_device"; then + test_result_record "FAIL" \ + "$TESTNAME FAIL - could not enable the $PLAYBACK_ROUTE and $CAPTURE_ROUTE UCM devices together on card '$playback_ucm_card'" + test_result_finish + fi + elif [ -n "$playback_ucm_card" ]; then + if ! audio_playback_alsa_prepare \ + "$(audio_alsa_device_card "$PLAYBACK_DEVICE")" \ + "$playback_ucm_card" "$playback_ucm_device"; then + test_result_record "FAIL" \ + "$TESTNAME FAIL - could not prepare the $PLAYBACK_ROUTE ALSA playback route" + test_result_finish + fi + + if ! audio_record_alsa_prepare_capture \ + "$(audio_alsa_device_card "$AUDIO_ALSA_CAPTURE_DEVICE")" \ + "$capture_ucm_card" "$capture_ucm_device"; then + test_result_record "FAIL" \ + "$TESTNAME FAIL - could not prepare the $CAPTURE_ROUTE ALSA capture route" + test_result_finish + fi + else + if ! audio_record_alsa_prepare_capture \ + "$(audio_alsa_device_card "$AUDIO_ALSA_CAPTURE_DEVICE")" \ + "$capture_ucm_card" "$capture_ucm_device"; then + test_result_record "FAIL" \ + "$TESTNAME FAIL - could not prepare the $CAPTURE_ROUTE ALSA capture route" + test_result_finish + fi + + if ! audio_playback_alsa_prepare \ + "$(audio_alsa_device_card "$PLAYBACK_DEVICE")" \ + "$playback_ucm_card" "$playback_ucm_device"; then + test_result_record "FAIL" \ + "$TESTNAME FAIL - could not prepare the $PLAYBACK_ROUTE ALSA playback route" + test_result_finish + fi + fi CAPTURE_DEVICE="$AUDIO_ALSA_CAPTURE_DEVICE" - PLAYBACK_TARGET="$PLAYBACK_DEVICE" + PLAYBACK_TARGET="$( + audio_playback_effective_alsa_device \ + "$PLAYBACK_DEVICE" \ + "$PLAYBACK_DEVICE_SELECTION" + )" CAPTURE_TARGET="$CAPTURE_DEVICE" PLAYBACK_CLIENT="aplay" CAPTURE_CLIENT="arecord" @@ -442,7 +664,22 @@ else fi log_info "Using audio backend: $AUDIO_BACKEND" -log_info "Loopback route: playback=$PLAYBACK_TARGET capture=$CAPTURE_TARGET" +if [ "$AUDIO_BACKEND" = "alsa" ] && + [ -n "${playback_ucm_card:-}" ] && + [ -n "${playback_ucm_device:-}" ] && + [ -n "${capture_ucm_card:-}" ] && + [ -n "${capture_ucm_device:-}" ]; then + log_info "Loopback ALSA route: playback_choice=$PLAYBACK_ROUTE ucm_pcm='$playback_ucm_pcm' playback_target=$PLAYBACK_TARGET playback_selection=$PLAYBACK_DEVICE_SELECTION playback_ucm_card='$playback_ucm_card' playback_ucm_device='$playback_ucm_device' capture_choice=$CAPTURE_ROUTE capture_device=$CAPTURE_TARGET capture_ucm_card='$capture_ucm_card' capture_ucm_device='$capture_ucm_device'" +elif [ "$AUDIO_BACKEND" = "alsa" ]; then + log_info "Loopback ALSA route: playback_target=$PLAYBACK_TARGET playback_requested_choice=$PLAYBACK_ROUTE playback_selection=$PLAYBACK_DEVICE_SELECTION capture_device=$CAPTURE_TARGET capture_requested_choice=$CAPTURE_ROUTE selection=legacy-inventory-fallback" +else + log_info "Loopback route: playback=$PLAYBACK_TARGET capture=$CAPTURE_TARGET" +fi +CAPTURE_DEVICE_SELECTION="auto" +if [ -n "$requested_capture_device" ]; then + CAPTURE_DEVICE_SELECTION="explicit" +fi +log_info "AUDIO_ROUTE scope=loopback backend=$AUDIO_BACKEND playback_requested='$PLAYBACK_ROUTE_REQUESTED' playback_explicit=$PLAYBACK_ROUTE_EXPLICIT playback_canonical=$PLAYBACK_ROUTE playback_target='$PLAYBACK_TARGET' playback_device_selection=$PLAYBACK_DEVICE_SELECTION capture_requested='$CAPTURE_ROUTE_REQUESTED' capture_explicit=$CAPTURE_ROUTE_EXPLICIT capture_canonical=$CAPTURE_ROUTE capture_target='$CAPTURE_TARGET' capture_device_selection=$CAPTURE_DEVICE_SELECTION" reference_wav="$LOGDIR/reference_48KHz_${DURATION}s_8b_2ch.wav" reference_log="$LOGDIR/reference_validation.log" @@ -468,7 +705,8 @@ if ! audio_validate_wav_file \ 8 \ "$DURATION" \ "$reference_log" \ - playback; then + playback \ + 2; then test_result_record "FAIL" \ "$TESTNAME FAIL - generated reference failed basic-integrity validation, validation='${AUDIO_VALIDATION_SUMMARY:-unavailable}', artifact=$reference_log" test_result_finish diff --git a/Runner/suites/Multimedia/Audio/AudioPlayback/Read_me.md b/Runner/suites/Multimedia/Audio/AudioPlayback/Read_me.md index 159c1d97..74eef5db 100644 --- a/Runner/suites/Multimedia/Audio/AudioPlayback/Read_me.md +++ b/Runner/suites/Multimedia/Audio/AudioPlayback/Read_me.md @@ -29,7 +29,8 @@ empty WAV files, header-only payloads, all-zero audio, materially short clips, or filename metadata that disagrees with the WAV header. RMS, peak, clipping, digital-silence runs, DC offset, large sample transitions, and silent-channel counts are reported as diagnostic `AUDIO_VALIDATION` metrics and do not fail the -default policy. +default policy. Complete RIFF chunks after the audio `data` chunk are reported +as `post_data_chunks` metadata and are not misclassified as trailing audio. - Validates playback using multiple evidence sources: - PipeWire/PulseAudio streaming state - ALSA and ASoC runtime status @@ -152,16 +153,26 @@ AUDIO_PACKAGE_PROFILE=desktop AUDIO_PACKAGE_UPDATE=1 \ ## Backend and Route Selection -When no backend is requested, the suite uses automatic selection. A physical PipeWire audio sink uses `pw-play` or `pw-cat --playback`, and a physical PulseAudio sink uses `paplay`. For the `speakers` route, a speaker endpoint takes precedence over headphones, then other physical outputs. Dummy, null, monitor, and loopback PipeWire sinks are not accepted as speaker routes. +When no route is requested, `auto` discovers speakers first and then headphones on the running target. A physical PipeWire audio sink uses `pw-play` or `pw-cat --playback`, and a physical PulseAudio sink uses `paplay`. `speaker` and `speakers` explicitly select the speaker route. `headphone`, `headphones`, and `headset` explicitly select the headphones route. A generic physical output remains a compatibility fallback for automatic or explicit speaker selection, but an explicitly named headphones endpoint is never used as speakers. Dummy, null, monitor, and loopback PipeWire sinks are not accepted as physical routes. `--sink auto` also restores automatic route selection. The selected backend, request source, explicitness, requested alias, canonical route, runtime target, and UCM device are emitted to standard output in an `AUDIO_ROUTE` record. -If automatic selection finds no physical managed speaker sink, the suite probes direct ALSA playback. It selects an ALSA card and PCM from the available device inventory, applies only mixer controls exposed by that card, and runs `aplay -D `. This supports the Shikra primary-MI2S, secondary-TDM, and codec-direct route capabilities without selecting a form factor or assuming card `0`. When the audio remoteproc preflight proves that audio is applicable, failure of both managed-sink discovery and direct ALSA probing is reported as FAIL so the image or runtime regression remains tracked. The same absence remains SKIP only when runtime preflight found no applicable audio subsystem. +If automatic selection finds no matching managed sink, the suite probes direct ALSA speakers and then headphones. UCM discovery enumerates every card and HiFi device, retrieves `PlaybackPCM`, enables the exact device, verifies `_enadevs`, and functionally opens the resulting PCM. An explicit semantic route is never replaced by another route. An explicit route that cannot be discovered and opened reports FAIL, while automatic discovery with no applicable physical output reports SKIP. The remaining UCM namespace, `plughw:` conversion, legacy speaker fallback, and audio-remoteproc applicability rules remain unchanged. An explicit backend request is never replaced: -- `--backend pipewire` or `AUDIO_BACKEND=pipewire` runs `pw-play` or `pw-cat --playback`. A missing optional client can skip, but a ready backend with no requested physical sink fails. +- `--backend pipewire` or `AUDIO_BACKEND=pipewire` runs `pw-play` or `pw-cat --playback`. A client absent from the image remains an optional-capability SKIP for compatibility, but a ready backend with no requested physical sink fails. - `--backend pulseaudio` or `AUDIO_BACKEND=pulseaudio` runs `paplay` only. A ready backend with no requested sink fails. - `--backend alsa` or `AUDIO_BACKEND=alsa` runs `aplay` with the discovered ALSA route. +Existing Yocto/LAVA YAML definitions keep their `SINK_CHOICE=speakers` contract, +which is treated as an explicit speaker validation. Override that parameter with +`auto` when a job should choose speakers or headphones from runtime evidence. + +PulseAudio discovery rejects null, dummy, monitor, loopback, and freewheel +endpoints before selecting a physical route. It uses the endpoint name, +description, form factor, and active port to distinguish speakers from +headphones while preserving a generic physical sink as the compatibility +fallback on older distributions with sparse metadata. + ## Audio Remoteproc Preflight Before backend discovery, the suite enumerates runtime remoteprocs and checks the configured firmware and topology under `/lib/firmware` or `/usr/lib/firmware`. It recognizes audio DSP identities and the Shikra modem-hosted audio path. A remoteproc named `modem` is ignored on non-Shikra targets because it is not their audio preflight dependency. @@ -286,8 +297,14 @@ cd Runner/suites/Multimedia/Audio/AudioPlayback # Show usage/help ./run.sh --help -# Run with PipeWire, 3 loops, 10s timeout, speakers sink -./run.sh --backend pipewire --sink speakers --loops 3 --timeout 10s +# Run with PipeWire, 3 loops, 10s timeout, speakers sink +./run.sh --backend pipewire --sink speakers --loops 3 --timeout 10s + +# Select headphones using the runtime PipeWire endpoint +./run.sh --backend pipewire --sink headphones --loops 1 + +# Select headphones through the dynamically discovered UCM card and PlaybackPCM +./run.sh --backend alsa --sink headphones --loops 1 # Run with PulseAudio, null sink, strict mode, verbose ./run.sh --backend pulseaudio --sink null --strict --verbose @@ -315,7 +332,7 @@ cd Runner/suites/Multimedia/Audio/AudioPlayback Environment Variables: Variable Description Default AUDIO_BACKEND Selects backend: pipewire, pulseaudio, or alsa auto-detect -SINK_CHOICE Playback sink: speakers or null speakers +SINK_CHOICE Playback sink: auto, speakers, headphones, or null auto CLIP_NAMES Test specific clips (e.g., "playback_config1 playback_config2") playback_config1 CLIP_FILTER Filter clips by pattern (e.g., "48KHz" or "16b" or "2ch") unset FORMATS Audio formats: e.g. wav wav @@ -342,7 +359,7 @@ LAVA_TESTCASE_ID Unique testcase ID written into the .res file for LAVA CLI Options Option Description --backend Select backend: pipewire, pulseaudio, or alsa ---sink Playback sink: speakers or null +--sink Playback sink: auto, speaker(s), headphone(s), headset, or null --clip-name Test specific clips using playback_config1-playback_config10 or descriptive names (space-separated) --clip-filter Filter clips by sample rate, bit rate, or channels (space-separated patterns) --formats Audio formats (space/comma separated): e.g. wav diff --git a/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh b/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh index d1c47434..fc11c990 100755 --- a/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh +++ b/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh @@ -101,7 +101,14 @@ export AUDIO_TAR_URL # ------------- Defaults / CLI ------------- AUDIO_BACKEND="${AUDIO_BACKEND:-}" -SINK_CHOICE="${SINK_CHOICE:-speakers}" # speakers|null +if [ "${SINK_CHOICE+x}" = x ]; then + AUDIO_PLAYBACK_ROUTE_EXPLICIT=1 + AUDIO_PLAYBACK_ROUTE_SOURCE="environment" +else + SINK_CHOICE="auto" + AUDIO_PLAYBACK_ROUTE_EXPLICIT=0 + AUDIO_PLAYBACK_ROUTE_SOURCE="automatic" +fi 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}" @@ -149,7 +156,7 @@ usage() { cat <"$AUDIO_ALSA_PLAYBACK_PROBE_LOG"; then exit 1 fi -while [ $# -gt 0 ]; do - case "$1" in +# Parse playback arguments after privileged package preparation. +parse_args() { + while [ "$#" -gt 0 ]; do + case "$1" in --overlay) AUDIO_OVERLAY_REQUESTED=1 export AUDIO_OVERLAY_REQUESTED @@ -338,6 +347,8 @@ while [ $# -gt 0 ]; do ;; --sink) SINK_CHOICE="$2" + AUDIO_PLAYBACK_ROUTE_EXPLICIT=1 + AUDIO_PLAYBACK_ROUTE_SOURCE="command-line" shift 2 ;; --formats) @@ -436,8 +447,24 @@ while [ $# -gt 0 ]; do log_warn "Unknown option: $1" shift ;; - esac -done + esac + done +} + +parse_args "$@" + +SINK_REQUESTED="$SINK_CHOICE" +SINK_CHOICE="$( + audio_normalize_playback_route "$SINK_REQUESTED" 2>/dev/null +)" || { + log_fail "$TESTNAME FAIL - invalid sink '$SINK_REQUESTED', expected auto, speaker, speakers, headphone, headphones, headset, or null" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 +} +if [ "$SINK_CHOICE" = "auto" ]; then + AUDIO_PLAYBACK_ROUTE_EXPLICIT=0 +fi +export SINK_CHOICE AUDIO_PLAYBACK_ROUTE_EXPLICIT AUDIO_PLAYBACK_ROUTE_SOURCE # Preserve an explicit backend request. Automatic fallback is only appropriate # when backend selection was left in auto mode. @@ -496,12 +523,17 @@ esac # Desktop Ubuntu images run PipeWire and the PulseAudio compatibility server in # the logged-in user's session. The test itself is often launched by root, so # command-level backend probes and players must join that session rather than -# querying root's empty runtime directory. audio_run_as_test_user discovers the -# owner dynamically, which avoids hardcoding a desktop username. -AUDIO_USE_DESKTOP_SESSION=0 -if [ "$AUDIO_PLAYBACK_OS_ID" = "ubuntu" ] && - [ "$(id -u 2>/dev/null || echo 1)" -eq 0 ]; then - AUDIO_USE_DESKTOP_SESSION=1 +# querying root's empty runtime directory. Headless Ubuntu Server has no such +# runtime evidence and retains direct root ALSA execution. +if ! command -v audio_configure_ubuntu_desktop_session >/dev/null 2>&1; then + log_fail "$TESTNAME FAIL - required helper is unavailable: audio_configure_ubuntu_desktop_session" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 +fi + +audio_configure_ubuntu_desktop_session "$AUDIO_PLAYBACK_OS_ID" +if [ "${AUDIO_USE_DESKTOP_SESSION:-0}" -eq 1 ]; then + AUDIO_PLAYBACK_DESKTOP_ROOT_MODE=1 fi export AUDIO_PLAYBACK_DESKTOP_ROOT_MODE AUDIO_USE_DESKTOP_SESSION @@ -664,6 +696,7 @@ if [ -n "$AUDIO_CLIPS_BASE_DIR" ]; then fi log_info "Args: backend=${AUDIO_BACKEND:-auto} sink=$SINK_CHOICE overlay=$AUDIO_OVERLAY_REQUESTED volume=$AUDIO_PLAYBACK_VOLUME 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}" +log_info "AUDIO_ROUTE_DISCOVERY scope=playback requested='$SINK_REQUESTED' request_source=$AUDIO_PLAYBACK_ROUTE_SOURCE explicit=$AUDIO_PLAYBACK_ROUTE_EXPLICIT policy=$SINK_CHOICE automatic_order='speakers headphones'" if ! command -v audio_prepare_backend_client_packages >/dev/null 2>&1; then log_fail "$TESTNAME FAIL - required helper is unavailable: audio_prepare_backend_client_packages" @@ -814,7 +847,7 @@ fi export AUDIO_SYSTEMD_MANAGED if [ -z "$AUDIO_BACKEND" ]; then - if audio_playback_probe_alsa_with_recovery; then + if audio_playback_probe_selected_alsa_with_recovery; then AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 export AUDIO_SYSTEMD_MANAGED @@ -822,19 +855,33 @@ if [ -z "$AUDIO_BACKEND" ]; then elif audio_playback_bootstrap_backend_if_needed; then AUDIO_BACKEND="$(audio_run_helper_as_test_user --require-session detect_audio_backend 2>/dev/null || echo "")" if [ -z "$AUDIO_BACKEND" ]; then - if audio_playback_probe_alsa_with_recovery; then + if audio_playback_probe_selected_alsa_with_recovery; 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" + if [ "$AUDIO_PLAYBACK_ROUTE_EXPLICIT" -eq 1 ] || + [ "$audio_remoteproc_rc" -eq 0 ] 2>/dev/null; then + log_fail "$TESTNAME FAIL - requested or applicable playback route '$SINK_REQUESTED' is unavailable, no managed backend is running and direct ALSA discovery failed: ${AUDIO_ALSA_PLAYBACK_REASON:-probe failed}" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + log_skip "$TESTNAME SKIP - automatic playback discovery found no running managed backend or usable ALSA speakers/headphones route" echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" exit 0 fi fi else - log_skip "$TESTNAME SKIP - no audio backend running" + if [ "$AUDIO_PLAYBACK_ROUTE_EXPLICIT" -eq 1 ] || + [ "$audio_remoteproc_rc" -eq 0 ] 2>/dev/null; then + log_fail "$TESTNAME FAIL - requested or applicable playback route '$SINK_REQUESTED' is unavailable, backend bootstrap failed and direct ALSA discovery found no matching route: ${AUDIO_ALSA_PLAYBACK_REASON:-probe failed}" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + log_skip "$TESTNAME SKIP - automatic playback discovery found no running managed backend or usable ALSA speakers/headphones route" echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" exit 0 fi @@ -860,7 +907,7 @@ fi backend_ok=0 if [ "$AUDIO_BACKEND" = "alsa" ]; then - if audio_playback_probe_alsa_with_recovery; then + if audio_playback_probe_selected_alsa_with_recovery; then backend_ok=1 fi else @@ -913,7 +960,7 @@ if [ "$backend_ok" -ne 1 ] && [ -z "$AUDIO_BACKEND_REQUESTED" ] && fi if [ "$backend_ok" -ne 1 ] && [ "$AUDIO_BACKEND" != "alsa" ]; then - if audio_playback_probe_alsa_with_recovery; then + if audio_playback_probe_selected_alsa_with_recovery; then log_warn "$TESTNAME: falling back to ALSA direct playback path" AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 @@ -923,7 +970,15 @@ if [ "$backend_ok" -ne 1 ] && [ "$AUDIO_BACKEND" != "alsa" ]; then fi if [ "$backend_ok" -ne 1 ]; then - log_skip "$TESTNAME SKIP - backend not available: $AUDIO_BACKEND" + if [ "$AUDIO_PLAYBACK_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ] || + [ "$audio_remoteproc_rc" -eq 0 ] 2>/dev/null; then + log_fail "$TESTNAME FAIL - requested or applicable playback route '$SINK_REQUESTED' could not be validated on backend '$AUDIO_BACKEND' or direct ALSA, reason=${AUDIO_ALSA_PLAYBACK_REASON:-backend unavailable}" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + log_skip "$TESTNAME SKIP - automatic playback discovery found no ready backend or usable ALSA speakers/headphones route" echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" exit 0 fi @@ -951,14 +1006,21 @@ case "$AUDIO_BACKEND" in pipewire) if [ -z "$AUDIO_PIPEWIRE_PLAY_COMMAND" ]; then if [ -z "$AUDIO_BACKEND_REQUESTED" ] && - audio_playback_probe_alsa_with_recovery && + audio_playback_probe_selected_alsa_with_recovery && command -v aplay >/dev/null 2>&1; 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 - PipeWire playback client is absent, provision the pipewire-utils image package providing pw-play or pw-cat" + if [ "$AUDIO_PLAYBACK_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ]; then + log_fail "$TESTNAME FAIL - requested sink '$SINK_REQUESTED' cannot be validated because the PipeWire playback client is absent and no matching direct ALSA route opened, provision pipewire-utils or the required ALSA/UCM route" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + log_skip "$TESTNAME SKIP - PipeWire playback client is absent and automatic ALSA discovery found no usable speakers/headphones route, provision pipewire-utils providing pw-play or pw-cat" echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" exit 0 fi @@ -971,14 +1033,21 @@ case "$AUDIO_BACKEND" in pulseaudio) if ! command -v paplay >/dev/null 2>&1; then if [ -z "$AUDIO_BACKEND_REQUESTED" ] && - audio_playback_probe_alsa_with_recovery && + audio_playback_probe_selected_alsa_with_recovery && command -v aplay >/dev/null 2>&1; 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: paplay" + if [ "$AUDIO_PLAYBACK_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ]; then + log_fail "$TESTNAME FAIL - requested sink '$SINK_REQUESTED' cannot be validated because paplay is absent and no matching direct ALSA route opened" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + log_skip "$TESTNAME SKIP - paplay is absent and automatic ALSA discovery found no usable speakers/headphones route" echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" exit 0 fi @@ -986,7 +1055,14 @@ case "$AUDIO_BACKEND" in ;; alsa) if ! command -v aplay >/dev/null 2>&1; then - log_skip "$TESTNAME SKIP - missing ALSA playback utility: aplay" + if [ "$AUDIO_PLAYBACK_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ]; then + log_fail "$TESTNAME FAIL - requested sink '$SINK_REQUESTED' cannot be validated because aplay is missing from the image" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + log_skip "$TESTNAME SKIP - automatic ALSA playback discovery requires image-provided aplay" echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" exit 0 fi @@ -1010,15 +1086,22 @@ if [ "$AUDIO_BACKEND" = "pipewire" ]; then if ! audio_run_helper_as_test_user --require-session audio_pw_ctl_ok 2>/dev/null; then if [ -z "$AUDIO_BACKEND_REQUESTED" ] && - audio_playback_probe_alsa_with_recovery && + audio_playback_probe_selected_alsa_with_recovery && command -v aplay >/dev/null 2>&1; 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 "$RESULT_TESTNAME SKIP" > "$RES_FILE" + if [ "$AUDIO_PLAYBACK_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ]; then + log_fail "$TESTNAME FAIL - requested sink '$SINK_REQUESTED' cannot be validated because the PipeWire control-plane is unavailable and no matching direct ALSA route opened" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + log_skip "$TESTNAME SKIP - PipeWire control-plane is unavailable and automatic ALSA discovery found no usable speakers/headphones route" + echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" exit 0 fi fi @@ -1035,15 +1118,22 @@ elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then if ! audio_run_helper_as_test_user --require-session audio_pa_ctl_ok 2>/dev/null; then if [ -z "$AUDIO_BACKEND_REQUESTED" ] && - audio_playback_probe_alsa_with_recovery && + audio_playback_probe_selected_alsa_with_recovery && command -v aplay >/dev/null 2>&1; 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 "$RESULT_TESTNAME SKIP" > "$RES_FILE" + if [ "$AUDIO_PLAYBACK_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ]; then + log_fail "$TESTNAME FAIL - requested sink '$SINK_REQUESTED' cannot be validated because the PulseAudio control-plane is unavailable and no matching direct ALSA route opened" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + log_skip "$TESTNAME SKIP - PulseAudio control-plane is unavailable and automatic ALSA discovery found no usable speakers/headphones route" + echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" exit 0 fi fi @@ -1052,41 +1142,64 @@ fi # ----- Route sink (set default; player uses default sink) ----- SINK_ID="" -case "$AUDIO_BACKEND:$SINK_CHOICE" in - pipewire:null) - SINK_ID="$(audio_run_helper_as_test_user --require-session pw_default_null)" - ;; - pipewire:*) - SINK_ID="$(audio_run_helper_as_test_user --require-session pw_default_speakers)" - ;; - pulseaudio:null) - SINK_ID="$(audio_run_helper_as_test_user --require-session pa_default_null)" - ;; - pulseaudio:*) - SINK_ID="$(audio_run_helper_as_test_user --require-session pa_default_speakers)" - ;; - alsa:null) - SINK_ID="null" +MANAGED_ROUTE_RECORD="" +ALSA_ROUTE_RECORD="" +ALSA_ROUTE_DEVICE="" +ALSA_ROUTE_UCM_CARD="" +ALSA_ROUTE_UCM_DEVICE="" +case "$AUDIO_BACKEND" in + pipewire|pulseaudio) + MANAGED_ROUTE_RECORD="$( + audio_run_helper_as_test_user \ + --require-session \ + audio_select_managed_playback_route \ + "$AUDIO_BACKEND" "$SINK_CHOICE" 2>/dev/null || true + )" + if [ -n "$MANAGED_ROUTE_RECORD" ]; then + SINK_CHOICE="$( + printf '%s\n' "$MANAGED_ROUTE_RECORD" | cut -d'|' -f1 + )" + SINK_ID="$( + printf '%s\n' "$MANAGED_ROUTE_RECORD" | cut -d'|' -f2- + )" + export SINK_CHOICE + fi ;; - alsa:*) - if [ -n "${AUDIO_ALSA_PLAYBACK_DEVICE:-}" ]; then - SINK_ID="$AUDIO_ALSA_PLAYBACK_DEVICE" + alsa) + if [ "$SINK_CHOICE" = "null" ]; then + SINK_ID="null" + elif [ -z "${AUDIO_ALSA_PLAYBACK_DEVICE:-}" ] && + ! audio_playback_probe_selected_alsa_with_recovery; then + SINK_ID="" else - SINK_ID="$(audio_playback_pick_alsa_sink)" + SINK_ID="$AUDIO_ALSA_PLAYBACK_DEVICE" fi - audio_playback_alsa_prepare \ - "$(audio_alsa_device_card "$SINK_ID")" >/dev/null 2>&1 || true + if [ "$SINK_CHOICE" != "null" ]; then + ALSA_ROUTE_RECORD="$(audio_playback_resolve_alsa_route "$SINK_CHOICE")" + ALSA_ROUTE_DEVICE="$(printf '%s\n' "$ALSA_ROUTE_RECORD" | cut -d'|' -f1)" + ALSA_ROUTE_UCM_CARD="$(printf '%s\n' "$ALSA_ROUTE_RECORD" | cut -d'|' -f2)" + ALSA_ROUTE_UCM_DEVICE="$(printf '%s\n' "$ALSA_ROUTE_RECORD" | cut -d'|' -f3)" + + if [ -n "$SINK_ID" ] && + ! audio_playback_alsa_prepare \ + "$(audio_alsa_device_card "$SINK_ID")" \ + "$ALSA_ROUTE_UCM_CARD" "$ALSA_ROUTE_UCM_DEVICE"; then + log_fail "$TESTNAME FAIL - discovered ALSA $SINK_CHOICE route '$SINK_ID' but could not apply its runtime UCM or mixer configuration" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + fi ;; esac if [ -z "$SINK_ID" ] && [ -z "$AUDIO_BACKEND_REQUESTED" ] && - [ "$SINK_CHOICE" = "speakers" ] && + [ "$SINK_CHOICE" != "null" ] && { [ "$AUDIO_BACKEND" = "pipewire" ] || [ "$AUDIO_BACKEND" = "pulseaudio" ]; } then - log_warn "$TESTNAME: no physical $AUDIO_BACKEND speaker sink found, probing direct ALSA playback" + log_warn "$TESTNAME: no physical $AUDIO_BACKEND $SINK_CHOICE sink found, probing direct ALSA playback" - if audio_playback_probe_alsa_with_recovery; then + if audio_playback_probe_selected_alsa_with_recovery; then AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 export AUDIO_SYSTEMD_MANAGED @@ -1094,40 +1207,57 @@ then SINK_ID="$AUDIO_ALSA_PLAYBACK_DEVICE" log_warn "$TESTNAME: falling back to direct ALSA playback device: $SINK_ID" else - if [ "$audio_remoteproc_rc" -eq 0 ] 2>/dev/null; then + if [ "$AUDIO_PLAYBACK_ROUTE_EXPLICIT" -eq 1 ] || + [ "$audio_remoteproc_rc" -eq 0 ] 2>/dev/null; then log_warn "$TESTNAME: ALSA playback probe evidence follows, artifact=$AUDIO_ALSA_PLAYBACK_PROBE_LOG" log_file_with_label \ "ALSA-PROBE" "$AUDIO_ALSA_PLAYBACK_PROBE_LOG" 40 - log_fail "$TESTNAME FAIL - audio runtime is applicable but no physical $AUDIO_BACKEND speaker sink was discovered and the direct ALSA playback probe failed, verify sound-card registration, topology, UCM, mixer routing, and image audio packages" + log_fail "$TESTNAME FAIL - requested or applicable playback route '$SINK_REQUESTED' is unavailable on $AUDIO_BACKEND and direct ALSA, reason=${AUDIO_ALSA_PLAYBACK_REASON:-probe failed}, verify sound-card registration, topology, the requested UCM device, mixer routing, and image audio packages" echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" exit 1 fi - log_skip "$TESTNAME SKIP - no physical $AUDIO_BACKEND speaker sink or direct ALSA playback path was discovered, audio remoteproc preflight was not applicable" + log_skip "$TESTNAME SKIP - no physical $AUDIO_BACKEND $SINK_CHOICE sink or matching direct ALSA playback path was discovered, audio remoteproc preflight was not applicable" echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" exit 0 fi fi if [ -z "$SINK_ID" ]; then - if [ -n "$AUDIO_BACKEND_REQUESTED" ] || + if [ "$AUDIO_PLAYBACK_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ] || [ "$audio_remoteproc_rc" -eq 0 ] 2>/dev/null; then - log_fail "$TESTNAME FAIL - requested sink '$SINK_CHOICE' is unavailable for ready backend '$AUDIO_BACKEND', verify the selected backend route and image audio configuration" + log_fail "$TESTNAME FAIL - requested sink '$SINK_REQUESTED' is unavailable for ready backend '$AUDIO_BACKEND', discovered_route='${SINK_CHOICE:-none}', verify the selected backend route, UCM data, and image audio configuration" echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" exit 1 fi - log_skip "$TESTNAME SKIP - no applicable sink '$SINK_CHOICE' was discovered for backend '$AUDIO_BACKEND'" + log_skip "$TESTNAME SKIP - automatic playback discovery found no usable speakers or headphones route for backend '$AUDIO_BACKEND', provide --sink only when that route is physically available" echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" exit 0 fi +if [ "$AUDIO_BACKEND" = "alsa" ] && [ "$SINK_CHOICE" != "null" ]; then + ALSA_ROUTE_RECORD="$( + audio_playback_resolve_alsa_route "$SINK_CHOICE" 2>/dev/null || true + )" + ALSA_ROUTE_DEVICE="$( + printf '%s\n' "$ALSA_ROUTE_RECORD" | cut -d'|' -f1 + )" + ALSA_ROUTE_UCM_CARD="$( + printf '%s\n' "$ALSA_ROUTE_RECORD" | cut -d'|' -f2 + )" + ALSA_ROUTE_UCM_DEVICE="$( + printf '%s\n' "$ALSA_ROUTE_RECORD" | cut -d'|' -f3 + )" +fi + if [ "$AUDIO_BACKEND" = "pipewire" ]; then SINK_NAME="$(audio_run_helper_as_test_user --require-session pw_sink_name_safe "$SINK_ID")" audio_run_helper_as_test_user --require-session pw_set_default_sink "$SINK_ID" >/dev/null 2>&1 || log_warn "Could not set PipeWire default sink id=$SINK_ID" - # Apply speaker volume to the discovered sink only. Null sinks remain + # Apply volume to the discovered physical sink only. Null sinks remain # untouched because volume/mute state is irrelevant to null playback. if [ "$SINK_CHOICE" != "null" ]; then audio_run_with_timeout_as_test_user --require-session 3s \ @@ -1152,8 +1282,14 @@ elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then 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" + if [ -n "$ALSA_ROUTE_UCM_CARD" ] && + [ -n "$ALSA_ROUTE_UCM_DEVICE" ]; then + log_info "Routing to ALSA sink: choice=$SINK_CHOICE ucm_pcm='$ALSA_ROUTE_DEVICE' playback_target='$SINK_NAME' ucm_card='$ALSA_ROUTE_UCM_CARD' ucm_device='$ALSA_ROUTE_UCM_DEVICE'" + else + log_info "Routing to ALSA playback device: device='$SINK_NAME' requested_choice=$SINK_CHOICE selection=legacy-inventory-fallback" + fi fi +log_info "AUDIO_ROUTE scope=playback backend=$AUDIO_BACKEND requested='$SINK_REQUESTED' request_source=$AUDIO_PLAYBACK_ROUTE_SOURCE explicit=$AUDIO_PLAYBACK_ROUTE_EXPLICIT canonical=$SINK_CHOICE target='$SINK_ID' label='$SINK_NAME' ucm_card='${ALSA_ROUTE_UCM_CARD:-none}' ucm_device='${ALSA_ROUTE_UCM_DEVICE:-none}'" # Decide minimum ok seconds if timeout>0 dur_s="$(duration_to_secs "$TIMEOUT" 2>/dev/null || echo 0)" diff --git a/Runner/suites/Multimedia/Audio/AudioRecord/Read_me.md b/Runner/suites/Multimedia/Audio/AudioRecord/Read_me.md index 58f0493d..824cb1bf 100644 --- a/Runner/suites/Multimedia/Audio/AudioRecord/Read_me.md +++ b/Runner/suites/Multimedia/Audio/AudioRecord/Read_me.md @@ -9,7 +9,8 @@ all-zero audio, materially short captures, and explicitly requested format mismatches. RMS, peak, clipping, digital-silence runs, DC offset, large sample transitions, and silent-channel counts are emitted as diagnostic `AUDIO_VALIDATION` metrics. These metrics do not fail the default policy unless -strict signal validation is explicitly enabled. +strict signal validation is explicitly enabled. Complete RIFF chunks after the +audio `data` chunk are reported as metadata rather than as trailing audio. ## Features @@ -24,8 +25,9 @@ strict signal validation is explicitly enabled. - Unique result file suffixes prevent file collisions in parallel test runs - Unique testcase IDs prevent LAVA testcase ID collisions - Enables running multiple AudioRecord configurations simultaneously in CI -- Records audio with configurable duration and loop count -- Automatically detects and routes to appropriate source (e.g., mic, null) +- Records audio with configurable duration and loop count +- Automatically detects and routes to the requested source (`mic`, + `microphone`, `headset-mic`, or `null`) - Validates recording using multiple evidence sources: - PipeWire/PulseAudio streaming state - ALSA and ASoC runtime status @@ -79,9 +81,9 @@ operations in that user's context. Yocto retains its native execution model. ## Backend and Route Selection -When no backend is requested, the suite uses automatic selection. A real PipeWire audio source uses `pw-record`, and a real PulseAudio source uses `parecord`. Camera, dummy, null, monitor, and loopback PipeWire nodes are not accepted as microphone sources. +When no route is requested, `auto` discovers a built-in microphone first and then a headset microphone on the running target. A real PipeWire audio source uses `pw-record`, and a real PulseAudio source uses `parecord`. `mic` and `microphone` explicitly select the built-in microphone route. `headset-mic`, `headset_mic`, and quoted `headset mic` explicitly select the headset microphone route. `--source auto` restores automatic selection. Camera, dummy, null, monitor, and loopback PipeWire nodes are not accepted as physical microphone sources. The selected backend, request source, explicitness, requested alias, canonical route, runtime target, and UCM device are emitted to standard output in an `AUDIO_ROUTE` record. -If automatic selection finds no physical managed microphone source, the suite probes direct ALSA capture. It selects a card and PCM from the available device inventory, applies only mixer controls exposed by that card, and runs `arecord -D `. This discovers the VA-DMIC capture route from its controls without selecting a form factor or assuming card `0`. When audio remoteproc preflight proves that audio is applicable, failure of both managed-source discovery and direct ALSA probing is reported as FAIL so the image or runtime regression remains tracked. The same absence remains SKIP only when runtime preflight found no applicable audio subsystem. +If automatic selection finds no matching managed source, the suite probes direct ALSA mic and then headset-mic routes. UCM discovery retrieves `CapturePCM`, enables and verifies the exact HiFi device, and the suite functionally probes supported capture formats. An explicit semantic source is never replaced by another source. An explicit source that cannot be discovered and opened reports FAIL, while automatic discovery with no applicable physical input reports SKIP. The remaining UCM namespace, legacy microphone fallback, and audio-remoteproc applicability rules remain unchanged. An explicit backend request is never replaced: @@ -89,6 +91,11 @@ An explicit backend request is never replaced: - `--backend pulseaudio` or `AUDIO_BACKEND=pulseaudio` runs `parecord` only and fails if the requested source is unavailable. - `--backend alsa` or `AUDIO_BACKEND=alsa` runs `arecord` with the discovered ALSA route. +Existing Yocto/LAVA YAML definitions keep their `SOURCE_CHOICE=mic` contract, +which is treated as an explicit built-in microphone validation. Override that +parameter with `auto` when a job should choose mic or headset-mic from runtime +evidence. + ## Audio Remoteproc Preflight Before backend discovery, the suite enumerates runtime remoteprocs and checks the configured firmware and topology under `/lib/firmware` or `/usr/lib/firmware`. It recognizes audio DSP identities and the Shikra modem-hosted audio path. A remoteproc named `modem` is ignored on non-Shikra targets because it is not their audio preflight dependency. @@ -109,8 +116,15 @@ rerunning validation. Yocto remains image-provided and non-installing. Backend client checks run after recovery of the selected backend's complete playback and recording client package set. Package recovery failure is reported -as FAIL. A client that remains unavailable after successful recovery produces -a clean SKIP for the unavailable recording path. +as FAIL. In automatic discovery, a client that remains unavailable after +successful recovery produces a clean SKIP for the unavailable recording path. +An explicit backend or source request instead reports FAIL because the requested +path cannot be validated. + +PulseAudio discovery rejects null, dummy, monitor, loopback, and freewheel +sources. It uses the endpoint name, description, form factor, and active port +to distinguish a built-in microphone from a headset microphone while retaining +a generic physical source as the compatibility fallback for older releases. For overlay builds using audioreach kernel modules, the test automatically: - Detects the overlay build configuration @@ -200,8 +214,14 @@ cd Runner/suites/Multimedia/Audio/AudioRecord # Show usage/help ./run.sh --help -# Run with PipeWire, 3 loops, 10s timeout, mic source -./run.sh --backend pipewire --source mic --loops 3 --timeout 10s +# Run with PipeWire, 3 loops, 10s timeout, mic source +./run.sh --backend pipewire --source mic --loops 3 --timeout 10s + +# Select a headset microphone from runtime PipeWire endpoint data +./run.sh --backend pipewire --source headset-mic --loops 1 + +# Select the headset CapturePCM dynamically from UCM +./run.sh --backend alsa --source headset-mic --loops 1 # Run with PulseAudio, null source, strict mode, verbose ./run.sh --backend pulseaudio --source null --strict --verbose @@ -226,7 +246,7 @@ cd Runner/suites/Multimedia/Audio/AudioRecord Environment Variables: Variable Description Default AUDIO_BACKEND Selects backend: pipewire, pulseaudio, or alsa auto-detect -SOURCE_CHOICE Recording source: mic or null mic +SOURCE_CHOICE Recording source: auto, mic, microphone, headset-mic, or null auto CONFIG_NAMES Test specific configs (e.g., "record_config1 record_config2") record_config1 CONFIG_FILTER Filter configs by pattern (e.g., "48KHz" or "2ch") unset DURATIONS Recording durations: short, medium, long (legacy mode only) "" @@ -245,7 +265,7 @@ LAVA_TESTCASE_ID Unique testcase ID written into the .res file for LAVA CLI Options: Option Description --backend Select backend: pipewire, pulseaudio, or alsa ---source Recording source: mic or null +--source Recording source: auto, mic, microphone, headset-mic, or null --config-name Test specific configs using record_config1-record_config10 or descriptive names (space-separated) --config-filter Filter configs by sample rate or channels (space-separated patterns) --record-seconds Number of seconds to record (e.g., 5s, 10s) diff --git a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh index f1cdc889..bedfc9a2 100755 --- a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh +++ b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh @@ -92,7 +92,18 @@ export AUDIO_OVERLAY_REQUESTED # ---------------- Defaults / CLI ---------------- AUDIO_BACKEND="${AUDIO_BACKEND:-}" -SRC_CHOICE="${SRC_CHOICE:-mic}" # mic|null +if [ "${SRC_CHOICE+x}" = x ]; then + AUDIO_CAPTURE_ROUTE_EXPLICIT=1 + AUDIO_CAPTURE_ROUTE_SOURCE="SRC_CHOICE environment" +elif [ "${SOURCE_CHOICE+x}" = x ]; then + SRC_CHOICE="$SOURCE_CHOICE" + AUDIO_CAPTURE_ROUTE_EXPLICIT=1 + AUDIO_CAPTURE_ROUTE_SOURCE="SOURCE_CHOICE environment" +else + SRC_CHOICE="auto" + AUDIO_CAPTURE_ROUTE_EXPLICIT=0 + AUDIO_CAPTURE_ROUTE_SOURCE="automatic" +fi DURATIONS="" # Will be set to default only if using legacy mode RECORD_SECONDS="${RECORD_SECONDS:-30s}" # DEFAULT: 30s; 'auto' maps short/med/long LOOPS="${LOOPS:-1}" @@ -132,7 +143,7 @@ usage() { cat <"$LOGDIR/summary.txt"; then exit 1 fi -while [ $# -gt 0 ]; do - case "$1" in +# Parse recording arguments after privileged package preparation. +parse_args() { + while [ "$#" -gt 0 ]; do + case "$1" in --overlay) AUDIO_OVERLAY_REQUESTED=1 export AUDIO_OVERLAY_REQUESTED @@ -258,6 +271,8 @@ while [ $# -gt 0 ]; do ;; --source) SRC_CHOICE="$2" + AUDIO_CAPTURE_ROUTE_EXPLICIT=1 + AUDIO_CAPTURE_ROUTE_SOURCE="command-line" shift 2 ;; --config-name) @@ -341,8 +356,24 @@ while [ $# -gt 0 ]; do log_warn "Unknown option: $1" shift ;; - esac -done + esac + done +} + +parse_args "$@" + +SOURCE_REQUESTED="$SRC_CHOICE" +SRC_CHOICE="$( + audio_normalize_capture_route "$SOURCE_REQUESTED" 2>/dev/null +)" || { + log_fail "$TESTNAME FAIL - invalid source '$SOURCE_REQUESTED', expected auto, mic, microphone, headset-mic, or null" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 +} +if [ "$SRC_CHOICE" = "auto" ]; then + AUDIO_CAPTURE_ROUTE_EXPLICIT=0 +fi +export SRC_CHOICE AUDIO_CAPTURE_ROUTE_EXPLICIT AUDIO_CAPTURE_ROUTE_SOURCE # Preserve an explicit backend request. Automatic fallback is only appropriate # when backend selection was left in auto mode. @@ -409,7 +440,18 @@ case "$AUDIO_RECORD_OS_ID" in ;; esac -export AUDIO_RECORD_DESKTOP_ROOT_MODE +if ! command -v audio_configure_ubuntu_desktop_session >/dev/null 2>&1; then + log_fail "$TESTNAME FAIL - required helper is unavailable: audio_configure_ubuntu_desktop_session" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 +fi + +audio_configure_ubuntu_desktop_session "$AUDIO_RECORD_OS_ID" +if [ "${AUDIO_USE_DESKTOP_SESSION:-0}" -eq 1 ]; then + AUDIO_RECORD_DESKTOP_ROOT_MODE=1 +fi + +export AUDIO_RECORD_DESKTOP_ROOT_MODE AUDIO_USE_DESKTOP_SESSION # Only this scratch directory is writable by the desktop Audio user. Root keeps # ownership of the final LOGDIR, logs, summary, JUnit data, and promoted WAVs. @@ -425,7 +467,13 @@ if [ "$AUDIO_RECORD_DESKTOP_ROOT_MODE" -eq 1 ]; then exit 1 fi - if ! chown "$AUDIO_TEST_USER:audio" \ + if [ "$AUDIO_RECORD_OS_ID" = "ubuntu" ]; then + audio_record_workspace_owner="$AUDIO_TEST_USER" + else + audio_record_workspace_owner="$AUDIO_TEST_USER:audio" + fi + + if ! chown "$audio_record_workspace_owner" \ "$AUDIO_RECORD_USER_CAPTURE_DIR"; then log_fail "$TESTNAME FAIL - failed to assign Audio user capture directory" echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" @@ -555,6 +603,7 @@ if [ -n "$CONFIG_NAMES" ] && [ -n "$CONFIG_FILTER" ]; then fi log_info "Args: backend=${AUDIO_BACKEND:-auto} source=$SRC_CHOICE overlay=$AUDIO_OVERLAY_REQUESTED loops=$LOOPS durations='$DURATIONS' record_seconds=$RECORD_SECONDS timeout=$TIMEOUT start_grace=${AUDIO_RECORD_START_GRACE:-5} strict=$STRICT signal_strict=$AUDIO_RECORD_STRICT_SIGNAL dmesg=$DMESG_SCAN bootstrap=$AUDIO_BOOTSTRAP_MODE runtime_dir=${AUDIO_RUNTIME_DIR:-auto}" +log_info "AUDIO_ROUTE_DISCOVERY scope=record requested='$SOURCE_REQUESTED' request_source='$AUDIO_CAPTURE_ROUTE_SOURCE' explicit=$AUDIO_CAPTURE_ROUTE_EXPLICIT policy=$SRC_CHOICE automatic_order='mic headset-mic'" # Resolve backend (allow minimal-build ALSA capture fallback) if [ -z "$AUDIO_BACKEND" ]; then @@ -584,14 +633,15 @@ if [ -z "$AUDIO_BACKEND" ]; then AUDIO_BACKEND="$(audio_run_helper_as_test_user --require-session detect_audio_backend 2>/dev/null || echo "")" audio_record_set_recovered_backend_management else - if audio_record_probe_alsa_capture_profile; then + if audio_record_probe_selected_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 - if [ "$audio_remoteproc_rc" -eq 0 ] 2>/dev/null; then + if [ "$AUDIO_CAPTURE_ROUTE_EXPLICIT" -eq 1 ] || + [ "$audio_remoteproc_rc" -eq 0 ] 2>/dev/null; then log_fail "$TESTNAME FAIL - audio runtime is applicable but no managed recording backend or direct ALSA capture path is usable, reason=${AUDIO_ALSA_CAPTURE_REASON:-capture path unavailable}, verify sound-card registration, topology, UCM, mixer routing, and image audio packages" echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" exit 1 @@ -623,7 +673,7 @@ if [ "$AUDIO_BACKEND" = "alsa" ]; then if [ "$ALSA_CAPTURE_PROBED" -eq 1 ]; then backend_ok=1 else - if audio_record_probe_alsa_capture_profile; then + if audio_record_probe_selected_alsa_capture_profile; then ALSA_CAPTURE_PROBED=1 backend_ok=1 fi @@ -662,7 +712,7 @@ if [ "$backend_ok" -ne 1 ] && [ -z "$AUDIO_BACKEND_REQUESTED" ] && fi if [ "$backend_ok" -ne 1 ] && [ "$AUDIO_BACKEND" != "alsa" ]; then - if audio_record_probe_alsa_capture_profile; then + if audio_record_probe_selected_alsa_capture_profile; then ALSA_CAPTURE_PROBED=1 AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 @@ -673,7 +723,8 @@ if [ "$backend_ok" -ne 1 ] && [ "$AUDIO_BACKEND" != "alsa" ]; then fi if [ "$backend_ok" -ne 1 ]; then - if [ -n "$AUDIO_BACKEND_REQUESTED" ] || + if [ "$AUDIO_CAPTURE_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ] || [ "$audio_remoteproc_rc" -eq 0 ] 2>/dev/null; then if [ "$AUDIO_BACKEND" = "alsa" ] || [ "$ALSA_CAPTURE_PROBED" -eq 1 ]; then log_fail "$TESTNAME FAIL - audio runtime is applicable but the ALSA capture path is unusable, reason=${AUDIO_ALSA_CAPTURE_REASON:-capture device could not be opened}" @@ -695,23 +746,44 @@ case "$AUDIO_BACKEND" in pipewire) if ! command -v wpctl >/dev/null 2>&1 || ! command -v pw-record >/dev/null 2>&1; then - log_skip "$TESTNAME SKIP - missing PipeWire recording utilities: wpctl and/or pw-record" - echo "$RESULT_TESTNAME SKIP" > "$RES_FILE" + if [ "$AUDIO_CAPTURE_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ]; then + log_fail "$TESTNAME FAIL - requested source '$SOURCE_REQUESTED' cannot be validated because wpctl or pw-record is missing from the image" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + log_skip "$TESTNAME SKIP - automatic capture discovery requires image-provided wpctl and pw-record for the active PipeWire backend" + echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" exit 0 fi ;; pulseaudio) if ! command -v pactl >/dev/null 2>&1 || ! command -v parecord >/dev/null 2>&1; then - log_skip "$TESTNAME SKIP - missing PulseAudio recording utilities: pactl and/or parecord" - echo "$RESULT_TESTNAME SKIP" > "$RES_FILE" + if [ "$AUDIO_CAPTURE_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ]; then + log_fail "$TESTNAME FAIL - requested source '$SOURCE_REQUESTED' cannot be validated because pactl or parecord is missing from the image" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + log_skip "$TESTNAME SKIP - automatic capture discovery requires image-provided pactl and parecord for the active PulseAudio backend" + echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" exit 0 fi ;; alsa) if ! command -v arecord >/dev/null 2>&1; then - log_skip "$TESTNAME SKIP - missing ALSA recording utility: arecord" - echo "$RESULT_TESTNAME SKIP" > "$RES_FILE" + if [ "$AUDIO_CAPTURE_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ]; then + log_fail "$TESTNAME FAIL - requested source '$SOURCE_REQUESTED' cannot be validated because arecord is missing from the image" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + log_skip "$TESTNAME SKIP - automatic ALSA capture discovery requires image-provided arecord" + echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" exit 0 fi ;; @@ -735,9 +807,23 @@ if [ "$AUDIO_BACKEND" = "pipewire" ]; then audio_record_set_recovered_backend_management fi if ! audio_run_helper_as_test_user --require-session audio_pw_ctl_ok 2>/dev/null; then - log_skip "$TESTNAME SKIP - PipeWire control-plane not responsive" - echo "$RESULT_TESTNAME SKIP" > "$RES_FILE" - exit 0 + if [ -z "$AUDIO_BACKEND_REQUESTED" ] && + audio_record_probe_selected_alsa_capture_profile; then + ALSA_CAPTURE_PROBED=1 + AUDIO_BACKEND="alsa" + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + log_warn "$TESTNAME: PipeWire control-plane is unavailable, using the validated direct ALSA capture route" + elif [ "$AUDIO_CAPTURE_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ]; then + log_fail "$TESTNAME FAIL - requested source '$SOURCE_REQUESTED' cannot be validated because the PipeWire control-plane is unavailable and no matching direct ALSA route opened" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + else + log_skip "$TESTNAME SKIP - PipeWire control-plane is unavailable and automatic ALSA capture discovery found no usable mic or headset-mic route" + echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" + exit 0 + fi fi fi elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then @@ -752,36 +838,57 @@ elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then audio_record_set_recovered_backend_management fi if ! audio_run_helper_as_test_user --require-session audio_pa_ctl_ok 2>/dev/null; then - log_skip "$TESTNAME SKIP - PulseAudio control-plane not responsive" - echo "$RESULT_TESTNAME SKIP" > "$RES_FILE" - exit 0 + if [ -z "$AUDIO_BACKEND_REQUESTED" ] && + audio_record_probe_selected_alsa_capture_profile; then + ALSA_CAPTURE_PROBED=1 + AUDIO_BACKEND="alsa" + AUDIO_SYSTEMD_MANAGED=0 + export AUDIO_SYSTEMD_MANAGED + log_warn "$TESTNAME: PulseAudio control-plane is unavailable, using the validated direct ALSA capture route" + elif [ "$AUDIO_CAPTURE_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ]; then + log_fail "$TESTNAME FAIL - requested source '$SOURCE_REQUESTED' cannot be validated because the PulseAudio control-plane is unavailable and no matching direct ALSA route opened" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + else + log_skip "$TESTNAME SKIP - PulseAudio control-plane is unavailable and automatic ALSA capture discovery found no usable mic or headset-mic route" + echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" + exit 0 + fi fi fi fi # ----- Route source (set default; recorder uses default source) ----- SRC_ID="" -case "$AUDIO_BACKEND:$SRC_CHOICE" in - pipewire:null) - SRC_ID="$(audio_run_helper_as_test_user --require-session pw_default_null_source)" - ;; - pipewire:*) - SRC_ID="$(audio_run_helper_as_test_user --require-session pw_default_mic)" - ;; - pulseaudio:null) - SRC_ID="$(audio_run_helper_as_test_user --require-session pa_default_null_source)" - ;; - pulseaudio:*) - SRC_ID="$(audio_run_helper_as_test_user --require-session pa_default_mic)" - ;; - alsa:null) - SRC_ID="" +MANAGED_ROUTE_RECORD="" +case "$AUDIO_BACKEND" in + pipewire|pulseaudio) + MANAGED_ROUTE_RECORD="$( + audio_run_helper_as_test_user \ + --require-session \ + audio_select_managed_capture_route \ + "$AUDIO_BACKEND" "$SRC_CHOICE" 2>/dev/null || true + )" + if [ -n "$MANAGED_ROUTE_RECORD" ]; then + SRC_CHOICE="$( + printf '%s\n' "$MANAGED_ROUTE_RECORD" | cut -d'|' -f1 + )" + SRC_ID="$( + printf '%s\n' "$MANAGED_ROUTE_RECORD" | cut -d'|' -f2- + )" + export SRC_CHOICE + fi ;; - alsa:*) - if [ "$ALSA_CAPTURE_PROBED" -eq 1 ] && [ -n "$AUDIO_ALSA_CAPTURE_DEVICE" ]; then + alsa) + if [ "$SRC_CHOICE" = "null" ]; then + SRC_ID="" + elif [ "$ALSA_CAPTURE_PROBED" -eq 1 ] && + [ -n "$AUDIO_ALSA_CAPTURE_DEVICE" ]; then + SRC_ID="$AUDIO_ALSA_CAPTURE_DEVICE" + elif audio_record_probe_selected_alsa_capture_profile; then + ALSA_CAPTURE_PROBED=1 SRC_ID="$AUDIO_ALSA_CAPTURE_DEVICE" - else - SRC_ID="$(audio_run_helper_as_test_user alsa_pick_capture)" fi ;; esac @@ -792,10 +899,10 @@ esac # a real capture source, this can record from dummy/null/default paths or fail # after creating empty files. if [ -z "$SRC_ID" ] && [ -z "$AUDIO_BACKEND_REQUESTED" ] && - [ "$SRC_CHOICE" = "mic" ] && [ "$AUDIO_BACKEND" = "pipewire" ]; then - log_warn "$TESTNAME: no concrete PipeWire mic source found, probing direct ALSA capture path" + [ "$SRC_CHOICE" != "null" ] && [ "$AUDIO_BACKEND" = "pipewire" ]; then + log_warn "$TESTNAME: no concrete PipeWire $SRC_CHOICE source found, probing direct ALSA capture path" - if audio_record_probe_alsa_capture_profile; then + if audio_record_probe_selected_alsa_capture_profile; then ALSA_CAPTURE_PROBED=1 AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 @@ -806,8 +913,9 @@ if [ -z "$SRC_ID" ] && [ -z "$AUDIO_BACKEND_REQUESTED" ] && log_warn "$TESTNAME: falling back to direct ALSA capture device: $SRC_ID" else - if [ "$audio_remoteproc_rc" -eq 0 ] 2>/dev/null; then - log_fail "$TESTNAME FAIL - audio runtime is applicable but no physical PipeWire microphone source or direct ALSA capture path is usable, reason=${AUDIO_ALSA_CAPTURE_REASON:-capture path unavailable}, verify sound-card registration, topology, UCM, mixer routing, and image audio packages" + if [ "$AUDIO_CAPTURE_ROUTE_EXPLICIT" -eq 1 ] || + [ "$audio_remoteproc_rc" -eq 0 ] 2>/dev/null; then + log_fail "$TESTNAME FAIL - audio runtime is applicable but no physical PipeWire $SRC_CHOICE source or matching direct ALSA capture path is usable, reason=${AUDIO_ALSA_CAPTURE_REASON:-capture path unavailable}, verify sound-card registration, topology, UCM, mixer routing, and image audio packages" echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" exit 1 fi @@ -819,31 +927,55 @@ if [ -z "$SRC_ID" ] && [ -z "$AUDIO_BACKEND_REQUESTED" ] && fi if [ -z "$SRC_ID" ] && [ -z "$AUDIO_BACKEND_REQUESTED" ] && - [ "$SRC_CHOICE" = "mic" ] && [ "$AUDIO_BACKEND" != "pipewire" ]; then + [ "$SRC_CHOICE" != "null" ] && [ "$AUDIO_BACKEND" != "pipewire" ]; then for b in $BACKENDS_TO_TRY; do [ "$b" = "$AUDIO_BACKEND" ] && continue case "$b" in pipewire) - cand="$(audio_run_helper_as_test_user --require-session pw_default_mic)" - if [ -n "$cand" ]; then + cand_record="$( + audio_run_helper_as_test_user \ + --require-session \ + audio_select_managed_capture_route \ + pipewire "$SRC_CHOICE" 2>/dev/null || true + )" + if [ -n "$cand_record" ]; then + SRC_CHOICE="$( + printf '%s\n' "$cand_record" | cut -d'|' -f1 + )" + cand="$( + printf '%s\n' "$cand_record" | cut -d'|' -f2- + )" AUDIO_BACKEND="pipewire" SRC_ID="$cand" + export SRC_CHOICE log_info "Falling back to backend: pipewire (source id=$SRC_ID)" break fi ;; pulseaudio) - cand="$(audio_run_helper_as_test_user --require-session pa_default_mic)" - if [ -n "$cand" ]; then + cand_record="$( + audio_run_helper_as_test_user \ + --require-session \ + audio_select_managed_capture_route \ + pulseaudio "$SRC_CHOICE" 2>/dev/null || true + )" + if [ -n "$cand_record" ]; then + SRC_CHOICE="$( + printf '%s\n' "$cand_record" | cut -d'|' -f1 + )" + cand="$( + printf '%s\n' "$cand_record" | cut -d'|' -f2- + )" AUDIO_BACKEND="pulseaudio" SRC_ID="$cand" + export SRC_CHOICE log_info "Falling back to backend: pulseaudio (source=$SRC_ID)" break fi ;; alsa) - if audio_record_probe_alsa_capture_profile; then + if audio_record_probe_selected_alsa_capture_profile; then ALSA_CAPTURE_PROBED=1 AUDIO_BACKEND="alsa" AUDIO_SYSTEMD_MANAGED=0 @@ -861,14 +993,15 @@ if [ -z "$SRC_ID" ] && [ -z "$AUDIO_BACKEND_REQUESTED" ] && fi if [ -z "$SRC_ID" ]; then - if [ -n "$AUDIO_BACKEND_REQUESTED" ] || + if [ "$AUDIO_CAPTURE_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ] || [ "$audio_remoteproc_rc" -eq 0 ] 2>/dev/null; then - log_fail "$TESTNAME FAIL - requested source '$SRC_CHOICE' is unavailable on ready audio backends (${BACKENDS_TO_TRY:-unknown}), verify the capture route and image audio configuration" + log_fail "$TESTNAME FAIL - requested source '$SOURCE_REQUESTED' is unavailable on ready audio backends (${BACKENDS_TO_TRY:-unknown}), discovered_route='${SRC_CHOICE:-none}' reason=${AUDIO_ALSA_CAPTURE_REASON:-managed source not found}, verify the capture route, UCM data, and image audio configuration" echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" exit 1 fi - log_skip "$TESTNAME SKIP - requested source '$SRC_CHOICE' not available on any backend (${BACKENDS_TO_TRY:-unknown})" + log_skip "$TESTNAME SKIP - automatic capture discovery found no usable mic or headset-mic route on any backend (${BACKENDS_TO_TRY:-unknown})" echo "$RESULT_TESTNAME SKIP" > "$RES_FILE" exit 0 fi @@ -916,6 +1049,23 @@ if [ "$AUDIO_BACKEND" = "alsa" ]; then esac fi +# Resolve the final ALSA route again after backend fallback so the log records +# the exact UCM card and device that were selected during the capture probe. +ALSA_CAPTURE_ROUTE_RECORD="" +ALSA_CAPTURE_ROUTE_UCM_CARD="" +ALSA_CAPTURE_ROUTE_UCM_DEVICE="" +if [ "$AUDIO_BACKEND" = "alsa" ] && [ "$SRC_CHOICE" != "null" ]; then + ALSA_CAPTURE_ROUTE_RECORD="$( + audio_record_resolve_alsa_route "$SRC_CHOICE" 2>/dev/null || true + )" + ALSA_CAPTURE_ROUTE_UCM_CARD="$( + printf '%s\n' "$ALSA_CAPTURE_ROUTE_RECORD" | cut -d'|' -f2 + )" + ALSA_CAPTURE_ROUTE_UCM_DEVICE="$( + printf '%s\n' "$ALSA_CAPTURE_ROUTE_RECORD" | cut -d'|' -f3 + )" +fi + # ---- Routing log / defaults per backend ---- if [ "$AUDIO_BACKEND" = "pipewire" ]; then if [ -n "$SRC_ID" ]; then @@ -933,8 +1083,14 @@ elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then log_info "Routing to source: name='$SRC_LABEL' choice=$SRC_CHOICE" else # ALSA SRC_LABEL="${SRC_ID:-default}" - log_info "Routing to source: name='$SRC_LABEL' choice=$SRC_CHOICE" + if [ -n "$ALSA_CAPTURE_ROUTE_UCM_CARD" ] && + [ -n "$ALSA_CAPTURE_ROUTE_UCM_DEVICE" ]; then + log_info "Routing to ALSA source: choice=$SRC_CHOICE device='$SRC_LABEL' ucm_card='$ALSA_CAPTURE_ROUTE_UCM_CARD' ucm_device='$ALSA_CAPTURE_ROUTE_UCM_DEVICE'" + else + log_info "Routing to ALSA capture device: device='$SRC_LABEL' requested_choice=$SRC_CHOICE selection=legacy-inventory-fallback" + fi fi +log_info "AUDIO_ROUTE scope=record backend=$AUDIO_BACKEND requested='$SOURCE_REQUESTED' request_source='$AUDIO_CAPTURE_ROUTE_SOURCE' explicit=$AUDIO_CAPTURE_ROUTE_EXPLICIT canonical=$SRC_CHOICE target='${SRC_ID:-default}' label='$SRC_LABEL' ucm_card='${ALSA_CAPTURE_ROUTE_UCM_CARD:-none}' ucm_device='${ALSA_CAPTURE_ROUTE_UCM_DEVICE:-none}'" # If route discovery changed the backend, prepare that backend's complete # playback and recording client set before validating its commands. @@ -948,23 +1104,44 @@ case "$AUDIO_BACKEND" in pipewire) if ! command -v wpctl >/dev/null 2>&1 || ! command -v pw-record >/dev/null 2>&1; then - log_skip "$TESTNAME SKIP - missing PipeWire recording utilities: wpctl and/or pw-record" - echo "$RESULT_TESTNAME SKIP" > "$RES_FILE" + if [ "$AUDIO_CAPTURE_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ]; then + log_fail "$TESTNAME FAIL - requested source '$SOURCE_REQUESTED' cannot be recorded because wpctl or pw-record is missing after backend selection" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + log_skip "$TESTNAME SKIP - selected PipeWire capture route cannot run because wpctl or pw-record is missing" + echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" exit 0 fi ;; pulseaudio) if ! command -v pactl >/dev/null 2>&1 || ! command -v parecord >/dev/null 2>&1; then - log_skip "$TESTNAME SKIP - missing PulseAudio recording utilities: pactl and/or parecord" - echo "$RESULT_TESTNAME SKIP" > "$RES_FILE" + if [ "$AUDIO_CAPTURE_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ]; then + log_fail "$TESTNAME FAIL - requested source '$SOURCE_REQUESTED' cannot be recorded because pactl or parecord is missing after backend selection" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + log_skip "$TESTNAME SKIP - selected PulseAudio capture route cannot run because pactl or parecord is missing" + echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" exit 0 fi ;; alsa) if ! command -v arecord >/dev/null 2>&1; then - log_skip "$TESTNAME SKIP - missing ALSA recording utility: arecord" - echo "$RESULT_TESTNAME SKIP" > "$RES_FILE" + if [ "$AUDIO_CAPTURE_ROUTE_EXPLICIT" -eq 1 ] || + [ -n "$AUDIO_BACKEND_REQUESTED" ]; then + log_fail "$TESTNAME FAIL - requested source '$SOURCE_REQUESTED' cannot be recorded because arecord is missing after route selection" + echo "$RESULT_TESTNAME FAIL" >"$RES_FILE" + exit 1 + fi + + log_skip "$TESTNAME SKIP - selected ALSA capture route cannot run because arecord is missing" + echo "$RESULT_TESTNAME SKIP" >"$RES_FILE" exit 0 fi ;; diff --git a/Runner/utils/audio_common.sh b/Runner/utils/audio_common.sh index e3104459..bf4727c9 100755 --- a/Runner/utils/audio_common.sh +++ b/Runner/utils/audio_common.sh @@ -307,7 +307,7 @@ audio_fetch_assets_from_url() { # 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" + log_info "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") @@ -983,9 +983,9 @@ pw_sink_is_real_audio() { return 0 } -# Prefer speaker sinks before other physical outputs. Headphones are only a -# fallback for speakers, so a listed headphone does not override a speaker. -# Do not use a PipeWire dummy sink as a successful speakers route. +# Prefer speaker sinks before other physical outputs. A generic physical sink +# remains a compatibility fallback, but an explicitly named headphones sink is +# never selected for a speakers request. pw_default_speakers() { st="$(pwctl_status_safe 2>/dev/null)" || { printf '%s\n' "" @@ -1003,18 +1003,13 @@ pw_default_speakers() { grep -Ei 'line[._ -]*out|analog|hdmi' | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' )" - headphone_ids="$( - printf '%s\n' "$block" | - grep -Ei 'headphone' | - sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' - )" all_ids="$( printf '%s\n' "$block" | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' )" checked_ids="" - for id in $speaker_ids $output_ids $headphone_ids $all_ids; do + for id in $speaker_ids $output_ids $all_ids; do case " $checked_ids " in *" $id "*) continue @@ -1023,6 +1018,17 @@ pw_default_speakers() { checked_ids="$checked_ids $id" + ps_label="$( + pwctl_inspect_safe "$id" 2>/dev/null | + grep -E 'node[.](description|name)[[:space:]]*=' | + cut -d'"' -f2 | + tr '\n' ' ' + )" + + if printf '%s\n' "$ps_label" | grep -Eiq 'headphone|headset|3[.]5|hsj'; then + continue + fi + if pw_sink_is_real_audio "$id"; then printf '%s\n' "$id" return 0 @@ -1032,6 +1038,30 @@ pw_default_speakers() { printf '%s\n' "" } +# Return the first real PipeWire sink identified as headphones at runtime. +pw_default_headphones() { + pdh_status="$(pwctl_status_safe 2>/dev/null)" || { + printf '%s\n' "" + return 0 + } + + pdh_block="$(printf '%s\n' "$pdh_status" | sed -n '/Sinks:/,/Sources:/p')" + pdh_ids="$( + printf '%s\n' "$pdh_block" | + grep -Ei 'headphone|headset|3[.]5|hsj' | + sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' + )" + + for pdh_id in $pdh_ids; do + if pw_sink_is_real_audio "$pdh_id"; then + printf '%s\n' "$pdh_id" + return 0 + fi + done + + printf '%s\n' "" +} + pw_default_null() { st="$(pwctl_status_safe 2>/dev/null)" || return 0 printf '%s\n' "$st" \ @@ -1137,6 +1167,18 @@ pw_default_mic() { checked_ids="$checked_ids $id" + pdm_label="$( + pwctl_inspect_safe "$id" 2>/dev/null | + grep -E 'node[.](description|name)[[:space:]]*=' | + cut -d'"' -f2 | + tr '\n' ' ' + )" + + if printf '%s\n' "$pdm_label" | + grep -Eiq 'headphone|headset|3[.]5|hsj|hsmic'; then + continue + fi + if pw_source_is_real_audio "$id"; then printf '%s\n' "$id" return 0 @@ -1146,6 +1188,33 @@ pw_default_mic() { printf '%s\n' "" } +# Return the first real PipeWire source identified as a headset microphone. +pw_default_headset_mic() { + pdhm_status="$(pwctl_status_safe 2>/dev/null)" || { + printf '%s\n' "" + return 0 + } + + pdhm_block="$( + printf '%s\n' "$pdhm_status" | + sed -n '/Sources:/,/Filters:/p' + )" + pdhm_ids="$( + printf '%s\n' "$pdhm_block" | + grep -Ei 'headphone|headset|3[.]5|hsj|hsmic' | + sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' + )" + + for pdhm_id in $pdhm_ids; do + if pw_source_is_real_audio "$pdhm_id"; then + printf '%s\n' "$pdhm_id" + return 0 + fi + done + + printf '%s\n' "" +} + pw_default_null_source() { st="$(pwctl_status_safe 2>/dev/null)" || { printf '%s\n' ""; return 0; } @@ -1180,14 +1249,115 @@ pw_source_label_safe() { printf '%s\n' "$pw_source_label" } # ---------- PulseAudio: sinks (playback) ---------- +# Print stable route-identification fields for one PulseAudio sink. Do not +# include the complete Ports section because it lists inactive routes too. +pa_sink_route_label() { + pasrl_name="$1" + + [ -n "$pasrl_name" ] || return 1 + + { + printf '%s\n' "$pasrl_name" + pactl list sinks 2>/dev/null | + awk -v requested="$pasrl_name" ' + $1 == "Name:" { + selected = ($2 == requested) + } + selected && + ($1 == "Description:" || + $1 == "Active" && $2 == "Port:" || + $1 == "device.description" || + $1 == "device.form_factor") { + print + } + ' + } | + tr '\n' ' ' +} + +# Return success for a non-virtual PulseAudio playback sink. The short sink +# inventory is authoritative for existence, while metadata only rejects known +# virtual endpoints so older distributions with sparse properties still work. +pa_sink_is_real_audio() { + pasira_name="$1" + + [ -n "$pasira_name" ] || return 1 + + if ! pactl list short sinks 2>/dev/null | + awk -v requested="$pasira_name" \ + '$2 == requested { found = 1 } END { exit !found }'; then + return 1 + fi + + pasira_label="$(pa_sink_route_label "$pasira_name" 2>/dev/null || true)" + if printf '%s\n' "$pasira_label" | + grep -Eiq '(^|[._ /-])(null|dummy|monitor|loopback|freewheel)($|[._ /-])'; then + return 1 + fi + + return 0 +} + pa_default_speakers() { - sinks="$(pactl list short sinks 2>/dev/null)" - name="$(printf '%s\n' "$sinks" | awk '{print $2}' | grep -i 'speaker' | head -n1)" - [ -n "$name" ] || name="$(printf '%s\n' "$sinks" | awk '{print $2}' | grep -Ei 'line[._ -]*out|analog|hdmi' | head -n1)" - [ -n "$name" ] || name="$(printf '%s\n' "$sinks" | awk '{print $2}' | grep -i 'headphone' | head -n1)" - [ -n "$name" ] || name="$(pactl info 2>/dev/null | sed -n 's/^Default Sink:[[:space:]]*//p' | head -n1)" - [ -n "$name" ] || name="$(printf '%s\n' "$sinks" | awk '{print $2}' | head -n1)" - printf '%s\n' "$name" + pds_sinks="$(pactl list short sinks 2>/dev/null)" + pds_names="$(printf '%s\n' "$pds_sinks" | awk '{print $2}')" + pds_default="$( + pactl info 2>/dev/null | + sed -n 's/^Default Sink:[[:space:]]*//p' | + head -n 1 + )" + pds_preferred="" + pds_fallback="" + + for pds_name in $pds_names; do + if ! pa_sink_is_real_audio "$pds_name"; then + continue + fi + + pds_label="$(pa_sink_route_label "$pds_name" 2>/dev/null || true)" + if printf '%s\n' "$pds_label" | + grep -Eiq 'headphone|headset|3[.]5|hsj'; then + continue + fi + + if printf '%s\n' "$pds_label" | + grep -Eiq 'speaker|line[._ /-]*out|analog|hdmi'; then + pds_preferred="$pds_name" + break + fi + + if [ "$pds_name" = "$pds_default" ]; then + pds_fallback="$pds_name" + elif [ -z "$pds_fallback" ]; then + pds_fallback="$pds_name" + fi + done + + if [ -n "$pds_preferred" ]; then + printf '%s\n' "$pds_preferred" + else + printf '%s\n' "$pds_fallback" + fi +} + +# Return the first PulseAudio sink identified as headphones at runtime. +pa_default_headphones() { + pdh_names="$(pactl list short sinks 2>/dev/null | awk '{print $2}')" + + for pdh_name in $pdh_names; do + if ! pa_sink_is_real_audio "$pdh_name"; then + continue + fi + + pdh_label="$(pa_sink_route_label "$pdh_name" 2>/dev/null || true)" + if printf '%s\n' "$pdh_label" | + grep -Eiq 'headphone|headset|3[.]5|hsj'; then + printf '%s\n' "$pdh_name" + return 0 + fi + done + + printf '%s\n' "" } pa_default_null() { @@ -1207,6 +1377,53 @@ pa_sink_name() { } # ---------- PulseAudio: sources (record) ---------- +# Print stable route-identification fields for one PulseAudio source. This +# excludes inactive port inventory for the same reason as pa_sink_route_label. +pa_source_route_label() { + pasrl_name="$1" + + [ -n "$pasrl_name" ] || return 1 + + { + printf '%s\n' "$pasrl_name" + pactl list sources 2>/dev/null | + awk -v requested="$pasrl_name" ' + $1 == "Name:" { + selected = ($2 == requested) + } + selected && + ($1 == "Description:" || + $1 == "Active" && $2 == "Port:" || + $1 == "device.description" || + $1 == "device.form_factor") { + print + } + ' + } | + tr '\n' ' ' +} + +# Return success for a non-monitor, non-virtual PulseAudio capture source. +pa_source_is_real_audio() { + pasira_name="$1" + + [ -n "$pasira_name" ] || return 1 + + if ! pactl list short sources 2>/dev/null | + awk -v requested="$pasira_name" \ + '$2 == requested { found = 1 } END { exit !found }'; then + return 1 + fi + + pasira_label="$(pa_source_route_label "$pasira_name" 2>/dev/null || true)" + if printf '%s\n' "$pasira_label" | + grep -Eiq '(^|[._ /-])(null|dummy|monitor|loopback|freewheel)($|[._ /-])'; then + return 1 + fi + + return 0 +} + pa_default_source() { s="$(pactl get-default-source 2>/dev/null | tr -d '\r')" [ -n "$s" ] || s="$(pactl info 2>/dev/null | awk -F': ' '/Default Source:/{print $2}')" @@ -1238,14 +1455,66 @@ pa_resolve_mic_fallback() { # ----------- PulseAudio Source Helpers ----------- pa_default_mic() { - def="$(pactl info 2>/dev/null | sed -n 's/^Default Source:[[:space:]]*//p' | head -n1)" - if [ -n "$def" ]; then - printf '%s\n' "$def"; return 0 + pdm_names="$(pactl list short sources 2>/dev/null | awk '{print $2}')" + pdm_default="$( + pactl info 2>/dev/null | + sed -n 's/^Default Source:[[:space:]]*//p' | + head -n 1 + )" + pdm_preferred="" + pdm_fallback="" + + for pdm_name in $pdm_names; do + if ! pa_source_is_real_audio "$pdm_name"; then + continue + fi + + pdm_label="$(pa_source_route_label "$pdm_name" 2>/dev/null || true)" + if printf '%s\n' "$pdm_label" | + grep -Eiq 'headphone|headset|3[.]5|hsj|hsmic'; then + continue + fi + + if printf '%s\n' "$pdm_label" | + grep -Eiq 'mic|microphone|capture|audio[._ /-]*input'; then + pdm_preferred="$pdm_name" + break + fi + + if [ "$pdm_name" = "$pdm_default" ]; then + pdm_fallback="$pdm_name" + elif [ -z "$pdm_fallback" ]; then + pdm_fallback="$pdm_name" + fi + done + + if [ -n "$pdm_preferred" ]; then + printf '%s\n' "$pdm_preferred" + else + printf '%s\n' "$pdm_fallback" fi - name="$(pactl list short sources 2>/dev/null | awk '{print $2}' | grep -i 'mic' | head -n1)" - [ -n "$name" ] || name="$(pactl list short sources 2>/dev/null | awk '{print $2}' | head -n1)" - printf '%s\n' "$name" } + +# Return the first PulseAudio source identified as a headset microphone. +pa_default_headset_mic() { + pdhm_names="$(pactl list short sources 2>/dev/null | awk '{print $2}')" + + for pdhm_name in $pdhm_names; do + if ! pa_source_is_real_audio "$pdhm_name"; then + continue + fi + + pdhm_label="$(pa_source_route_label "$pdhm_name" 2>/dev/null || true)" + if printf '%s\n' "$pdhm_label" | + grep -Eiq 'headphone|headset|3[.]5|hsj|hsmic'; then + printf '%s\n' "$pdhm_name" + return 0 + fi + done + + printf '%s\n' "" +} + pa_default_null_source() { name="$(pactl list short sources 2>/dev/null | awk '{print $2}' | grep -i 'null\|dummy' | head -n1)" printf '%s\n' "$name" @@ -3201,7 +3470,7 @@ audio_alsa_card_indexes() { # Extract the card index from an hw:CARD,DEV or plughw:CARD,DEV identifier. audio_alsa_device_card() { printf '%s\n' "$1" | - sed -n 's/^\(plug\)\{0,1\}hw:\([0-9][0-9]*\),.*/\2/p' + sed -n 's/^\(plug\)\{0,1\}hw:\([^,][^,]*\),.*/\2/p' } # Return success after ALSA exposes at least one hardware capture PCM. @@ -3257,45 +3526,462 @@ audio_alsa_playback_profile() { return 0 fi - printf '%s\n' "generic" -} + printf '%s\n' "generic" +} + +# Find the first card exposing a recognized hardware playback route. +audio_alsa_find_playback_card() { + for aafpc_card in $(audio_alsa_card_indexes); do + aafpc_profile="$(audio_alsa_playback_profile "$aafpc_card")" + if [ "$aafpc_profile" != "generic" ]; then + printf '%s\n' "$aafpc_card" + return 0 + fi + done + + return 1 +} + +# List UCM card identifiers without returning their descriptive comments. +audio_alsa_ucm_cards() { + command -v alsaucm >/dev/null 2>&1 || return 1 + + audio_exec_with_timeout 5s alsaucm listcards 2>/dev/null | + sed -n 's/^[[:space:]]*[0-9][0-9]*:[[:space:]]*//p' +} + +# Run an alsaucm batch without depending on the caller's standard input. +# audio_exec_with_timeout backgrounds commands, and dash redirects an +# asynchronous command's unredirected stdin from /dev/null. Pass the batch as +# a positional parameter so alsaucm still receives every command on Ubuntu. +audio_alsa_ucm_batch() { + aaub_commands="$1" + + [ -n "$aaub_commands" ] || return 1 + + # $1 is expanded by the inner shell from the positional argument below. + # shellcheck disable=SC2016 + audio_exec_with_timeout 5s sh -c ' + printf "%s\n" "$1" | alsaucm -n -b - + ' sh "$aaub_commands" +} + +# Convert a UCM PCM value into a name safe for a separate aplay or arecord +# process. Remove alsa-lib's private namespace only around standard hardware +# PCMs. Reject other private names because they require the UCM manager's +# generated alsa-lib configuration and are not standalone direct PCM routes. +audio_alsa_direct_pcm_from_ucm_value() { + aadpfuv_pcm="$( + printf '%s\n' "$1" | + sed \ + -e 's/^_ucm[0-9][0-9]*[.]plughw:/plughw:/' \ + -e 's/^_ucm[0-9][0-9]*[.]hw:/hw:/' + )" + + [ -n "$aadpfuv_pcm" ] || return 1 + + if printf '%s\n' "$aadpfuv_pcm" | + grep -Eq '^_ucm[0-9]+[.]'; then + return 1 + fi + + printf '%s\n' "$aadpfuv_pcm" +} + +# Normalize public playback route aliases to the internal semantic names. +audio_normalize_playback_route() { + case "$1" in + auto) + printf '%s\n' auto + ;; + speaker|speakers) + printf '%s\n' speakers + ;; + headphone|headphones|headset) + printf '%s\n' headphones + ;; + null) + printf '%s\n' null + ;; + *) + printf '%s\n' "unsupported playback route: $1" >&2 + return 1 + ;; + esac +} + +# Normalize public capture route aliases to the internal semantic names. +audio_normalize_capture_route() { + case "$1" in + auto) + printf '%s\n' auto + ;; + mic|microphone) + printf '%s\n' mic + ;; + headset-mic|headset_mic|'headset mic') + printf '%s\n' headset-mic + ;; + null) + printf '%s\n' null + ;; + *) + printf '%s\n' "unsupported capture route: $1" >&2 + return 1 + ;; + esac +} + +# Discover one usable managed-backend playback route and print route|target. +audio_select_managed_playback_route() { + asmpr_backend="$1" + asmpr_requested="${2:-auto}" + + case "$asmpr_requested" in + auto) + asmpr_routes="speakers headphones" + ;; + speakers|headphones|null) + asmpr_routes="$asmpr_requested" + ;; + *) + printf '%s\n' \ + "unsupported managed playback route: $asmpr_requested" >&2 + return 1 + ;; + esac + + for asmpr_route in $asmpr_routes; do + asmpr_target="" + case "$asmpr_backend:$asmpr_route" in + pipewire:speakers) + asmpr_target="$(pw_default_speakers 2>/dev/null || true)" + ;; + pipewire:headphones) + asmpr_target="$(pw_default_headphones 2>/dev/null || true)" + ;; + pipewire:null) + asmpr_target="$(pw_default_null 2>/dev/null || true)" + ;; + pulseaudio:speakers) + asmpr_target="$(pa_default_speakers 2>/dev/null || true)" + ;; + pulseaudio:headphones) + asmpr_target="$(pa_default_headphones 2>/dev/null || true)" + ;; + pulseaudio:null) + asmpr_target="$(pa_default_null 2>/dev/null || true)" + ;; + *) + printf '%s\n' \ + "unsupported managed playback backend: $asmpr_backend" >&2 + return 1 + ;; + esac + + if [ -n "$asmpr_target" ]; then + printf '%s|%s\n' "$asmpr_route" "$asmpr_target" + return 0 + fi + + printf '%s\n' \ + "[AUDIO-ROUTE] managed playback route unavailable: backend=$asmpr_backend route=$asmpr_route" >&2 + done + + return 1 +} + +# Discover one usable managed-backend capture route and print route|target. +audio_select_managed_capture_route() { + asmcr_backend="$1" + asmcr_requested="${2:-auto}" + + case "$asmcr_requested" in + auto) + asmcr_routes="mic headset-mic" + ;; + mic|headset-mic|null) + asmcr_routes="$asmcr_requested" + ;; + *) + printf '%s\n' \ + "unsupported managed capture route: $asmcr_requested" >&2 + return 1 + ;; + esac + + for asmcr_route in $asmcr_routes; do + asmcr_target="" + case "$asmcr_backend:$asmcr_route" in + pipewire:mic) + asmcr_target="$(pw_default_mic 2>/dev/null || true)" + ;; + pipewire:headset-mic) + asmcr_target="$(pw_default_headset_mic 2>/dev/null || true)" + ;; + pipewire:null) + asmcr_target="$(pw_default_null_source 2>/dev/null || true)" + ;; + pulseaudio:mic) + asmcr_target="$(pa_default_mic 2>/dev/null || true)" + ;; + pulseaudio:headset-mic) + asmcr_target="$(pa_default_headset_mic 2>/dev/null || true)" + ;; + pulseaudio:null) + asmcr_target="$(pa_default_null_source 2>/dev/null || true)" + ;; + *) + printf '%s\n' \ + "unsupported managed capture backend: $asmcr_backend" >&2 + return 1 + ;; + esac + + if [ -n "$asmcr_target" ]; then + printf '%s|%s\n' "$asmcr_route" "$asmcr_target" + return 0 + fi + + printf '%s\n' \ + "[AUDIO-ROUTE] managed capture route unavailable: backend=$asmcr_backend route=$asmcr_route" >&2 + done + + return 1 +} + +# Select the effective ALSA playback target without changing an explicit user +# override. Auto-discovered raw hardware PCMs use ALSA's plug layer so valid +# WAV formats are converted to the capabilities exposed by the hardware PCM. +audio_playback_effective_alsa_device() { + apead_device="$1" + apead_selection="${2:-auto}" + + [ -n "$apead_device" ] || return 1 + + case "$apead_selection" in + auto) + case "$apead_device" in + hw:*) + printf '%s\n' "plughw:${apead_device#hw:}" + ;; + *) + printf '%s\n' "$apead_device" + ;; + esac + ;; + explicit) + printf '%s\n' "$apead_device" + ;; + *) + printf '%s\n' \ + "unsupported ALSA playback-device selection: $apead_selection" >&2 + return 1 + ;; + esac +} + +# List the HiFi devices exposed by one UCM card without changing card state. +audio_alsa_ucm_hifi_devices() { + aauhd_card="$1" + aauhd_commands="" + + [ -n "$aauhd_card" ] || return 1 + + case "$aauhd_card" in + *\"*) + return 1 + ;; + esac + + aauhd_commands="$( + printf 'open "%s"\nlist _devices/HiFi\n' "$aauhd_card" + )" + audio_alsa_ucm_batch "$aauhd_commands" 2>/dev/null | + sed -n 's/^[[:space:]]*[0-9][0-9]*:[[:space:]]*//p' +} + +# Print the exact PlaybackPCM declared for one UCM HiFi device. +audio_alsa_ucm_playback_pcm() { + aaupp_card="$1" + aaupp_device="$2" + aaupp_commands="" + aaupp_output="" + aaupp_pcm="" + + [ -n "$aaupp_card" ] && [ -n "$aaupp_device" ] || return 1 + + case "$aaupp_card$aaupp_device" in + *\"*) + return 1 + ;; + esac + + aaupp_commands="$( + printf 'open "%s"\ngetval "=PlaybackPCM/%s/HiFi"\n' \ + "$aaupp_card" "$aaupp_device" + )" + aaupp_output="$( + audio_alsa_ucm_batch "$aaupp_commands" 2>/dev/null + )" || return 1 + aaupp_pcm="$( + printf '%s\n' "$aaupp_output" | + sed -n '/[^[:space:]]/ { + s/^[[:space:]]*// + s/[[:space:]]*$// + p + q + }' + )" + [ -n "$aaupp_pcm" ] || return 1 + + audio_alsa_direct_pcm_from_ucm_value "$aaupp_pcm" +} + +# Return the UCM device-name expression for a semantic playback selection. +audio_alsa_ucm_route_pattern() { + case "$1" in + speakers) + printf '%s\n' 'speaker|line[ _-]*out|internal[ _-]*speaker' + ;; + headphones) + printf '%s\n' 'headphone|headset|3[.]5|(^|[^[:alnum:]])hsj([^[:alnum:]]|$)' + ;; + *) + return 1 + ;; + esac +} + +# Resolve semantic route selection to PCM, UCM card, and UCM device fields. +audio_alsa_find_ucm_playback_route() { + aafur_route="$1" + aafur_pattern="$(audio_alsa_ucm_route_pattern "$aafur_route")" || return 1 + aafur_cards="$(audio_alsa_ucm_cards)" || return 1 + aafur_old_ifs=$IFS + IFS=' +' + + for aafur_card in $aafur_cards; do + aafur_devices="$(audio_alsa_ucm_hifi_devices "$aafur_card")" + for aafur_device in $aafur_devices; do + if ! printf '%s\n' "$aafur_device" | + grep -Eiq "$aafur_pattern"; then + continue + fi + + aafur_pcm="$( + audio_alsa_ucm_playback_pcm "$aafur_card" "$aafur_device" + )" + if [ -n "$aafur_pcm" ]; then + IFS=$aafur_old_ifs + printf '%s|%s|%s\n' \ + "$aafur_pcm" "$aafur_card" "$aafur_device" + return 0 + fi + done + done + + IFS=$aafur_old_ifs + return 1 +} + +# Reset UCM and enable one or two exact dynamically discovered HiFi devices. +audio_alsa_enable_ucm_hifi_devices() { + aaeupr_card="$1" + aaeupr_device="$2" + aaeupr_second_device="${3:-}" + aaeupr_commands="" + aaeupr_output="" + aaeupr_enabled_devices="" + + [ -n "$aaeupr_card" ] && [ -n "$aaeupr_device" ] || return 1 + + case "$aaeupr_card$aaeupr_device$aaeupr_second_device" in + *\"*) + return 1 + ;; + esac + + if [ -n "$aaeupr_second_device" ]; then + aaeupr_commands="$( + printf 'open "%s"\nreset\nset _verb HiFi\nset _enadev "%s"\nset _enadev "%s"\nlist1 _enadevs\n' \ + "$aaeupr_card" "$aaeupr_device" "$aaeupr_second_device" + )" + else + aaeupr_commands="$( + printf 'open "%s"\nreset\nset _verb HiFi\nset _enadev "%s"\nlist1 _enadevs\n' \ + "$aaeupr_card" "$aaeupr_device" + )" + fi + + aaeupr_output="$( + audio_alsa_ucm_batch "$aaeupr_commands" 2>/dev/null + )" || return 1 + aaeupr_enabled_devices="$( + printf '%s\n' "$aaeupr_output" | + sed -n 's/^[[:space:]]*[0-9][0-9]*:[[:space:]]*//p' + )" + + if ! printf '%s\n' "$aaeupr_enabled_devices" | + grep -F -x "$aaeupr_device" >/dev/null 2>&1; then + printf '%s\n' \ + "UCM device '$aaeupr_device' was not reported enabled on card '$aaeupr_card'" >&2 + return 1 + fi -# Find the first card exposing a recognized hardware playback route. -audio_alsa_find_playback_card() { - for aafpc_card in $(audio_alsa_card_indexes); do - aafpc_profile="$(audio_alsa_playback_profile "$aafpc_card")" - if [ "$aafpc_profile" != "generic" ]; then - printf '%s\n' "$aafpc_card" - return 0 - fi - done + if [ -n "$aaeupr_second_device" ] && + ! printf '%s\n' "$aaeupr_enabled_devices" | + grep -F -x "$aaeupr_second_device" >/dev/null 2>&1; then + printf '%s\n' \ + "UCM device '$aaeupr_second_device' was not reported enabled on card '$aaeupr_card'" >&2 + return 1 + fi - return 1 + return 0 } -# Enable UCM first, then apply only mixer controls present for the detected -# primary-MI2S, secondary-TDM, codec-direct, or legacy playback path. +# Enable the selected UCM route when available, then retain legacy mixer +# preparation only for images without a usable speaker UCM route. audio_playback_alsa_prepare() { ap_card="${1:-}" + ap_ucm_card="${2:-}" + ap_ucm_device="${3:-}" ap_profile="" - ap_ucm_card="" + ap_route_record="" 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 + if [ -z "$ap_ucm_card" ] || [ -z "$ap_ucm_device" ]; then + ap_route_record="$( + audio_alsa_find_ucm_playback_route \ + "${SINK_CHOICE:-speakers}" 2>/dev/null || true + )" + ap_ucm_card="$(printf '%s\n' "$ap_route_record" | cut -d'|' -f2)" + ap_ucm_device="$(printf '%s\n' "$ap_route_record" | cut -d'|' -f3)" + fi + + if [ -n "$ap_ucm_card" ] && [ -n "$ap_ucm_device" ]; then + if ! audio_alsa_enable_ucm_hifi_devices \ + "$ap_ucm_card" "$ap_ucm_device"; then + printf '%s\n' \ + "could not enable UCM device '$ap_ucm_device' on card '$ap_ucm_card'" >&2 + return 1 + fi + + return 0 fi fi + if [ "${SINK_CHOICE:-speakers}" = "headphones" ]; then + printf '%s\n' \ + 'headphones playback requires a matching UCM HiFi device' >&2 + return 1 + fi + if ! command -v amixer >/dev/null 2>&1; then return 0 fi @@ -3374,7 +4060,7 @@ EOF # Prefer the PCM associated with a detected hardware route, then retain the # existing default, sysdefault, and first-device fallback order. -audio_playback_pick_alsa_sink() { +audio_playback_pick_legacy_alsa_sink() { ap_dev="" ap_card="" ap_profile="generic" @@ -3430,12 +4116,86 @@ audio_playback_pick_alsa_sink() { return 1 } +# Resolve the selected semantic ALSA route without encoding card or PCM IDs. +audio_playback_resolve_alsa_route() { + apar_route="${1:-${SINK_CHOICE:-speakers}}" + apar_record="" + apar_device="" + + case "$apar_route" in + null) + printf '%s\n' 'null||' + return 0 + ;; + speakers|headphones) + ;; + *) + printf '%s\n' "unsupported ALSA playback route: $apar_route" >&2 + return 1 + ;; + esac + + apar_record="$( + audio_alsa_find_ucm_playback_route "$apar_route" 2>/dev/null || true + )" + if [ -n "$apar_record" ]; then + printf '%s\n' "$apar_record" + return 0 + fi + + if [ "$apar_route" = "headphones" ]; then + printf '%s\n' \ + 'no UCM HiFi headphones device with a PlaybackPCM value was discovered' >&2 + return 1 + fi + + apar_device="$(audio_playback_pick_legacy_alsa_sink)" + if [ -n "$apar_device" ]; then + printf '%s||\n' "$apar_device" + return 0 + fi + + return 1 +} + +# Print only the PCM field for existing callers that need a playback device. +audio_playback_pick_alsa_sink() { + appas_record="$( + audio_playback_resolve_alsa_route "${SINK_CHOICE:-speakers}" + )" || return 1 + + printf '%s\n' "$appas_record" | cut -d'|' -f1 +} + # Prepare the selected card before verifying that its playback PCM can open. +# An explicit device is opened exactly as supplied after semantic-route checks. audio_playback_alsa_probe() { - ap_probe_dev="$(audio_playback_pick_alsa_sink)" + ap_probe_requested_device="${1:-}" + ap_probe_selection="auto" + case "$ap_probe_requested_device" in + *[[:space:]]*) + printf '%s\n' \ + "[ALSA-PROBE] explicit playback device contains whitespace: $ap_probe_requested_device" >&2 + return 1 + ;; + esac + ap_probe_route="$( + audio_playback_resolve_alsa_route "${SINK_CHOICE:-speakers}" + )" || return 1 + ap_probe_route_dev="$(printf '%s\n' "$ap_probe_route" | cut -d'|' -f1)" + if [ -n "$ap_probe_requested_device" ]; then + ap_probe_route_dev="$ap_probe_requested_device" + ap_probe_selection="explicit" + fi + ap_probe_dev="$( + audio_playback_effective_alsa_device \ + "$ap_probe_route_dev" "$ap_probe_selection" + )" + ap_probe_ucm_card="$(printf '%s\n' "$ap_probe_route" | cut -d'|' -f2)" + ap_probe_ucm_device="$(printf '%s\n' "$ap_probe_route" | cut -d'|' -f3)" if [ -z "$ap_probe_dev" ]; then printf '%s\n' \ - '[ALSA-PROBE] no playback candidate was discovered from aplay inventory' >&2 + "[ALSA-PROBE] no ${SINK_CHOICE:-speakers} playback route was discovered from UCM or ALSA inventory" >&2 return 1 fi @@ -3445,9 +4205,23 @@ audio_playback_alsa_probe() { ap_probe_profile="$(audio_alsa_playback_profile "$ap_probe_card")" fi + if [ -n "$ap_probe_requested_device" ] && + [ -n "$ap_probe_ucm_card" ] && + [ -n "$ap_probe_card" ] && + [ "$ap_probe_ucm_card" != "$ap_probe_card" ]; then + printf '%s\n' \ + "[ALSA-PROBE] explicit playback device card $ap_probe_card does not match the discovered ${SINK_CHOICE:-speakers} UCM card $ap_probe_ucm_card" >&2 + return 1 + fi + printf '%s\n' \ - "[ALSA-PROBE] candidate=$ap_probe_dev card=${ap_probe_card:-unknown} profile=$ap_probe_profile" >&2 - audio_playback_alsa_prepare "$ap_probe_card" >/dev/null 2>&1 || true + "[ALSA-PROBE] discovered_pcm=$ap_probe_route_dev effective_target=$ap_probe_dev card=${ap_probe_card:-unknown} profile=$ap_probe_profile ucm_card=${ap_probe_ucm_card:-none} ucm_device=${ap_probe_ucm_device:-none}" >&2 + if ! audio_playback_alsa_prepare \ + "$ap_probe_card" "$ap_probe_ucm_card" "$ap_probe_ucm_device"; then + printf '%s\n' \ + "[ALSA-PROBE] route preparation failed: choice=${SINK_CHOICE:-speakers} device=$ap_probe_dev" >&2 + return 1 + fi printf '%s\n' \ "[ALSA-PROBE] exec: aplay -D $ap_probe_dev -t raw -f S16_LE -r 48000 -c 2 -d 1 /dev/zero" >&2 @@ -3472,7 +4246,7 @@ audio_playback_alsa_probe() { # Print the successfully probed ALSA playback device for propagation from a # desktop Audio-user subprocess back into the root orchestrator. audio_playback_alsa_probe_device() { - if ! audio_playback_alsa_probe; then + if ! audio_playback_alsa_probe "${1:-}"; then return 1 fi @@ -3499,6 +4273,7 @@ audio_alsa_playback_inventory_available() { # Probe direct ALSA playback in the prepared user context. When no playback PCM # exists, root may start one fully provisioned offline audio DSP and retry. audio_playback_probe_alsa_with_recovery() { + appawr_requested_device="${1:-}" appawr_log="${AUDIO_ALSA_PLAYBACK_PROBE_LOG:-/dev/stderr}" if [ "$appawr_log" != "/dev/stderr" ]; then printf '\n%s\n' \ @@ -3508,6 +4283,7 @@ audio_playback_probe_alsa_with_recovery() { appawr_output="$( audio_run_helper_as_test_user audio_playback_alsa_probe_device \ + "$appawr_requested_device" \ 2>>"$appawr_log" )" appawr_rc=$? @@ -3518,9 +4294,14 @@ audio_playback_probe_alsa_with_recovery() { return 0 fi - appawr_candidate="$( - audio_run_helper_as_test_user audio_playback_pick_alsa_sink 2>/dev/null || true - )" + if [ -n "$appawr_requested_device" ]; then + appawr_candidate="$appawr_requested_device" + else + appawr_candidate="$( + audio_run_helper_as_test_user \ + audio_playback_pick_alsa_sink 2>/dev/null || true + )" + fi if [ -n "$appawr_candidate" ]; then printf '%s\n' \ "[ALSA-PROBE] candidate remains present after failed open: $appawr_candidate" \ @@ -3559,6 +4340,7 @@ audio_playback_probe_alsa_with_recovery() { appawr_output="$( audio_run_helper_as_test_user audio_playback_alsa_probe_device \ + "$appawr_requested_device" \ 2>>"$appawr_log" )" appawr_rc=$? @@ -3572,23 +4354,190 @@ audio_playback_probe_alsa_with_recovery() { return 0 } -# Enable UCM first, then configure any exposed VA-DMIC or legacy capture route. +# Probe the requested ALSA playback route, or discover speakers then headphones. +audio_playback_probe_selected_alsa_with_recovery() { + appsawr_requested_device="${1:-}" + appsawr_requested_route="${SINK_CHOICE:-auto}" + appsawr_reasons="" + + case "$appsawr_requested_device" in + *[[:space:]]*) + AUDIO_ALSA_PLAYBACK_REASON="explicit playback device contains whitespace: $appsawr_requested_device" + export AUDIO_ALSA_PLAYBACK_REASON + return 1 + ;; + esac + + case "$appsawr_requested_route" in + auto) + appsawr_routes="speakers headphones" + ;; + speakers|headphones) + appsawr_routes="$appsawr_requested_route" + ;; + *) + AUDIO_ALSA_PLAYBACK_REASON="unsupported ALSA playback route: $appsawr_requested_route" + export AUDIO_ALSA_PLAYBACK_REASON + return 1 + ;; + esac + + for appsawr_route in $appsawr_routes; do + SINK_CHOICE="$appsawr_route" + export SINK_CHOICE + printf '%s\n' \ + "[AUDIO-ROUTE] probing ALSA playback route=$appsawr_route device=${appsawr_requested_device:-auto}" >&2 + + if audio_playback_probe_alsa_with_recovery \ + "$appsawr_requested_device"; then + AUDIO_PLAYBACK_ROUTE_SELECTED="$appsawr_route" + AUDIO_ALSA_PLAYBACK_REASON="" + export AUDIO_PLAYBACK_ROUTE_SELECTED AUDIO_ALSA_PLAYBACK_REASON + return 0 + fi + + if [ -n "$appsawr_reasons" ]; then + appsawr_reasons="$appsawr_reasons, $appsawr_route unavailable" + else + appsawr_reasons="$appsawr_route unavailable" + fi + done + + SINK_CHOICE="$appsawr_requested_route" + AUDIO_ALSA_PLAYBACK_REASON="$appsawr_reasons" + export SINK_CHOICE AUDIO_ALSA_PLAYBACK_REASON + return 1 +} + +# Print the exact CapturePCM declared for one UCM HiFi device. +audio_alsa_ucm_capture_pcm() { + aaucp_card="$1" + aaucp_device="$2" + aaucp_commands="" + aaucp_output="" + aaucp_pcm="" + + [ -n "$aaucp_card" ] && [ -n "$aaucp_device" ] || return 1 + + case "$aaucp_card$aaucp_device" in + *\"*) + return 1 + ;; + esac + + aaucp_commands="$( + printf 'open "%s"\ngetval "=CapturePCM/%s/HiFi"\n' \ + "$aaucp_card" "$aaucp_device" + )" + aaucp_output="$( + audio_alsa_ucm_batch "$aaucp_commands" 2>/dev/null + )" || return 1 + aaucp_pcm="$( + printf '%s\n' "$aaucp_output" | + sed -n '/[^[:space:]]/ { + s/^[[:space:]]*// + s/[[:space:]]*$// + p + q + }' + )" + [ -n "$aaucp_pcm" ] || return 1 + + audio_alsa_direct_pcm_from_ucm_value "$aaucp_pcm" +} + +# Return the UCM device-name expression for a semantic capture selection. +audio_alsa_ucm_capture_route_pattern() { + case "$1" in + mic) + printf '%s\n' 'mic|microphone|dmic|audio[ _-]*input|capture' + ;; + headset-mic) + printf '%s\n' 'headphone|headset|3[.]5|hsj|hsmic' + ;; + *) + return 1 + ;; + esac +} + +# Resolve semantic capture selection to PCM, UCM card, and UCM device fields. +audio_alsa_find_ucm_capture_route() { + aafuc_route="$1" + aafuc_pattern="$( + audio_alsa_ucm_capture_route_pattern "$aafuc_route" + )" || return 1 + aafuc_cards="$(audio_alsa_ucm_cards)" || return 1 + aafuc_old_ifs=$IFS + IFS=' +' + + for aafuc_card in $aafuc_cards; do + aafuc_devices="$(audio_alsa_ucm_hifi_devices "$aafuc_card")" + for aafuc_device in $aafuc_devices; do + if ! printf '%s\n' "$aafuc_device" | + grep -Eiq "$aafuc_pattern"; then + continue + fi + + if [ "$aafuc_route" = "mic" ] && + printf '%s\n' "$aafuc_device" | + grep -Eiq 'headphone|headset|3[.]5|hsj|hsmic'; then + continue + fi + + aafuc_pcm="$( + audio_alsa_ucm_capture_pcm "$aafuc_card" "$aafuc_device" + )" + if [ -n "$aafuc_pcm" ]; then + IFS=$aafuc_old_ifs + printf '%s|%s|%s\n' \ + "$aafuc_pcm" "$aafuc_card" "$aafuc_device" + return 0 + fi + done + done + + IFS=$aafuc_old_ifs + return 1 +} + +# Enable the selected UCM route when available, then retain legacy mixer +# preparation only for images without a usable built-in microphone UCM route. audio_record_alsa_prepare_capture() { ar_card="${1:-}" - ar_ucm_card="" + ar_ucm_card="${2:-}" + ar_ucm_device="${3:-}" + ar_route_record="" 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 + if [ -z "$ar_ucm_card" ] || [ -z "$ar_ucm_device" ]; then + ar_route_record="$( + audio_alsa_find_ucm_capture_route \ + "${SRC_CHOICE:-mic}" 2>/dev/null || true + )" + ar_ucm_card="$(printf '%s\n' "$ar_route_record" | cut -d'|' -f2)" + ar_ucm_device="$(printf '%s\n' "$ar_route_record" | cut -d'|' -f3)" + fi + + if [ -n "$ar_ucm_card" ] && [ -n "$ar_ucm_device" ]; then + if ! audio_alsa_enable_ucm_hifi_devices \ + "$ar_ucm_card" "$ar_ucm_device"; then + printf '%s\n' \ + "could not enable UCM device '$ar_ucm_device' on card '$ar_ucm_card'" >&2 + return 1 + fi + + return 0 fi fi + if [ "${SRC_CHOICE:-mic}" = "headset-mic" ]; then + printf '%s\n' \ + 'headset capture requires a matching UCM HiFi device' >&2 + return 1 + fi + if ! command -v amixer >/dev/null 2>&1; then return 0 fi @@ -3625,7 +4574,7 @@ EOF # Prefer the capture PCM implied by the available VA DMA controls, falling back # to the first ALSA capture device for platforms with other routing schemes. -audio_record_pick_alsa_capture() { +audio_record_pick_legacy_alsa_capture() { ar_dev="" ar_card="" @@ -3664,6 +4613,53 @@ audio_record_pick_alsa_capture() { return 1 } +# Resolve the selected semantic ALSA capture route without fixed card/PCM IDs. +audio_record_resolve_alsa_route() { + arar_route="${1:-${SRC_CHOICE:-mic}}" + arar_record="" + arar_device="" + + case "$arar_route" in + mic|headset-mic) + ;; + *) + printf '%s\n' "unsupported ALSA capture route: $arar_route" >&2 + return 1 + ;; + esac + + arar_record="$( + audio_alsa_find_ucm_capture_route "$arar_route" 2>/dev/null || true + )" + if [ -n "$arar_record" ]; then + printf '%s\n' "$arar_record" + return 0 + fi + + if [ "$arar_route" = "headset-mic" ]; then + printf '%s\n' \ + 'no UCM HiFi headset capture device with a CapturePCM value was discovered' >&2 + return 1 + fi + + arar_device="$(audio_record_pick_legacy_alsa_capture)" + if [ -n "$arar_device" ]; then + printf '%s||\n' "$arar_device" + return 0 + fi + + return 1 +} + +# Print only the PCM field for existing callers that need a capture device. +audio_record_pick_alsa_capture() { + arpac_record="$( + audio_record_resolve_alsa_route "${SRC_CHOICE:-mic}" + )" || return 1 + + printf '%s\n' "$arpac_record" | cut -d'|' -f1 +} + audio_record_alsa_capture_probe() { ar_probe_dev="$(audio_record_pick_alsa_capture)" if [ -z "$ar_probe_dev" ]; then @@ -3757,10 +4753,45 @@ audio_probe_alsa_capture_profile() { } probe_devices="" + probe_route_record="$( + audio_record_resolve_alsa_route "${SRC_CHOICE:-mic}" 2>/dev/null || true + )" + probe_ucm_card="$( + printf '%s\n' "$probe_route_record" | cut -d'|' -f2 + )" + probe_ucm_device="$( + printf '%s\n' "$probe_route_record" | cut -d'|' -f3 + )" if [ -n "$requested_capture_device" ]; then + case "$requested_capture_device" in + *[[:space:]]*) + AUDIO_ALSA_CAPTURE_REASON="explicit capture device contains whitespace: $requested_capture_device" + probe_cleanup + return 1 + ;; + esac + + if [ -z "$probe_route_record" ]; then + AUDIO_ALSA_CAPTURE_REASON="the ${SRC_CHOICE:-mic} capture route was not discovered for explicit device $requested_capture_device" + probe_cleanup + return 1 + fi + + requested_capture_card="$( + audio_alsa_device_card "$requested_capture_device" + )" + if [ -n "$probe_ucm_card" ] && + [ -n "$requested_capture_card" ] && + [ "$probe_ucm_card" != "$requested_capture_card" ]; then + AUDIO_ALSA_CAPTURE_REASON="explicit capture device card $requested_capture_card does not match the discovered ${SRC_CHOICE:-mic} UCM card $probe_ucm_card" + probe_cleanup + return 1 + fi + probe_devices="$requested_capture_device" else - cand="$(alsa_pick_capture 2>/dev/null || true)" + cand="$(printf '%s\n' "$probe_route_record" | cut -d'|' -f1)" + if [ -n "$cand" ]; then probe_devices="$cand" case "$cand" in @@ -3773,7 +4804,14 @@ audio_probe_alsa_capture_profile() { 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)" + extra_devices="" + if [ -z "$probe_ucm_card" ] && [ "${SRC_CHOICE:-mic}" = "mic" ]; then + extra_devices="$( + sed -n \ + 's/^\([0-9][0-9]*\)-\([0-9][0-9]*\):.*capture.*/hw:\1,\2/p' \ + /proc/asound/pcm 2>/dev/null + )" + fi if [ -n "$extra_devices" ]; then for dev in $extra_devices; do seen=0 @@ -3797,7 +4835,11 @@ audio_probe_alsa_capture_profile() { if [ -z "$probe_devices" ]; then # shellcheck disable=SC2034 - AUDIO_ALSA_CAPTURE_REASON="no ALSA capture device candidates found" + if [ "${SRC_CHOICE:-mic}" = "headset-mic" ]; then + AUDIO_ALSA_CAPTURE_REASON="no UCM HiFi headset capture device with a CapturePCM value was discovered" + else + AUDIO_ALSA_CAPTURE_REASON="no ALSA capture device candidates found" + fi probe_cleanup return 1 fi @@ -3805,7 +4847,12 @@ audio_probe_alsa_capture_profile() { for dev in $probe_devices; do # Mixer controls and PCM devices must come from the same ALSA card. probe_card="$(audio_alsa_device_card "$dev")" - audio_record_alsa_prepare_capture "$probe_card" >/dev/null 2>&1 || true + if ! audio_record_alsa_prepare_capture \ + "$probe_card" "$probe_ucm_card" "$probe_ucm_device"; then + AUDIO_ALSA_CAPTURE_REASON="could not prepare the ${SRC_CHOICE:-mic} capture route for $dev" + probe_cleanup + return 1 + fi for combo in \ "S16_LE 48000 1" \ @@ -6115,6 +7162,80 @@ audio_find_desktop_audio_user() { return 1 } +# Configure Ubuntu command execution from runtime desktop-session evidence. +# Headless Ubuntu Server stays in the current root context for direct ALSA. +audio_configure_ubuntu_desktop_session() { + acud_os_id="${1:-unknown}" + acud_user="" + acud_uid="" + acud_home="" + acud_passwd_entry="" + acud_runtime_dir="" + + AUDIO_USE_DESKTOP_SESSION=0 + export AUDIO_USE_DESKTOP_SESSION + + if [ "$acud_os_id" != "ubuntu" ] || + [ "$(id -u 2>/dev/null || echo 1)" -ne 0 ]; then + return 0 + fi + + acud_user="$(audio_find_desktop_audio_user 2>/dev/null || true)" + if [ -z "$acud_user" ]; then + log_info "No active Ubuntu desktop Audio session was found, keeping direct system context" + return 0 + fi + + acud_uid="$(id -u "$acud_user" 2>/dev/null || true)" + if [ -z "$acud_uid" ]; then + log_warn "Could not resolve the active Ubuntu Audio user uid: $acud_user" + return 0 + fi + + acud_runtime_dir="/run/user/$acud_uid" + if [ ! -d "$acud_runtime_dir" ] || + [ ! -S "$acud_runtime_dir/bus" ]; then + log_info "Ubuntu Audio runtime lacks a desktop D-Bus session, keeping direct system context: $acud_runtime_dir" + return 0 + fi + + if command -v getent >/dev/null 2>&1; then + acud_passwd_entry="$( + getent passwd "$acud_user" 2>/dev/null | + sed -n '1p' + )" + else + acud_passwd_entry="$( + awk -F: -v requested_user="$acud_user" \ + '$1 == requested_user { print; exit }' \ + /etc/passwd 2>/dev/null + )" + fi + + acud_home="$( + printf '%s\n' "$acud_passwd_entry" | + awk -F: 'NR == 1 { print $6 }' + )" + [ -n "$acud_home" ] || acud_home="/home/$acud_user" + + AUDIO_TEST_USER="$acud_user" + AUDIO_TEST_UID="$acud_uid" + AUDIO_TEST_HOME="$acud_home" + AUDIO_TEST_RUNTIME_DIR="$acud_runtime_dir" + AUDIO_TEST_DBUS_ADDRESS="unix:path=$acud_runtime_dir/bus" + AUDIO_USE_DESKTOP_SESSION=1 + + export AUDIO_TEST_USER + export AUDIO_TEST_UID + export AUDIO_TEST_HOME + export AUDIO_TEST_RUNTIME_DIR + export AUDIO_TEST_DBUS_ADDRESS + export AUDIO_USE_DESKTOP_SESSION + + log_pass "Using active Ubuntu desktop Audio session: user=$acud_user uid=$acud_uid" + return 0 +} + # Platform behavior: # Debian and CentOS: # - require the caller to be root @@ -6791,6 +7912,60 @@ audio_record_probe_alsa_capture_profile() { return 1 } +# Probe the requested ALSA capture route, or discover mic then headset-mic. +audio_record_probe_selected_alsa_capture_profile() { + arpsacp_requested_device="${1:-}" + arpsacp_requested_route="${SRC_CHOICE:-auto}" + arpsacp_reasons="" + + case "$arpsacp_requested_device" in + *[[:space:]]*) + AUDIO_ALSA_CAPTURE_REASON="explicit capture device contains whitespace: $arpsacp_requested_device" + export AUDIO_ALSA_CAPTURE_REASON + return 1 + ;; + esac + + case "$arpsacp_requested_route" in + auto) + arpsacp_routes="mic headset-mic" + ;; + mic|headset-mic) + arpsacp_routes="$arpsacp_requested_route" + ;; + *) + AUDIO_ALSA_CAPTURE_REASON="unsupported ALSA capture route: $arpsacp_requested_route" + export AUDIO_ALSA_CAPTURE_REASON + return 1 + ;; + esac + + for arpsacp_route in $arpsacp_routes; do + SRC_CHOICE="$arpsacp_route" + export SRC_CHOICE + printf '%s\n' \ + "[AUDIO-ROUTE] probing ALSA capture route=$arpsacp_route device=${arpsacp_requested_device:-auto}" >&2 + + if audio_record_probe_alsa_capture_profile \ + "$arpsacp_requested_device"; then + AUDIO_CAPTURE_ROUTE_SELECTED="$arpsacp_route" + export AUDIO_CAPTURE_ROUTE_SELECTED + return 0 + fi + + if [ -n "$arpsacp_reasons" ]; then + arpsacp_reasons="$arpsacp_reasons, $arpsacp_route: ${AUDIO_ALSA_CAPTURE_REASON:-unavailable}" + else + arpsacp_reasons="$arpsacp_route: ${AUDIO_ALSA_CAPTURE_REASON:-unavailable}" + fi + done + + SRC_CHOICE="$arpsacp_requested_route" + AUDIO_ALSA_CAPTURE_REASON="$arpsacp_reasons" + export SRC_CHOICE AUDIO_ALSA_CAPTURE_REASON + return 1 +} + # Prepare the Debian or CentOS Audio test user and re-execute the current test # as that user. # @@ -7629,7 +8804,7 @@ EOF } # audio_validate_wav_file -# +# [min-distinct-samples] # Validate WAV structure and basic signal integrity. Advanced signal metrics # remain diagnostic unless strict-signal mode is explicitly enabled. audio_validate_wav_file() { @@ -7641,6 +8816,7 @@ audio_validate_wav_file() { avrw_expected_seconds="${6:-0}" avrw_log="${7:-}" avrw_scope="${8:-record}" + avrw_min_distinct="${9:-${AUDIO_RECORD_MIN_DISTINCT_SAMPLES:-4}}" avrw_validator="${AUDIO_WAV_VALIDATOR:-${TOOLS:-}/audio_wav_validate.py}" AUDIO_WAV_VALIDATION_SUMMARY="" @@ -7658,7 +8834,7 @@ audio_validate_wav_file() { --analyze-bytes "${AUDIO_RECORD_ANALYZE_BYTES:-4194304}" \ --min-active-samples "${AUDIO_RECORD_MIN_ACTIVE_SAMPLES:-100}" \ --sample-threshold-lsb "${AUDIO_RECORD_SAMPLE_THRESHOLD_LSB:-${AUDIO_RECORD_SAMPLE_THRESHOLD:-8}}" \ - --min-distinct-samples "${AUDIO_RECORD_MIN_DISTINCT_SAMPLES:-4}" \ + --min-distinct-samples "$avrw_min_distinct" \ --min-duration-ratio "${AUDIO_RECORD_MIN_DURATION_RATIO:-0.70}" \ --strict-signal "${AUDIO_RECORD_STRICT_SIGNAL:-0}" \ --min-rms-dbfs "${AUDIO_RECORD_MIN_RMS_DBFS:--60}" \ @@ -7819,7 +8995,7 @@ audio_validate_playback_wav() { # # Args: # $1 file -# $2 source kind: mic|null +# $2 source kind: mic|headset-mic|null # $3 expected rate, 0 disables the rate check # $4 expected channels, 0 disables the channel check # $5 expected recording duration diff --git a/Runner/utils/audio_wav_validate.py b/Runner/utils/audio_wav_validate.py index 08c80ad9..d10193a3 100755 --- a/Runner/utils/audio_wav_validate.py +++ b/Runner/utils/audio_wav_validate.py @@ -42,6 +42,9 @@ class WaveInfo: available_data_bytes: int usable_data_bytes: int file_bytes: int + post_data_bytes: int + post_data_chunks: tuple[str, ...] + post_data_layout_valid: bool @property def frames(self) -> int: @@ -115,6 +118,34 @@ def _format_name(code: int) -> str: }.get(code, f"format-{code}") +def inspect_post_data_chunks( + handle: BinaryIO, + start_offset: int, + file_bytes: int, +) -> tuple[bool, tuple[str, ...]]: + """Return whether bytes after data are complete RIFF chunks.""" + chunk_names: list[str] = [] + offset = start_offset + + while offset < file_bytes: + if offset + 8 > file_bytes: + return False, tuple(chunk_names) + + handle.seek(offset) + chunk_header = _read_exact(handle, 8) + chunk_id = chunk_header[0:4] + chunk_size = struct.unpack_from(" file_bytes: + return False, tuple(chunk_names) + + chunk_name = chunk_id.decode("ascii", errors="replace").strip() or "unknown" + chunk_names.append(chunk_name) + offset = next_offset + + return offset == file_bytes, tuple(chunk_names) + + def parse_wave(path: str, max_header_bytes: int = 1024 * 1024) -> WaveInfo: file_bytes = os.path.getsize(path) if file_bytes < 44: @@ -131,6 +162,9 @@ def parse_wave(path: str, max_header_bytes: int = 1024 * 1024) -> WaveInfo: fmt_payload: bytes | None = None data_offset: int | None = None declared_data_bytes: int | None = None + post_data_bytes = 0 + post_data_chunks: tuple[str, ...] = () + post_data_layout_valid = True while handle.tell() + 8 <= file_bytes: if handle.tell() > max_header_bytes: @@ -173,6 +207,20 @@ def parse_wave(path: str, max_header_bytes: int = 1024 * 1024) -> WaveInfo: if data_offset is None or declared_data_bytes is None: raise WavValidationError("missing-data-chunk") + available_after_data_offset = max(0, file_bytes - data_offset) + if declared_data_bytes not in {0, 0xFFFFFFFF}: + data_end = data_offset + declared_data_bytes + post_data_bytes = max(0, file_bytes - data_end) + post_chunk_offset = data_end + (declared_data_bytes & 1) + if data_end <= file_bytes and post_chunk_offset <= file_bytes: + post_data_layout_valid, post_data_chunks = inspect_post_data_chunks( + handle, + post_chunk_offset, + file_bytes, + ) + elif declared_data_bytes <= available_after_data_offset: + post_data_layout_valid = False + if len(fmt_payload) < 16: raise WavValidationError("truncated-fmt-chunk") @@ -251,6 +299,9 @@ def parse_wave(path: str, max_header_bytes: int = 1024 * 1024) -> WaveInfo: available_data_bytes=available_data_bytes, usable_data_bytes=usable_data_bytes, file_bytes=file_bytes, + post_data_bytes=post_data_bytes, + post_data_chunks=post_data_chunks, + post_data_layout_valid=post_data_layout_valid, ) @@ -422,6 +473,17 @@ def emit(status: str, reason: str, info: WaveInfo | None, stats: SignalStats | N ("available_data_bytes", info.available_data_bytes), ("usable_data_bytes", info.usable_data_bytes), ("duration_s", f"{info.duration_seconds:.3f}"), + ("post_data_bytes", info.post_data_bytes), + ( + "post_data_chunks", + ",".join(info.post_data_chunks) + if info.post_data_chunks + else "none", + ), + ( + "post_data_layout", + "valid" if info.post_data_layout_valid else "unrecognized", + ), ] ) @@ -472,7 +534,11 @@ def emit(status: str, reason: str, info: WaveInfo | None, stats: SignalStats | N def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--file", required=True) - parser.add_argument("--source-kind", choices=("mic", "null"), default="mic") + parser.add_argument( + "--source-kind", + choices=("mic", "headset-mic", "null"), + default="mic", + ) parser.add_argument("--expect-rate", type=int, default=0) parser.add_argument("--expect-channels", type=int, default=0) parser.add_argument("--expect-bits", type=int, default=0) @@ -543,7 +609,10 @@ def main() -> int: elif info.declared_data_bytes not in {0xFFFFFFFF, info.available_data_bytes}: if info.declared_data_bytes > info.available_data_bytes: warnings.append("declared-data-exceeds-file") - elif info.declared_data_bytes < info.available_data_bytes: + elif ( + info.declared_data_bytes < info.available_data_bytes + and not info.post_data_layout_valid + ): warnings.append("trailing-bytes-after-data") if args.expected_seconds > 0.0: