From 8d14295733be405af38b33c1f30ff64c960a5095 Mon Sep 17 00:00:00 2001 From: Srikanth Muppandam Date: Tue, 29 Sep 2026 10:25:58 +0530 Subject: [PATCH 1/2] utils: add generic ALSA route helpers Add bounded playback and capture route discovery using runtime ALSA PCM and mixer inventories. Reject ambiguous mappings, preserve probe failures, generate deterministic playback audio, and restore mixer state after route preparation. Extend the WAV fallback so strict recording thresholds remain enforced when Python is unavailable. Signed-off-by: Srikanth Muppandam --- Runner/utils/audio_common.sh | 1423 +++++++++++++++++++++++++++++++++- 1 file changed, 1408 insertions(+), 15 deletions(-) diff --git a/Runner/utils/audio_common.sh b/Runner/utils/audio_common.sh index ae5c744e..8a6f9c61 100755 --- a/Runner/utils/audio_common.sh +++ b/Runner/utils/audio_common.sh @@ -3053,19 +3053,1388 @@ audio_alsa_set_control_if_present() { cset "iface=MIXER,name=$aascip_name" "$aascip_value" >/dev/null 2>&1 } -# List unique ALSA card indexes from procfs and the playback/capture inventories. -audio_alsa_card_indexes() { +# audio_alsa_route_run [arguments...] +# Run one ALSA route-discovery or mixer command with a fixed finite bound. The +# command stdout/stderr and return code are preserved, and no state is retained. +audio_alsa_route_run() { + audio_exec_with_timeout 5s env LC_ALL=C "$@" +} + +# audio_alsa_route_control_exists +# Return 0 only when the exact control is listed within the bounded probe. The +# function emits no stdout and does not mutate mixer state. +audio_alsa_route_control_exists() { + aarce_card="$1" + aarce_name="$2" + aarce_controls="" + + command -v amixer >/dev/null 2>&1 || return 1 + + aarce_controls="$( + audio_alsa_route_run amixer -c "$aarce_card" controls + )" + aarce_rc=$? + if [ "$aarce_rc" -ne 0 ]; then + printf '%s\n' \ + "amixer controls probe failed or timed out for card $aarce_card, rc=$aarce_rc" >&2 + return 2 + fi + + printf '%s\n' "$aarce_controls" | + grep -F "name='$aarce_name'" >/dev/null 2>&1 +} + +# audio_generate_u8_stereo_tone [rate] [frequency] +# Generate a bounded unsigned 8-bit stereo square wave without external audio +# assets. The function emits no stdout, replaces only the requested output, +# and returns 0 only when the exact expected byte count was written. +audio_generate_u8_stereo_tone() { + agust_output="$1" + agust_seconds="$2" + agust_rate="${3:-48000}" + agust_frequency="${4:-1000}" + + if [ -z "$agust_output" ] || + ! is_unsigned_number "$agust_seconds" || + ! is_unsigned_number "$agust_rate" || + ! is_unsigned_number "$agust_frequency" || + [ "$agust_seconds" -le 0 ] || + [ "$agust_rate" -le 0 ] || + [ "$agust_frequency" -le 0 ]; then + printf '%s\n' 'invalid tone output, duration, rate, or frequency' >&2 + return 1 + fi + + agust_half_period=$((agust_rate / (agust_frequency * 2))) + if [ "$agust_half_period" -le 0 ]; then + printf '%s\n' \ + "tone frequency $agust_frequency is too high for rate $agust_rate" >&2 + return 1 + fi + + agust_frames=$((agust_seconds * agust_rate)) + agust_expected_bytes=$((agust_frames * 2)) + rm -f "$agust_output" + + if ! audio_exec_with_timeout 15s env LC_ALL=C awk \ + -v frames="$agust_frames" \ + -v half_period="$agust_half_period" ' + BEGIN { + cycle_frames = half_period * 2 + cycle = "" + for (frame = 0; frame < cycle_frames; frame++) { + value = frame < half_period ? 80 : 176 + cycle = cycle sprintf("%c%c", value, value) + } + + cycles = int(frames / cycle_frames) + for (cycle_index = 0; cycle_index < cycles; cycle_index++) { + printf "%s", cycle + } + + remaining = frames - (cycles * cycle_frames) + for (frame = 0; frame < remaining; frame++) { + value = frame < half_period ? 80 : 176 + printf "%c%c", value, value + } + } + ' >"$agust_output"; then + rm -f "$agust_output" + printf '%s\n' "could not generate playback tone: $agust_output" >&2 + return 1 + fi + + agust_actual_bytes="$(file_size_bytes "$agust_output" 2>/dev/null || echo 0)" + if [ "$agust_actual_bytes" -ne "$agust_expected_bytes" ] 2>/dev/null; then + printf '%s\n' \ + "playback tone size mismatch: expected=$agust_expected_bytes observed=${agust_actual_bytes:-0}" >&2 + return 1 + fi + + return 0 +} + +# audio_alsa_playback_route_name_pattern +# Print one extended regular expression for generic playback PCM and mixer +# names. Returns 1 for an unsupported route and has no side effects. +audio_alsa_playback_route_name_pattern() { + aaprnp_route="$1" + + case "$aaprnp_route" in + hdmi) + printf '%s\n' 'hdmi|high[ _-]*definition[ _-]*multimedia' + ;; + displayport) + printf '%s\n' 'display[ _-]*port|(^|[^[:alnum:]])e?dp[0-9]*([^[:alnum:]]|$)' + ;; + headphones) + printf '%s\n' 'headphone|headset|3[.]5|(^|[^[:alnum:]])hsj([^[:alnum:]]|$)|(^|[^[:alnum:]])hp[ _-]*(out|rx)([^[:alnum:]]|$)' + ;; + *) + return 1 + ;; + esac +} + +# audio_alsa_playback_pcm_device_for_label +# Print the first exact-label playback device number on one card. Procfs is +# preferred, with aplay inventory as fallback. Returns 1 when no match exists. +audio_alsa_playback_pcm_device_for_label() { + aappdfl_card="$1" + aappdfl_label="$2" + aappdfl_devices="" + + if [ -r /proc/asound/pcm ]; then + aappdfl_devices="$({ + awk -v wanted_card="$aappdfl_card" -v wanted_label="$aappdfl_label" ' + { + key = $1 + sub(/:$/, "", key) + split(key, parts, "-") + line = tolower($0) + label = tolower(wanted_label) + label_pos = index(line, label) + label_after = substr(line, label_pos + length(label), 1) + + if ((parts[1] + 0) == wanted_card && + label_pos > 0 && + label_after !~ /[[:alnum:]_]/ && + line ~ /playback/) { + device = parts[2] + 0 + if (!seen[device]++) print device + } + } + ' /proc/asound/pcm + } 2>/dev/null)" + fi + + if [ -z "$aappdfl_devices" ] && command -v aplay >/dev/null 2>&1; then + aappdfl_inventory="$(audio_alsa_route_run aplay -l)" + aappdfl_rc=$? + if [ "$aappdfl_rc" -ne 0 ]; then + printf '%s\n' \ + "aplay inventory failed or timed out while resolving '$aappdfl_label', rc=$aappdfl_rc" >&2 + return 2 + fi + + aappdfl_devices="$({ + printf '%s\n' "$aappdfl_inventory" | + awk -v wanted_card="$aappdfl_card" -v wanted_label="$aappdfl_label" ' + { + line = tolower($0) + label = tolower(wanted_label) + label_pos = index(line, label) + label_after = substr(line, label_pos + length(label), 1) + parsed = $0 + + if (line !~ /^card[[:space:]]+[0-9]+:/ || + label_pos == 0 || + label_after ~ /[[:alnum:]_]/) { + next + } + + sub(/^card[[:space:]]+/, "", parsed) + split(parsed, card_parts, ":") + if ((card_parts[1] + 0) != wanted_card) { + next + } + + sub(/^.*device[[:space:]]+/, "", parsed) + sub(/:.*/, "", parsed) + device = parsed + 0 + if (!seen[device]++) print device + } + ' + } 2>/dev/null)" + fi + + if [ -z "$aappdfl_devices" ]; then + return 1 + fi + + aappdfl_count="$( + printf '%s\n' "$aappdfl_devices" | + sed '/^[[:space:]]*$/d' | + wc -l | + tr -d '[:space:]' + )" + if [ "${aappdfl_count:-0}" -gt 1 ] 2>/dev/null; then + printf '%s\n' \ + "PCM label '$aappdfl_label' matches multiple playback devices on card $aappdfl_card, provide --alsa-device" >&2 + return 2 + fi + + printf '%s\n' "$aappdfl_devices" | sed -n '1p' +} + +# audio_alsa_playback_control_is_output +# Return 0 when a route-matching control is safe to treat as playback. Headset +# microphone, capture, and TX controls are rejected for the headphones route. +audio_alsa_playback_control_is_output() { + aapcio_route="$1" + aapcio_control="$2" + + if [ "$aapcio_route" != "headphones" ]; then + return 0 + fi + + if printf '%s\n' "$aapcio_control" | + grep -Eiq 'mic|microphone|capture|record|(^|[^[:alnum:]])tx([^[:alnum:]]|$)'; then + return 1 + fi + + return 0 +} + +# audio_alsa_resolve_playback_route [control] [value] [label] [device] +# Resolve a generic or explicitly selected playback route. On success, export +# AUDIO_ALSA_ROUTE_* selection fields and return 0. Diagnostics go to stderr, +# and the function does not mutate mixer state. +audio_alsa_resolve_playback_route() { + aarp_route="$1" + aarp_requested_control="${2:-}" + aarp_requested_value="${3:-}" + aarp_requested_label="${4:-}" + aarp_requested_device="${5:-}" + aarp_pattern="" + + AUDIO_ALSA_ROUTE_CARD="" + AUDIO_ALSA_ROUTE_DEVICE="" + AUDIO_ALSA_ROUTE_CONTROL="" + AUDIO_ALSA_ROUTE_VALUE="" + AUDIO_ALSA_ROUTE_PCM_LABEL="" + AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE="" + AUDIO_ALSA_ROUTE_DISCOVERY="" + + aarp_pattern="$( + audio_alsa_playback_route_name_pattern "$aarp_route" + )" || { + printf '%s\n' "unsupported audio route: $aarp_route" >&2 + return 1 + } + + if [ -n "$aarp_requested_device" ]; then + AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE="$aarp_requested_device" + AUDIO_ALSA_ROUTE_CARD="$(audio_alsa_device_card "$aarp_requested_device")" + AUDIO_ALSA_ROUTE_DEVICE="$({ + printf '%s\n' "$aarp_requested_device" | + sed -n 's/^\(plug\)\{0,1\}hw:[0-9][0-9]*,\([0-9][0-9]*\).*/\2/p' + } 2>/dev/null)" + AUDIO_ALSA_ROUTE_CONTROL="$aarp_requested_control" + if [ -n "$AUDIO_ALSA_ROUTE_CONTROL" ]; then + AUDIO_ALSA_ROUTE_VALUE="${aarp_requested_value:-1}" + fi + AUDIO_ALSA_ROUTE_PCM_LABEL="${aarp_requested_label:-explicit-device}" + AUDIO_ALSA_ROUTE_DISCOVERY="explicit-device" + + if [ -n "$AUDIO_ALSA_ROUTE_CONTROL" ]; then + if [ -z "$AUDIO_ALSA_ROUTE_CARD" ]; then + printf '%s\n' \ + "a numeric hw:CARD,DEVICE or plughw:CARD,DEVICE is required when a mixer control is supplied with an ALSA device" >&2 + return 1 + fi + + audio_alsa_route_control_exists \ + "$AUDIO_ALSA_ROUTE_CARD" "$AUDIO_ALSA_ROUTE_CONTROL" + aarp_control_rc=$? + case "$aarp_control_rc" in + 0) + ;; + 2) + return 1 + ;; + *) + printf '%s\n' \ + "requested mixer control is absent on ALSA card $AUDIO_ALSA_ROUTE_CARD: $AUDIO_ALSA_ROUTE_CONTROL" >&2 + return 1 + ;; + esac + fi + + export AUDIO_ALSA_ROUTE_CARD AUDIO_ALSA_ROUTE_DEVICE + export AUDIO_ALSA_ROUTE_CONTROL AUDIO_ALSA_ROUTE_VALUE + export AUDIO_ALSA_ROUTE_PCM_LABEL + export AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE AUDIO_ALSA_ROUTE_DISCOVERY + return 0 + fi + + aarp_cards="$(audio_alsa_card_indexes playback)" + aarp_cards_rc=$? + if [ "$aarp_cards_rc" -ne 0 ]; then + return 1 + fi + + if [ -n "$aarp_requested_label" ]; then + aarp_label_match_count=0 + for aarp_card in $aarp_cards; do + aarp_device="$( + audio_alsa_playback_pcm_device_for_label \ + "$aarp_card" "$aarp_requested_label" + )" + aarp_device_rc=$? + + if [ "$aarp_device_rc" -eq 2 ]; then + return 1 + fi + + if [ -z "$aarp_device" ]; then + continue + fi + + if [ -n "$aarp_requested_control" ]; then + audio_alsa_route_control_exists \ + "$aarp_card" "$aarp_requested_control" + aarp_control_rc=$? + case "$aarp_control_rc" in + 0) + ;; + 2) + return 1 + ;; + *) + continue + ;; + esac + fi + + aarp_label_match_count=$((aarp_label_match_count + 1)) + aarp_label_card="$aarp_card" + aarp_label_device="$aarp_device" + done + + if [ "$aarp_label_match_count" -gt 1 ]; then + printf '%s\n' \ + "PCM label '$aarp_requested_label' matches multiple playback cards, provide --alsa-device" >&2 + return 1 + fi + + if [ "$aarp_label_match_count" -eq 1 ]; then + AUDIO_ALSA_ROUTE_CARD="$aarp_label_card" + AUDIO_ALSA_ROUTE_DEVICE="$aarp_label_device" + AUDIO_ALSA_ROUTE_CONTROL="$aarp_requested_control" + if [ -n "$AUDIO_ALSA_ROUTE_CONTROL" ]; then + AUDIO_ALSA_ROUTE_VALUE="${aarp_requested_value:-1}" + fi + AUDIO_ALSA_ROUTE_PCM_LABEL="$aarp_requested_label" + AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE="plughw:$aarp_label_card,$aarp_label_device" + AUDIO_ALSA_ROUTE_DISCOVERY="pcm-label" + export AUDIO_ALSA_ROUTE_CARD AUDIO_ALSA_ROUTE_DEVICE + export AUDIO_ALSA_ROUTE_CONTROL AUDIO_ALSA_ROUTE_VALUE + export AUDIO_ALSA_ROUTE_PCM_LABEL + export AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE AUDIO_ALSA_ROUTE_DISCOVERY + return 0 + fi + fi + + if [ -z "$aarp_requested_control" ] && + [ -z "$aarp_requested_label" ] && + command -v aplay >/dev/null 2>&1; then + aarp_aplay_inventory="$(audio_alsa_route_run aplay -l)" + aarp_aplay_rc=$? + if [ "$aarp_aplay_rc" -ne 0 ]; then + printf '%s\n' \ + "aplay inventory failed or timed out during $aarp_route discovery, rc=$aarp_aplay_rc" >&2 + return 1 + fi + + aarp_pcm_lines="$( + printf '%s\n' "$aarp_aplay_inventory" | + grep -Ei "$aarp_pattern" + )" + + aarp_pcm_count="$({ + printf '%s\n' "$aarp_pcm_lines" | + sed '/^[[:space:]]*$/d' | + wc -l | + tr -d '[:space:]' + } 2>/dev/null)" + + if [ "${aarp_pcm_count:-0}" -gt 1 ] 2>/dev/null; then + printf '%s\n' \ + "multiple $aarp_route playback PCMs were discovered, provide --pcm-label or --alsa-device" >&2 + printf '%s\n' "$aarp_pcm_lines" >&2 + return 1 + fi + + aarp_pcm_line="$(printf '%s\n' "$aarp_pcm_lines" | sed -n '1p')" + if [ -n "$aarp_pcm_line" ]; then + AUDIO_ALSA_ROUTE_CARD="$({ + printf '%s\n' "$aarp_pcm_line" | + sed -n 's/^card[[:space:]]*\([0-9][0-9]*\):.*/\1/p' + } 2>/dev/null)" + AUDIO_ALSA_ROUTE_DEVICE="$({ + printf '%s\n' "$aarp_pcm_line" | + sed -n 's/^card[[:space:]]*[0-9][0-9]*:.*device[[:space:]]*\([0-9][0-9]*\):.*/\1/p' + } 2>/dev/null)" + AUDIO_ALSA_ROUTE_PCM_LABEL="$({ + printf '%s\n' "$aarp_pcm_line" | + sed -n 's/^.*device[[:space:]]*[0-9][0-9]*:[[:space:]]*\([^[]*\).*/\1/p' | + sed 's/[[:space:]]*$//' + } 2>/dev/null)" + + if [ -n "$AUDIO_ALSA_ROUTE_CARD" ] && + [ -n "$AUDIO_ALSA_ROUTE_DEVICE" ]; then + AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE="plughw:$AUDIO_ALSA_ROUTE_CARD,$AUDIO_ALSA_ROUTE_DEVICE" + AUDIO_ALSA_ROUTE_DISCOVERY="pcm-name" + export AUDIO_ALSA_ROUTE_CARD AUDIO_ALSA_ROUTE_DEVICE + export AUDIO_ALSA_ROUTE_CONTROL AUDIO_ALSA_ROUTE_VALUE + export AUDIO_ALSA_ROUTE_PCM_LABEL + export AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE AUDIO_ALSA_ROUTE_DISCOVERY + return 0 + fi + fi + fi + + if ! command -v amixer >/dev/null 2>&1; then + printf '%s\n' "amixer is unavailable for mixer-route discovery" >&2 + return 1 + fi + + aarp_control_match_count=0 + for aarp_card in $aarp_cards; do + aarp_control_inventory="$( + audio_alsa_route_run amixer -c "$aarp_card" controls + )" + aarp_control_inventory_rc=$? + if [ "$aarp_control_inventory_rc" -ne 0 ]; then + printf '%s\n' \ + "amixer controls probe failed or timed out for card $aarp_card, rc=$aarp_control_inventory_rc" >&2 + return 1 + fi + + if [ -n "$aarp_requested_control" ]; then + aarp_controls="$({ + printf '%s\n' "$aarp_control_inventory" | + sed -n "s/.*name='\([^']*\)'.*/\1/p" | + grep -F -x "$aarp_requested_control" + } 2>/dev/null)" + else + aarp_controls="$({ + printf '%s\n' "$aarp_control_inventory" | + sed -n "s/.*name='\([^']*\)'.*/\1/p" | + grep -Ei "$aarp_pattern" + } 2>/dev/null)" + fi + + if [ -z "$aarp_controls" ]; then + continue + fi + + while [ -n "$aarp_controls" ]; do + aarp_control="$(printf '%s\n' "$aarp_controls" | sed -n '1p')" + aarp_controls="$(printf '%s\n' "$aarp_controls" | sed '1d')" + + if ! audio_alsa_playback_control_is_output \ + "$aarp_route" "$aarp_control"; then + printf '%s\n' \ + "ignoring capture-oriented control during $aarp_route playback discovery: $aarp_control" >&2 + continue + fi + + if [ -n "$aarp_requested_label" ]; then + aarp_label="$aarp_requested_label" + else + aarp_label="$({ + printf '%s\n' "$aarp_control" | + sed -n 's/.*\(MultiMedia[0-9][0-9]*\).*/\1/p' + } 2>/dev/null)" + fi + + if [ -z "$aarp_label" ]; then + printf '%s\n' \ + "mixer control has no discoverable PCM label, provide a PCM label or ALSA device override: $aarp_control" >&2 + continue + fi + + aarp_device="$( + audio_alsa_playback_pcm_device_for_label \ + "$aarp_card" "$aarp_label" + )" + aarp_device_rc=$? + + if [ "$aarp_device_rc" -eq 2 ]; then + return 1 + fi + + if [ -z "$aarp_device" ]; then + printf '%s\n' \ + "no playback PCM matches $aarp_label on ALSA card $aarp_card" >&2 + continue + fi + + aarp_control_match_count=$((aarp_control_match_count + 1)) + aarp_control_card="$aarp_card" + aarp_control_device="$aarp_device" + aarp_control_name="$aarp_control" + aarp_control_label="$aarp_label" + done + done + + if [ "$aarp_control_match_count" -gt 1 ]; then + printf '%s\n' \ + "multiple $aarp_route mixer-to-PCM mappings were discovered, provide an exact --mixer-control with --pcm-label or --alsa-device" >&2 + return 1 + fi + + if [ "$aarp_control_match_count" -eq 1 ]; then + AUDIO_ALSA_ROUTE_CARD="$aarp_control_card" + AUDIO_ALSA_ROUTE_DEVICE="$aarp_control_device" + AUDIO_ALSA_ROUTE_CONTROL="$aarp_control_name" + AUDIO_ALSA_ROUTE_VALUE="${aarp_requested_value:-1}" + AUDIO_ALSA_ROUTE_PCM_LABEL="$aarp_control_label" + AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE="plughw:$aarp_control_card,$aarp_control_device" + AUDIO_ALSA_ROUTE_DISCOVERY="mixer-control" + export AUDIO_ALSA_ROUTE_CARD AUDIO_ALSA_ROUTE_DEVICE + export AUDIO_ALSA_ROUTE_CONTROL AUDIO_ALSA_ROUTE_VALUE + export AUDIO_ALSA_ROUTE_PCM_LABEL + export AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE AUDIO_ALSA_ROUTE_DISCOVERY + return 0 + fi + + printf '%s\n' \ + "no $aarp_route playback route was found from PCM names, mixer controls, or supplied overrides" >&2 + return 1 +} + +# audio_alsa_find_playback_route [control] [value] [label] [device] +# Print card|device|control|value|pcm-label|playback-device after resolution. +# Diagnostics go to stderr, and the function does not mutate mixer state. +audio_alsa_find_playback_route() { + if ! audio_alsa_resolve_playback_route "$@"; then + return 1 + fi + + printf '%s|%s|%s|%s|%s|%s\n' \ + "$AUDIO_ALSA_ROUTE_CARD" \ + "$AUDIO_ALSA_ROUTE_DEVICE" \ + "$AUDIO_ALSA_ROUTE_CONTROL" \ + "$AUDIO_ALSA_ROUTE_VALUE" \ + "$AUDIO_ALSA_ROUTE_PCM_LABEL" \ + "$AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE" +} + +# audio_alsa_prepare_playback_route +# Snapshot and apply the already resolved AUDIO_ALSA_ROUTE_* mixer selection. +# The function emits no stdout, exports restoration state, and returns 1 when +# no route is resolved or mixer snapshot/mutation fails. +audio_alsa_prepare_playback_route() { + if [ -z "${AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE:-}" ]; then + printf '%s\n' 'no playback route has been resolved' >&2 + return 1 + fi + + AUDIO_ALSA_ROUTE_MIXER_CHANGED=0 + AUDIO_ALSA_ROUTE_PREVIOUS_VALUE="" + + if [ -z "$AUDIO_ALSA_ROUTE_CONTROL" ]; then + export AUDIO_ALSA_ROUTE_PREVIOUS_VALUE AUDIO_ALSA_ROUTE_MIXER_CHANGED + return 0 + fi + + AUDIO_ALSA_ROUTE_PREVIOUS_VALUE="$({ + audio_alsa_route_run amixer -c "$AUDIO_ALSA_ROUTE_CARD" \ + cget "iface=MIXER,name=$AUDIO_ALSA_ROUTE_CONTROL" 2>/dev/null | + sed -n 's/^[[:space:]]*: values=//p' | + sed -n '1p' + } 2>/dev/null)" + + if [ -z "$AUDIO_ALSA_ROUTE_PREVIOUS_VALUE" ]; then + printf '%s\n' \ + "could not snapshot mixer control: $AUDIO_ALSA_ROUTE_CONTROL" >&2 + return 1 + fi + + AUDIO_ALSA_ROUTE_MIXER_CHANGED=1 + export AUDIO_ALSA_ROUTE_PREVIOUS_VALUE AUDIO_ALSA_ROUTE_MIXER_CHANGED + + if ! audio_alsa_route_run amixer -c "$AUDIO_ALSA_ROUTE_CARD" \ + cset "iface=MIXER,name=$AUDIO_ALSA_ROUTE_CONTROL" \ + "$AUDIO_ALSA_ROUTE_VALUE" >/dev/null 2>&1; then + audio_alsa_restore_playback_route >/dev/null 2>&1 || true + printf '%s\n' \ + "could not set playback mixer control $AUDIO_ALSA_ROUTE_CONTROL to $AUDIO_ALSA_ROUTE_VALUE" >&2 + return 1 + fi + + return 0 +} + +# audio_alsa_restore_playback_route +# Restore the mixer value saved by audio_alsa_prepare_playback_route. The +# function emits no stdout and returns 1 only when required state is missing or +# the restore operation fails. +audio_alsa_restore_playback_route() { + case "${AUDIO_ALSA_ROUTE_MIXER_CHANGED:-0}" in + 1) + ;; + *) + return 0 + ;; + esac + + if [ -z "${AUDIO_ALSA_ROUTE_CARD:-}" ] || + [ -z "${AUDIO_ALSA_ROUTE_CONTROL:-}" ] || + [ -z "${AUDIO_ALSA_ROUTE_PREVIOUS_VALUE:-}" ]; then + return 1 + fi + + audio_alsa_route_run amixer -c "$AUDIO_ALSA_ROUTE_CARD" \ + cset "iface=MIXER,name=$AUDIO_ALSA_ROUTE_CONTROL" \ + "$AUDIO_ALSA_ROUTE_PREVIOUS_VALUE" >/dev/null 2>&1 + aarpr_rc=$? + if [ "$aarpr_rc" -eq 0 ]; then + AUDIO_ALSA_ROUTE_MIXER_CHANGED=0 + export AUDIO_ALSA_ROUTE_MIXER_CHANGED + fi + return "$aarpr_rc" +} + +# audio_alsa_capture_playback_route_inventory +# Write ALSA card, playback PCM, mixer, and UCM inventory to the requested +# artifact. The function emits no stdout and does not mutate target state. +audio_alsa_capture_playback_route_inventory() { + aacpri_output="$1" + + if [ -z "$aacpri_output" ]; then + return 1 + fi + { + printf '%s\n' '### /proc/asound/cards' + if [ -r /proc/asound/cards ]; then + cat /proc/asound/cards + else + printf '%s\n' '/proc/asound/cards is unavailable' + fi + + printf '%s\n' '### /proc/asound/pcm' + if [ -r /proc/asound/pcm ]; then + cat /proc/asound/pcm + else + printf '%s\n' '/proc/asound/pcm is unavailable' + fi + + printf '%s\n' '### aplay -l' + if command -v aplay >/dev/null 2>&1; then + audio_alsa_route_run aplay -l || + printf '%s\n' 'aplay -l failed or timed out' + else + printf '%s\n' 'aplay is unavailable' + fi + + for aacpri_card in $(audio_alsa_card_indexes); do + printf '### amixer -c %s controls\n' "$aacpri_card" + if command -v amixer >/dev/null 2>&1; then + audio_alsa_route_run amixer -c "$aacpri_card" controls || + printf '%s\n' "amixer controls failed or timed out for card $aacpri_card" + else + printf '%s\n' 'amixer is unavailable' + fi + done + + printf '%s\n' '### alsaucm listcards' + if command -v alsaucm >/dev/null 2>&1; then + audio_alsa_route_run alsaucm listcards || + printf '%s\n' 'alsaucm listcards failed or timed out' + else + printf '%s\n' 'alsaucm is unavailable' + fi + } >"$aacpri_output" 2>&1 +} + +# audio_alsa_capture_route_name_pattern +# Print the generic headset-microphone expression used for ALSA capture +# discovery. Returns 1 for an unsupported source and has no side effects. +audio_alsa_capture_route_name_pattern() { + aacrnp_source="$1" + + case "$aacrnp_source" in + headset-mic) + printf '%s\n' 'head(set|phone)[ _-]*(mic|microphone)|3[.]5.*(mic|microphone)|(mic|microphone).*3[.]5|(^|[^[:alnum:]])hsj?[ _-]*(mic|tx)([^[:alnum:]]|$)|(^|[^[:alnum:]])hsmic([^[:alnum:]]|$)' + ;; + *) + return 1 + ;; + esac +} + +# audio_alsa_capture_pcm_device_for_label +# Print the first exact-label capture device number on one ALSA card. Procfs is +# preferred, with arecord inventory as fallback. Returns 1 when no match exists. +audio_alsa_capture_pcm_device_for_label() { + aacpdfl_card="$1" + aacpdfl_label="$2" + aacpdfl_devices="" + + if [ -r /proc/asound/pcm ]; then + aacpdfl_devices="$({ + awk -v wanted_card="$aacpdfl_card" -v wanted_label="$aacpdfl_label" ' + { + key = $1 + sub(/:$/, "", key) + split(key, parts, "-") + line = tolower($0) + label = tolower(wanted_label) + label_pos = index(line, label) + label_after = substr(line, label_pos + length(label), 1) + + if ((parts[1] + 0) == wanted_card && + label_pos > 0 && + label_after !~ /[[:alnum:]_]/ && + line ~ /capture/) { + device = parts[2] + 0 + if (!seen[device]++) print device + } + } + ' /proc/asound/pcm + } 2>/dev/null)" + fi + + if [ -z "$aacpdfl_devices" ] && command -v arecord >/dev/null 2>&1; then + aacpdfl_inventory="$(audio_alsa_route_run arecord -l)" + aacpdfl_rc=$? + if [ "$aacpdfl_rc" -ne 0 ]; then + printf '%s\n' \ + "arecord inventory failed or timed out while resolving '$aacpdfl_label', rc=$aacpdfl_rc" >&2 + return 2 + fi + + aacpdfl_devices="$({ + printf '%s\n' "$aacpdfl_inventory" | + awk -v wanted_card="$aacpdfl_card" -v wanted_label="$aacpdfl_label" ' + { + line = tolower($0) + label = tolower(wanted_label) + label_pos = index(line, label) + label_after = substr(line, label_pos + length(label), 1) + parsed = $0 + + if (line !~ /^card[[:space:]]+[0-9]+:/ || + label_pos == 0 || + label_after ~ /[[:alnum:]_]/) { + next + } + + sub(/^card[[:space:]]+/, "", parsed) + split(parsed, card_parts, ":") + if ((card_parts[1] + 0) != wanted_card) { + next + } + + sub(/^.*device[[:space:]]+/, "", parsed) + sub(/:.*/, "", parsed) + device = parsed + 0 + if (!seen[device]++) print device + } + ' + } 2>/dev/null)" + fi + + if [ -z "$aacpdfl_devices" ]; then + return 1 + fi + + aacpdfl_count="$( + printf '%s\n' "$aacpdfl_devices" | + sed '/^[[:space:]]*$/d' | + wc -l | + tr -d '[:space:]' + )" + if [ "${aacpdfl_count:-0}" -gt 1 ] 2>/dev/null; then + printf '%s\n' \ + "PCM label '$aacpdfl_label' matches multiple capture devices on card $aacpdfl_card, provide --alsa-device" >&2 + return 2 + fi + + printf '%s\n' "$aacpdfl_devices" | sed -n '1p' +} + +# audio_alsa_resolve_capture_route [control] [value] [label] [device] +# Resolve a generic or explicitly selected capture route. On success, export +# AUDIO_ALSA_CAPTURE_ROUTE_* fields and return 0. Diagnostics go to stderr, +# and the function does not mutate mixer state. +audio_alsa_resolve_capture_route() { + aacrr_source="$1" + aacrr_requested_control="${2:-}" + aacrr_requested_value="${3:-}" + aacrr_requested_label="${4:-}" + aacrr_requested_device="${5:-}" + aacrr_pattern="" + + AUDIO_ALSA_CAPTURE_ROUTE_CARD="" + AUDIO_ALSA_CAPTURE_ROUTE_DEVICE="" + AUDIO_ALSA_CAPTURE_ROUTE_CONTROL="" + AUDIO_ALSA_CAPTURE_ROUTE_VALUE="" + AUDIO_ALSA_CAPTURE_ROUTE_PCM_LABEL="" + AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME="" + AUDIO_ALSA_CAPTURE_ROUTE_DISCOVERY="" + + aacrr_pattern="$( + audio_alsa_capture_route_name_pattern "$aacrr_source" + )" || { + printf '%s\n' "unsupported audio capture source: $aacrr_source" >&2 + return 1 + } + + if [ -n "$aacrr_requested_device" ]; then + AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME="$aacrr_requested_device" + AUDIO_ALSA_CAPTURE_ROUTE_CARD="$( + audio_alsa_device_card "$aacrr_requested_device" + )" + AUDIO_ALSA_CAPTURE_ROUTE_DEVICE="$({ + printf '%s\n' "$aacrr_requested_device" | + sed -n 's/^\(plug\)\{0,1\}hw:[0-9][0-9]*,\([0-9][0-9]*\).*/\2/p' + } 2>/dev/null)" + AUDIO_ALSA_CAPTURE_ROUTE_CONTROL="$aacrr_requested_control" + if [ -n "$AUDIO_ALSA_CAPTURE_ROUTE_CONTROL" ]; then + AUDIO_ALSA_CAPTURE_ROUTE_VALUE="${aacrr_requested_value:-1}" + fi + AUDIO_ALSA_CAPTURE_ROUTE_PCM_LABEL="${aacrr_requested_label:-explicit-device}" + AUDIO_ALSA_CAPTURE_ROUTE_DISCOVERY="explicit-device" + + if [ -n "$AUDIO_ALSA_CAPTURE_ROUTE_CONTROL" ]; then + if [ -z "$AUDIO_ALSA_CAPTURE_ROUTE_CARD" ]; then + printf '%s\n' \ + "a numeric hw:CARD,DEVICE or plughw:CARD,DEVICE is required when a mixer control is supplied with an ALSA device" >&2 + return 1 + fi + + audio_alsa_route_control_exists \ + "$AUDIO_ALSA_CAPTURE_ROUTE_CARD" \ + "$AUDIO_ALSA_CAPTURE_ROUTE_CONTROL" + aacrr_control_rc=$? + case "$aacrr_control_rc" in + 0) + ;; + 2) + return 1 + ;; + *) + printf '%s\n' \ + "requested capture mixer control is absent on ALSA card $AUDIO_ALSA_CAPTURE_ROUTE_CARD: $AUDIO_ALSA_CAPTURE_ROUTE_CONTROL" >&2 + return 1 + ;; + esac + fi + + export AUDIO_ALSA_CAPTURE_ROUTE_CARD AUDIO_ALSA_CAPTURE_ROUTE_DEVICE + export AUDIO_ALSA_CAPTURE_ROUTE_CONTROL AUDIO_ALSA_CAPTURE_ROUTE_VALUE + export AUDIO_ALSA_CAPTURE_ROUTE_PCM_LABEL + export AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME + export AUDIO_ALSA_CAPTURE_ROUTE_DISCOVERY + return 0 + fi + + aacrr_cards="$(audio_alsa_card_indexes capture)" + aacrr_cards_rc=$? + if [ "$aacrr_cards_rc" -ne 0 ]; then + return 1 + fi + + if [ -n "$aacrr_requested_label" ]; then + aacrr_label_match_count=0 + for aacrr_card in $aacrr_cards; do + aacrr_device="$( + audio_alsa_capture_pcm_device_for_label \ + "$aacrr_card" "$aacrr_requested_label" + )" + aacrr_device_rc=$? + + if [ "$aacrr_device_rc" -eq 2 ]; then + return 1 + fi + + if [ -z "$aacrr_device" ]; then + continue + fi + + if [ -n "$aacrr_requested_control" ]; then + audio_alsa_route_control_exists \ + "$aacrr_card" "$aacrr_requested_control" + aacrr_control_rc=$? + case "$aacrr_control_rc" in + 0) + ;; + 2) + return 1 + ;; + *) + continue + ;; + esac + fi + + aacrr_label_match_count=$((aacrr_label_match_count + 1)) + aacrr_label_card="$aacrr_card" + aacrr_label_device="$aacrr_device" + done + + if [ "$aacrr_label_match_count" -gt 1 ]; then + printf '%s\n' \ + "PCM label '$aacrr_requested_label' matches multiple capture cards, provide --alsa-device" >&2 + return 1 + fi + + if [ "$aacrr_label_match_count" -eq 1 ]; then + AUDIO_ALSA_CAPTURE_ROUTE_CARD="$aacrr_label_card" + AUDIO_ALSA_CAPTURE_ROUTE_DEVICE="$aacrr_label_device" + AUDIO_ALSA_CAPTURE_ROUTE_CONTROL="$aacrr_requested_control" + if [ -n "$AUDIO_ALSA_CAPTURE_ROUTE_CONTROL" ]; then + AUDIO_ALSA_CAPTURE_ROUTE_VALUE="${aacrr_requested_value:-1}" + fi + AUDIO_ALSA_CAPTURE_ROUTE_PCM_LABEL="$aacrr_requested_label" + AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME="plughw:$aacrr_label_card,$aacrr_label_device" + AUDIO_ALSA_CAPTURE_ROUTE_DISCOVERY="pcm-label" + export AUDIO_ALSA_CAPTURE_ROUTE_CARD AUDIO_ALSA_CAPTURE_ROUTE_DEVICE + export AUDIO_ALSA_CAPTURE_ROUTE_CONTROL AUDIO_ALSA_CAPTURE_ROUTE_VALUE + export AUDIO_ALSA_CAPTURE_ROUTE_PCM_LABEL + export AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME + export AUDIO_ALSA_CAPTURE_ROUTE_DISCOVERY + return 0 + fi + fi + + if [ -z "$aacrr_requested_control" ] && + [ -z "$aacrr_requested_label" ] && + command -v arecord >/dev/null 2>&1; then + aacrr_arecord_inventory="$(audio_alsa_route_run arecord -l)" + aacrr_arecord_rc=$? + if [ "$aacrr_arecord_rc" -ne 0 ]; then + printf '%s\n' \ + "arecord inventory failed or timed out during $aacrr_source discovery, rc=$aacrr_arecord_rc" >&2 + return 1 + fi + + aacrr_pcm_lines="$( + printf '%s\n' "$aacrr_arecord_inventory" | + grep -Ei "$aacrr_pattern" + )" + + aacrr_pcm_count="$({ + printf '%s\n' "$aacrr_pcm_lines" | + sed '/^[[:space:]]*$/d' | + wc -l | + tr -d '[:space:]' + } 2>/dev/null)" + + if [ "${aacrr_pcm_count:-0}" -gt 1 ] 2>/dev/null; then + printf '%s\n' \ + "multiple $aacrr_source capture PCMs were discovered, provide --pcm-label or --alsa-device" >&2 + printf '%s\n' "$aacrr_pcm_lines" >&2 + return 1 + fi + + aacrr_pcm_line="$(printf '%s\n' "$aacrr_pcm_lines" | sed -n '1p')" + if [ -n "$aacrr_pcm_line" ]; then + AUDIO_ALSA_CAPTURE_ROUTE_CARD="$({ + printf '%s\n' "$aacrr_pcm_line" | + sed -n 's/^card[[:space:]]*\([0-9][0-9]*\):.*/\1/p' + } 2>/dev/null)" + AUDIO_ALSA_CAPTURE_ROUTE_DEVICE="$({ + printf '%s\n' "$aacrr_pcm_line" | + sed -n 's/^card[[:space:]]*[0-9][0-9]*:.*device[[:space:]]*\([0-9][0-9]*\):.*/\1/p' + } 2>/dev/null)" + AUDIO_ALSA_CAPTURE_ROUTE_PCM_LABEL="$({ + printf '%s\n' "$aacrr_pcm_line" | + sed -n 's/^.*device[[:space:]]*[0-9][0-9]*:[[:space:]]*\([^[]*\).*/\1/p' | + sed 's/[[:space:]]*$//' + } 2>/dev/null)" + + if [ -n "$AUDIO_ALSA_CAPTURE_ROUTE_CARD" ] && + [ -n "$AUDIO_ALSA_CAPTURE_ROUTE_DEVICE" ]; then + AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME="plughw:$AUDIO_ALSA_CAPTURE_ROUTE_CARD,$AUDIO_ALSA_CAPTURE_ROUTE_DEVICE" + AUDIO_ALSA_CAPTURE_ROUTE_DISCOVERY="pcm-name" + export AUDIO_ALSA_CAPTURE_ROUTE_CARD AUDIO_ALSA_CAPTURE_ROUTE_DEVICE + export AUDIO_ALSA_CAPTURE_ROUTE_CONTROL AUDIO_ALSA_CAPTURE_ROUTE_VALUE + export AUDIO_ALSA_CAPTURE_ROUTE_PCM_LABEL + export AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME + export AUDIO_ALSA_CAPTURE_ROUTE_DISCOVERY + return 0 + fi + fi + fi + + if [ -z "$aacrr_requested_control" ] && + [ -z "$aacrr_requested_label" ] && + [ -r /proc/asound/pcm ]; then + aacrr_proc_lines="$({ + grep -Ei "$aacrr_pattern" /proc/asound/pcm | + grep -Ei 'capture' + } 2>/dev/null)" + + aacrr_proc_count="$({ + printf '%s\n' "$aacrr_proc_lines" | + sed '/^[[:space:]]*$/d' | + wc -l | + tr -d '[:space:]' + } 2>/dev/null)" + + if [ "${aacrr_proc_count:-0}" -gt 1 ] 2>/dev/null; then + printf '%s\n' \ + "multiple $aacrr_source capture PCMs were discovered in procfs, provide --pcm-label or --alsa-device" >&2 + printf '%s\n' "$aacrr_proc_lines" >&2 + return 1 + fi + + aacrr_proc_line="$(printf '%s\n' "$aacrr_proc_lines" | sed -n '1p')" + if [ -n "$aacrr_proc_line" ]; then + aacrr_key="$(printf '%s\n' "$aacrr_proc_line" | awk '{print $1}')" + aacrr_key="${aacrr_key%:}" + AUDIO_ALSA_CAPTURE_ROUTE_CARD="${aacrr_key%%-*}" + AUDIO_ALSA_CAPTURE_ROUTE_DEVICE="${aacrr_key#*-}" + AUDIO_ALSA_CAPTURE_ROUTE_PCM_LABEL="$({ + printf '%s\n' "$aacrr_proc_line" | + sed -n 's/^[^:]*:[[:space:]]*\([^:]*\).*/\1/p' | + sed 's/[[:space:]]*$//' + } 2>/dev/null)" + + if [ -n "$AUDIO_ALSA_CAPTURE_ROUTE_CARD" ] && + [ -n "$AUDIO_ALSA_CAPTURE_ROUTE_DEVICE" ]; then + AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME="plughw:$AUDIO_ALSA_CAPTURE_ROUTE_CARD,$AUDIO_ALSA_CAPTURE_ROUTE_DEVICE" + AUDIO_ALSA_CAPTURE_ROUTE_DISCOVERY="proc-pcm-name" + export AUDIO_ALSA_CAPTURE_ROUTE_CARD AUDIO_ALSA_CAPTURE_ROUTE_DEVICE + export AUDIO_ALSA_CAPTURE_ROUTE_CONTROL AUDIO_ALSA_CAPTURE_ROUTE_VALUE + export AUDIO_ALSA_CAPTURE_ROUTE_PCM_LABEL + export AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME + export AUDIO_ALSA_CAPTURE_ROUTE_DISCOVERY + return 0 + fi + fi + fi + + if ! command -v amixer >/dev/null 2>&1; then + printf '%s\n' "amixer is unavailable for capture-route discovery" >&2 + return 1 + fi + + aacrr_control_match_count=0 + for aacrr_card in $aacrr_cards; do + aacrr_control_inventory="$( + audio_alsa_route_run amixer -c "$aacrr_card" controls + )" + aacrr_control_inventory_rc=$? + if [ "$aacrr_control_inventory_rc" -ne 0 ]; then + printf '%s\n' \ + "amixer controls probe failed or timed out for card $aacrr_card, rc=$aacrr_control_inventory_rc" >&2 + return 1 + fi + + if [ -n "$aacrr_requested_control" ]; then + aacrr_controls="$({ + printf '%s\n' "$aacrr_control_inventory" | + sed -n "s/.*name='\([^']*\)'.*/\1/p" | + grep -F -x "$aacrr_requested_control" + } 2>/dev/null)" + else + aacrr_controls="$({ + printf '%s\n' "$aacrr_control_inventory" | + sed -n "s/.*name='\([^']*\)'.*/\1/p" | + grep -Ei "$aacrr_pattern" + } 2>/dev/null)" + fi + + if [ -z "$aacrr_controls" ]; then + continue + fi + + while [ -n "$aacrr_controls" ]; do + aacrr_control="$(printf '%s\n' "$aacrr_controls" | sed -n '1p')" + aacrr_controls="$(printf '%s\n' "$aacrr_controls" | sed '1d')" + + if [ -n "$aacrr_requested_label" ]; then + aacrr_label="$aacrr_requested_label" + else + aacrr_label="$({ + printf '%s\n' "$aacrr_control" | + sed -n 's/.*\(MultiMedia[0-9][0-9]*\).*/\1/p' + } 2>/dev/null)" + fi + + if [ -z "$aacrr_label" ]; then + printf '%s\n' \ + "capture mixer control has no discoverable PCM label, provide --pcm-label or --alsa-device: $aacrr_control" >&2 + continue + fi + + aacrr_device="$( + audio_alsa_capture_pcm_device_for_label \ + "$aacrr_card" "$aacrr_label" + )" + aacrr_device_rc=$? + + if [ "$aacrr_device_rc" -eq 2 ]; then + return 1 + fi + + if [ -z "$aacrr_device" ]; then + printf '%s\n' \ + "no capture PCM matches $aacrr_label on ALSA card $aacrr_card" >&2 + continue + fi + + aacrr_control_match_count=$((aacrr_control_match_count + 1)) + aacrr_control_card="$aacrr_card" + aacrr_control_device="$aacrr_device" + aacrr_control_name="$aacrr_control" + aacrr_control_label="$aacrr_label" + done + done + + if [ "$aacrr_control_match_count" -gt 1 ]; then + printf '%s\n' \ + "multiple $aacrr_source mixer-to-PCM mappings were discovered, provide an exact --mixer-control with --pcm-label or --alsa-device" >&2 + return 1 + fi + + if [ "$aacrr_control_match_count" -eq 1 ]; then + AUDIO_ALSA_CAPTURE_ROUTE_CARD="$aacrr_control_card" + AUDIO_ALSA_CAPTURE_ROUTE_DEVICE="$aacrr_control_device" + AUDIO_ALSA_CAPTURE_ROUTE_CONTROL="$aacrr_control_name" + AUDIO_ALSA_CAPTURE_ROUTE_VALUE="${aacrr_requested_value:-1}" + AUDIO_ALSA_CAPTURE_ROUTE_PCM_LABEL="$aacrr_control_label" + AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME="plughw:$aacrr_control_card,$aacrr_control_device" + AUDIO_ALSA_CAPTURE_ROUTE_DISCOVERY="mixer-control" + export AUDIO_ALSA_CAPTURE_ROUTE_CARD AUDIO_ALSA_CAPTURE_ROUTE_DEVICE + export AUDIO_ALSA_CAPTURE_ROUTE_CONTROL AUDIO_ALSA_CAPTURE_ROUTE_VALUE + export AUDIO_ALSA_CAPTURE_ROUTE_PCM_LABEL + export AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME + export AUDIO_ALSA_CAPTURE_ROUTE_DISCOVERY + return 0 + fi + + printf '%s\n' \ + "no $aacrr_source capture route was found from PCM names, mixer controls, or supplied overrides" >&2 + return 1 +} + +# audio_alsa_find_capture_route [control] [value] [label] [device] +# Print card|device|control|value|pcm-label|capture-device after resolution. +# Diagnostics go to stderr, and the function does not mutate mixer state. +audio_alsa_find_capture_route() { + if ! audio_alsa_resolve_capture_route "$@"; then + return 1 + fi + + printf '%s|%s|%s|%s|%s|%s\n' \ + "$AUDIO_ALSA_CAPTURE_ROUTE_CARD" \ + "$AUDIO_ALSA_CAPTURE_ROUTE_DEVICE" \ + "$AUDIO_ALSA_CAPTURE_ROUTE_CONTROL" \ + "$AUDIO_ALSA_CAPTURE_ROUTE_VALUE" \ + "$AUDIO_ALSA_CAPTURE_ROUTE_PCM_LABEL" \ + "$AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME" +} + +# audio_alsa_prepare_capture_route +# Snapshot and apply the already resolved AUDIO_ALSA_CAPTURE_ROUTE_* mixer +# selection. The function emits no stdout, exports restoration state, and +# returns 1 when no route is resolved or mixer snapshot/mutation fails. +audio_alsa_prepare_capture_route() { + if [ -z "${AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME:-}" ]; then + printf '%s\n' 'no capture route has been resolved' >&2 + return 1 + fi + + AUDIO_ALSA_CAPTURE_ROUTE_MIXER_CHANGED=0 + AUDIO_ALSA_CAPTURE_ROUTE_PREVIOUS_VALUE="" + + if [ -z "$AUDIO_ALSA_CAPTURE_ROUTE_CONTROL" ]; then + export AUDIO_ALSA_CAPTURE_ROUTE_PREVIOUS_VALUE + export AUDIO_ALSA_CAPTURE_ROUTE_MIXER_CHANGED + return 0 + fi + + AUDIO_ALSA_CAPTURE_ROUTE_PREVIOUS_VALUE="$({ + audio_alsa_route_run amixer -c "$AUDIO_ALSA_CAPTURE_ROUTE_CARD" \ + cget "iface=MIXER,name=$AUDIO_ALSA_CAPTURE_ROUTE_CONTROL" 2>/dev/null | + sed -n 's/^[[:space:]]*: values=//p' | + sed -n '1p' + } 2>/dev/null)" + + if [ -z "$AUDIO_ALSA_CAPTURE_ROUTE_PREVIOUS_VALUE" ]; then + printf '%s\n' \ + "could not snapshot capture mixer control: $AUDIO_ALSA_CAPTURE_ROUTE_CONTROL" >&2 + return 1 + fi + + AUDIO_ALSA_CAPTURE_ROUTE_MIXER_CHANGED=1 + export AUDIO_ALSA_CAPTURE_ROUTE_PREVIOUS_VALUE + export AUDIO_ALSA_CAPTURE_ROUTE_MIXER_CHANGED + + if ! audio_alsa_route_run amixer -c "$AUDIO_ALSA_CAPTURE_ROUTE_CARD" \ + cset "iface=MIXER,name=$AUDIO_ALSA_CAPTURE_ROUTE_CONTROL" \ + "$AUDIO_ALSA_CAPTURE_ROUTE_VALUE" >/dev/null 2>&1; then + audio_alsa_restore_capture_route >/dev/null 2>&1 || true + printf '%s\n' \ + "could not set capture mixer control $AUDIO_ALSA_CAPTURE_ROUTE_CONTROL to $AUDIO_ALSA_CAPTURE_ROUTE_VALUE" >&2 + return 1 + fi + + return 0 +} + +# audio_alsa_restore_capture_route +# Restore the mixer value saved by audio_alsa_prepare_capture_route. The +# function emits no stdout and returns 1 only when required state is missing or +# the restore operation fails. +audio_alsa_restore_capture_route() { + case "${AUDIO_ALSA_CAPTURE_ROUTE_MIXER_CHANGED:-0}" in + 1) + ;; + *) + return 0 + ;; + esac + + if [ -z "${AUDIO_ALSA_CAPTURE_ROUTE_CARD:-}" ] || + [ -z "${AUDIO_ALSA_CAPTURE_ROUTE_CONTROL:-}" ] || + [ -z "${AUDIO_ALSA_CAPTURE_ROUTE_PREVIOUS_VALUE:-}" ]; then + return 1 + fi + + audio_alsa_route_run amixer -c "$AUDIO_ALSA_CAPTURE_ROUTE_CARD" \ + cset "iface=MIXER,name=$AUDIO_ALSA_CAPTURE_ROUTE_CONTROL" \ + "$AUDIO_ALSA_CAPTURE_ROUTE_PREVIOUS_VALUE" >/dev/null 2>&1 + aarc_restore_rc=$? + if [ "$aarc_restore_rc" -eq 0 ]; then + AUDIO_ALSA_CAPTURE_ROUTE_MIXER_CHANGED=0 + export AUDIO_ALSA_CAPTURE_ROUTE_MIXER_CHANGED + fi + return "$aarc_restore_rc" +} + +# audio_alsa_capture_route_inventory +# Write ALSA card, capture PCM, mixer, and UCM inventory to the requested +# artifact. The function emits no stdout and does not mutate target state. +audio_alsa_capture_route_inventory() { + aacri_output="$1" + + if [ -z "$aacri_output" ]; then + return 1 + fi + + { + printf '%s\n' '### /proc/asound/cards' + if [ -r /proc/asound/cards ]; then + cat /proc/asound/cards + else + printf '%s\n' '/proc/asound/cards is unavailable' + fi + + printf '%s\n' '### /proc/asound/pcm' + if [ -r /proc/asound/pcm ]; then + cat /proc/asound/pcm + else + printf '%s\n' '/proc/asound/pcm is unavailable' + fi + + printf '%s\n' '### arecord -l' + if command -v arecord >/dev/null 2>&1; then + audio_alsa_route_run arecord -l || + printf '%s\n' 'arecord -l failed or timed out' + else + printf '%s\n' 'arecord is unavailable' + fi + + for aacri_card in $(audio_alsa_card_indexes); do + printf '### amixer -c %s controls\n' "$aacri_card" + if command -v amixer >/dev/null 2>&1; then + audio_alsa_route_run amixer -c "$aacri_card" controls || + printf '%s\n' "amixer controls failed or timed out for card $aacri_card" + else + printf '%s\n' 'amixer is unavailable' + fi + done + + printf '%s\n' '### alsaucm listcards' + if command -v alsaucm >/dev/null 2>&1; then + audio_alsa_route_run alsaucm listcards || + printf '%s\n' 'alsaucm listcards failed or timed out' + else + printf '%s\n' 'alsaucm is unavailable' + fi + } >"$aacri_output" 2>&1 +} + +# audio_alsa_card_indexes [playback|capture|all] +# Print unique ALSA card indexes, preferring procfs and using only the requested +# bounded ALSA inventory fallback. Diagnostics go to stderr, and the function +# returns 1 when an available fallback command fails before finding any card. +audio_alsa_card_indexes() { + aaci_mode="${1:-all}" + aaci_cards="$( sed -n 's/^[[:space:]]*\([0-9][0-9]*\)[[:space:]].*/\1/p' \ /proc/asound/cards 2>/dev/null - aplay -l 2>/dev/null | - sed -n 's/^card[[:space:]]*\([0-9][0-9]*\):.*/\1/p' - arecord -l 2>/dev/null | - sed -n 's/^card[[:space:]]*\([0-9][0-9]*\):.*/\1/p' - } | awk '!seen[$0]++' + )" + aaci_probe_failed=0 + + if [ -n "$aaci_cards" ]; then + printf '%s\n' "$aaci_cards" | awk '!seen[$0]++' + return 0 + fi + + case "$aaci_mode" in + playback|all) + if command -v aplay >/dev/null 2>&1; then + aaci_inventory="$(audio_alsa_route_run aplay -l)" + aaci_rc=$? + if [ "$aaci_rc" -eq 0 ]; then + aaci_cards="$( + printf '%s\n' "$aaci_inventory" | + sed -n 's/^card[[:space:]]*\([0-9][0-9]*\):.*/\1/p' + )" + else + printf '%s\n' \ + "aplay inventory failed or timed out while listing ALSA cards, rc=$aaci_rc" >&2 + aaci_probe_failed=1 + fi + fi + ;; + capture) + ;; + *) + printf '%s\n' "invalid ALSA card inventory mode: $aaci_mode" >&2 + return 1 + ;; + esac + + case "$aaci_mode" in + capture|all) + if command -v arecord >/dev/null 2>&1; then + aaci_inventory="$(audio_alsa_route_run arecord -l)" + aaci_rc=$? + if [ "$aaci_rc" -eq 0 ]; then + aaci_capture_cards="$( + printf '%s\n' "$aaci_inventory" | + sed -n 's/^card[[:space:]]*\([0-9][0-9]*\):.*/\1/p' + )" + if [ -n "$aaci_capture_cards" ]; then + if [ -n "$aaci_cards" ]; then + aaci_cards="$aaci_cards +$aaci_capture_cards" + else + aaci_cards="$aaci_capture_cards" + fi + fi + else + printf '%s\n' \ + "arecord inventory failed or timed out while listing ALSA cards, rc=$aaci_rc" >&2 + aaci_probe_failed=1 + fi + fi + ;; + esac + + if [ -n "$aaci_cards" ]; then + printf '%s\n' "$aaci_cards" | awk '!seen[$0]++' + return 0 + fi + + [ "$aaci_probe_failed" -eq 0 ] } -# Extract the card index from an hw:CARD,DEV or plughw:CARD,DEV identifier. +# audio_alsa_device_card +# Print the numeric card index from hw:CARD,DEV or plughw:CARD,DEV. The function +# emits an empty string for nonnumeric aliases and has no side effects. audio_alsa_device_card() { printf '%s\n' "$1" | sed -n 's/^\(plug\)\{0,1\}hw:\([0-9][0-9]*\),.*/\2/p' @@ -6833,6 +8202,8 @@ audio_validate_recorded_wav_od() { avrwo_min_active="${AUDIO_RECORD_MIN_ACTIVE_SAMPLES:-100}" avrwo_min_distinct="${AUDIO_RECORD_MIN_DISTINCT_SAMPLES:-4}" avrwo_threshold_lsb="${AUDIO_RECORD_SAMPLE_THRESHOLD_LSB:-${AUDIO_RECORD_SAMPLE_THRESHOLD:-8}}" + avrwo_strict_signal="${AUDIO_RECORD_STRICT_SIGNAL:-0}" + avrwo_min_rms_dbfs="${AUDIO_RECORD_MIN_RMS_DBFS:--60}" command -v od >/dev/null 2>&1 || return 1 command -v dd >/dev/null 2>&1 || return 1 @@ -6987,7 +8358,9 @@ EOF -v bits="$avrwo_bits" \ -v min_active="$avrwo_min_active" \ -v min_distinct="$avrwo_min_distinct" \ - -v threshold_lsb="$avrwo_threshold_lsb" ' + -v threshold_lsb="$avrwo_threshold_lsb" \ + -v strict_signal="$avrwo_strict_signal" \ + -v min_rms_dbfs="$avrwo_min_rms_dbfs" ' function abs_value(value) { return value < 0 ? -value : value } @@ -6999,11 +8372,18 @@ EOF function consume_sample(raw, sign_limit, full_range, threshold, key) { sign_limit = power2(bits - 1) full_range = power2(bits) - if (raw >= sign_limit) raw -= full_range + if (bits == 8) { + raw -= 128 + } else if (raw >= sign_limit) { + raw -= full_range + } samples++ if (raw != 0) nonzero_samples++ + normalized = raw / sign_limit + sum_squares += normalized * normalized + threshold = (threshold_lsb / 32768.0) * sign_limit if (threshold < 0) threshold = 0 if (abs_value(raw) > threshold) active_samples++ @@ -7036,22 +8416,33 @@ EOF } } END { - printf "%d|%d|%d|%d\n", samples, nonzero_samples, active_samples, distinct_samples + rms = samples > 0 ? sqrt(sum_squares / samples) : 0 + rms_dbfs = rms > 0 ? 20 * log(rms) / log(10) : -999 + printf "%d|%d|%d|%d|%.2f\n", samples, nonzero_samples, active_samples, distinct_samples, rms_dbfs if (samples <= 0 || nonzero_samples <= 0 || active_samples < min_active || distinct_samples < min_distinct) { exit 1 } + if (strict_signal == 1 && rms_dbfs < min_rms_dbfs) exit 1 } - ')" || return 1 + ')" + avrwo_stats_rc=$? IFS='|' read -r \ avrwo_samples \ avrwo_nonzero_samples \ avrwo_active_samples \ - avrwo_distinct_samples <>"$avrw_log" log_fail "$AUDIO_WAV_VALIDATION_SUMMARY" From 113a53c21e459a89336f20eb70d0740c9022d9a9 Mon Sep 17 00:00:00 2001 From: Srikanth Muppandam Date: Tue, 29 Sep 2026 10:26:22 +0530 Subject: [PATCH 2/2] audio: add external route validation suites Add fixture-oriented HDMI, DisplayPort/eDP, and headphone playback coverage plus wired headset-microphone capture coverage. Discover route topology dynamically, support explicit CI overrides, retain collision-safe evidence, and expose portable LAVA parameters without changing the existing generic playback and recording flows. Signed-off-by: Srikanth Muppandam --- .../Multimedia/Audio/AudioPlayback/Read_me.md | 4 + .../Multimedia/Audio/AudioRecord/Read_me.md | 5 + .../AudioRoutePlayback_DisplayPort.yaml | 23 + .../AudioRoutePlayback_HDMI.yaml | 23 + .../AudioRoutePlayback_Headphones.yaml | 23 + .../Audio/AudioRoutePlayback/README.md | 176 +++++++ .../Audio/AudioRoutePlayback/run.sh | 393 ++++++++++++++++ .../AudioRouteRecord_HeadsetMic.yaml | 25 + .../Audio/AudioRouteRecord/README.md | 183 ++++++++ .../Multimedia/Audio/AudioRouteRecord/run.sh | 442 ++++++++++++++++++ .../README_Audio_Card_Registration.md | 5 +- 11 files changed, 1301 insertions(+), 1 deletion(-) create mode 100644 Runner/suites/Multimedia/Audio/AudioRoutePlayback/AudioRoutePlayback_DisplayPort.yaml create mode 100644 Runner/suites/Multimedia/Audio/AudioRoutePlayback/AudioRoutePlayback_HDMI.yaml create mode 100644 Runner/suites/Multimedia/Audio/AudioRoutePlayback/AudioRoutePlayback_Headphones.yaml create mode 100644 Runner/suites/Multimedia/Audio/AudioRoutePlayback/README.md create mode 100755 Runner/suites/Multimedia/Audio/AudioRoutePlayback/run.sh create mode 100644 Runner/suites/Multimedia/Audio/AudioRouteRecord/AudioRouteRecord_HeadsetMic.yaml create mode 100644 Runner/suites/Multimedia/Audio/AudioRouteRecord/README.md create mode 100755 Runner/suites/Multimedia/Audio/AudioRouteRecord/run.sh diff --git a/Runner/suites/Multimedia/Audio/AudioPlayback/Read_me.md b/Runner/suites/Multimedia/Audio/AudioPlayback/Read_me.md index 0878576f..9eccdc7b 100644 --- a/Runner/suites/Multimedia/Audio/AudioPlayback/Read_me.md +++ b/Runner/suites/Multimedia/Audio/AudioPlayback/Read_me.md @@ -103,6 +103,10 @@ AUDIO_PACKAGE_PROFILE=desktop AUDIO_PACKAGE_UPDATE=1 \ When no backend is requested, the suite uses automatic selection. A physical PipeWire audio sink uses `pw-play`, 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. +This suite does not prove a specific external connector. Use the focused +`AudioRoutePlayback` suite when a fixture-aware job must require HDMI, +DisplayPort/eDP, or 3.5 mm headphone routing. + 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`. An explicit backend request is never replaced: diff --git a/Runner/suites/Multimedia/Audio/AudioRecord/Read_me.md b/Runner/suites/Multimedia/Audio/AudioRecord/Read_me.md index 59298505..ab6d359f 100644 --- a/Runner/suites/Multimedia/Audio/AudioRecord/Read_me.md +++ b/Runner/suites/Multimedia/Audio/AudioRecord/Read_me.md @@ -67,6 +67,11 @@ When no backend is requested, the suite uses automatic selection. A real PipeWir 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`. +Use the separate `AudioRouteRecord` suite when a fixture-aware job must require +capture from the wired 3.5 mm headset microphone. It dynamically discovers the +route or accepts exact mixer, PCM, and ALSA-device overrides, and it never falls +back to an internal microphone. + An explicit backend request is never replaced: - `--backend pipewire` or `AUDIO_BACKEND=pipewire` runs `pw-record` only and skips if PipeWire has no physical microphone source. diff --git a/Runner/suites/Multimedia/Audio/AudioRoutePlayback/AudioRoutePlayback_DisplayPort.yaml b/Runner/suites/Multimedia/Audio/AudioRoutePlayback/AudioRoutePlayback_DisplayPort.yaml new file mode 100644 index 00000000..eeff1b63 --- /dev/null +++ b/Runner/suites/Multimedia/Audio/AudioRoutePlayback/AudioRoutePlayback_DisplayPort.yaml @@ -0,0 +1,23 @@ +metadata: + name: audio-route-playback-displayport + format: "Lava-Test Test Definition 1.0" + description: "Validate playback through a dynamically discovered DP or eDP audio route" + os: + - linux + scope: + - functional + +params: + MIXER_CONTROL: "" + MIXER_VALUE: "" + PCM_LABEL: "" + ALSA_DEVICE: "" + PLAYBACK_DURATION: "3" + DMESG_SCAN: "1" + +run: + steps: + - REPO_PATH=$PWD + - cd Runner/suites/Multimedia/Audio/AudioRoutePlayback/ + - ./run.sh --route displayport --mixer-control "${MIXER_CONTROL}" --mixer-value "${MIXER_VALUE}" --pcm-label "${PCM_LABEL}" --alsa-device "${ALSA_DEVICE}" --duration "${PLAYBACK_DURATION}" --dmesg-scan "${DMESG_SCAN}" --res-suffix DisplayPort --lava-testcase-id AudioRoutePlayback_DisplayPort || true + - $REPO_PATH/Runner/utils/send-to-lava.sh AudioRoutePlayback_DisplayPort.res diff --git a/Runner/suites/Multimedia/Audio/AudioRoutePlayback/AudioRoutePlayback_HDMI.yaml b/Runner/suites/Multimedia/Audio/AudioRoutePlayback/AudioRoutePlayback_HDMI.yaml new file mode 100644 index 00000000..da7e86b4 --- /dev/null +++ b/Runner/suites/Multimedia/Audio/AudioRoutePlayback/AudioRoutePlayback_HDMI.yaml @@ -0,0 +1,23 @@ +metadata: + name: audio-route-playback-hdmi + format: "Lava-Test Test Definition 1.0" + description: "Validate playback through a dynamically discovered HDMI audio route" + os: + - linux + scope: + - functional + +params: + MIXER_CONTROL: "" + MIXER_VALUE: "" + PCM_LABEL: "" + ALSA_DEVICE: "" + PLAYBACK_DURATION: "3" + DMESG_SCAN: "1" + +run: + steps: + - REPO_PATH=$PWD + - cd Runner/suites/Multimedia/Audio/AudioRoutePlayback/ + - ./run.sh --route hdmi --mixer-control "${MIXER_CONTROL}" --mixer-value "${MIXER_VALUE}" --pcm-label "${PCM_LABEL}" --alsa-device "${ALSA_DEVICE}" --duration "${PLAYBACK_DURATION}" --dmesg-scan "${DMESG_SCAN}" --res-suffix HDMI --lava-testcase-id AudioRoutePlayback_HDMI || true + - $REPO_PATH/Runner/utils/send-to-lava.sh AudioRoutePlayback_HDMI.res diff --git a/Runner/suites/Multimedia/Audio/AudioRoutePlayback/AudioRoutePlayback_Headphones.yaml b/Runner/suites/Multimedia/Audio/AudioRoutePlayback/AudioRoutePlayback_Headphones.yaml new file mode 100644 index 00000000..09e9b021 --- /dev/null +++ b/Runner/suites/Multimedia/Audio/AudioRoutePlayback/AudioRoutePlayback_Headphones.yaml @@ -0,0 +1,23 @@ +metadata: + name: audio-route-playback-headphones + format: "Lava-Test Test Definition 1.0" + description: "Validate playback through a dynamically discovered 3.5 mm headphone route" + os: + - linux + scope: + - functional + +params: + MIXER_CONTROL: "" + MIXER_VALUE: "" + PCM_LABEL: "" + ALSA_DEVICE: "" + PLAYBACK_DURATION: "3" + DMESG_SCAN: "1" + +run: + steps: + - REPO_PATH=$PWD + - cd Runner/suites/Multimedia/Audio/AudioRoutePlayback/ + - ./run.sh --route headphones --mixer-control "${MIXER_CONTROL}" --mixer-value "${MIXER_VALUE}" --pcm-label "${PCM_LABEL}" --alsa-device "${ALSA_DEVICE}" --duration "${PLAYBACK_DURATION}" --dmesg-scan "${DMESG_SCAN}" --res-suffix Headphones --lava-testcase-id AudioRoutePlayback_Headphones || true + - $REPO_PATH/Runner/utils/send-to-lava.sh AudioRoutePlayback_Headphones.res diff --git a/Runner/suites/Multimedia/Audio/AudioRoutePlayback/README.md b/Runner/suites/Multimedia/Audio/AudioRoutePlayback/README.md new file mode 100644 index 00000000..118d854c --- /dev/null +++ b/Runner/suites/Multimedia/Audio/AudioRoutePlayback/README.md @@ -0,0 +1,176 @@ +# AudioRoutePlayback + +## Purpose + +`AudioRoutePlayback` validates one explicitly requested external audio route on +any SoC. It first searches runtime ALSA PCM and mixer inventories for generic +route names. When a platform uses opaque topology names, the caller can supply +the exact mixer control, mixer value, PCM label, or ALSA device. The suite then +generates a bounded 1 kHz signal and performs stereo playback with `aplay`. + +This fills a gap in the generic `AudioPlayback` suite. The generic suite validates playback through a usable speaker/default sink, but it does not require or prove HDMI, DisplayPort/eDP, or 3.5 mm headphone routing. + +## Supported Routes + +| Route | CLI value | Generic runtime matches | +|---|---|---| +| HDMI | `hdmi` | HDMI-named PCM or mixer entries | +| DisplayPort or eDP | `displayport` | DisplayPort, DP, or eDP-named PCM or mixer entries | +| 3.5 mm headphones/headset | `headphones` | Headphone, headset, HSJ, or HP-output entries | + +Card and PCM device numbers are never hardcoded. The test derives them from +`amixer`, `/proc/asound/pcm`, and `aplay -l`, unless the caller explicitly +provides an ALSA endpoint. + +Automatic selection is accepted only when the route resolves to one candidate. +If several HDMI, DP/eDP, or headset mappings match, the test fails before +changing mixer state and asks the fixture job for an exact PCM label or ALSA +device. This avoids silently choosing the first enumerated connector. + +## Prerequisites and Fixtures + +- The image must provide `aplay` and `amixer`, normally from `alsa-utils`. +- The requested route must be provisioned by the board device tree, ASoC topology, firmware, and image configuration. +- HDMI and DisplayPort/eDP jobs require the corresponding connected display or audio-capable fixture. +- The headphone job requires a board variant or mezzanine that physically exposes the 3.5 mm codec route, plus a connected headset or audio fixture. + +These route definitions are opt-in and are not added to the generic config1 nightly plan. Add them to a board-specific job only when its external fixture is guaranteed. Once selected, a missing or nonfunctional route is reported as `FAIL` rather than silently falling back to another output. + +The same YAML definitions can be used across SoCs. Board-specific jobs may set +`MIXER_CONTROL`, `MIXER_VALUE`, `PCM_LABEL`, or `ALSA_DEVICE` when their runtime +names do not identify the physical connector. Empty YAML override parameters +leave any corresponding `AUDIO_*` environment value in effect. + +## Usage + +```sh +cd Runner/suites/Multimedia/Audio/AudioRoutePlayback +./run.sh --route hdmi +./run.sh --route displayport +./run.sh --route headphones +``` + +The aliases `dp`, `edp`, `headphone`, `headset`, and `3.5mm` are also accepted. + +Select an opaque ASoC route by exact mixer control. When the control contains a +`MultiMediaN` label, the PCM is derived automatically: + +```sh +./run.sh \ + --route hdmi \ + --mixer-control "Vendor HDMI Audio Mixer MultiMedia3" +``` + +Switch controls default to value `1`. Supply an exact value for an enum or +other non-boolean control: + +```sh +./run.sh \ + --route hdmi \ + --mixer-control "Display Audio Route" \ + --mixer-value "HDMI" \ + --pcm-label "MultiMedia3" +``` + +Supply a separate PCM label when it cannot be derived from the control: + +```sh +./run.sh \ + --route displayport \ + --mixer-control "Display Audio Route Switch" \ + --pcm-label "Display Playback" +``` + +Or provide the endpoint directly. Card and device numbers shown here are only +examples and must come from the target's `aplay -l` output: + +```sh +./run.sh --route headphones --alsa-device plughw:0,2 +``` + +Use a longer playback interval: + +```sh +./run.sh --route displayport --duration 10 +``` + +Create a unique result for a LAVA job: + +```sh +./run.sh \ + --route hdmi \ + --res-suffix HDMI \ + --lava-testcase-id AudioRoutePlayback_HDMI +``` + +Disable kernel log capture: + +```sh +./run.sh --route headphones --no-dmesg +``` + +Environment equivalents are available for all public parameters: + +```sh +AUDIO_ROUTE=displayport \ +AUDIO_MIXER_CONTROL="" \ +AUDIO_MIXER_VALUE="" \ +AUDIO_PCM_LABEL="" \ +AUDIO_ALSA_DEVICE="" \ +PLAYBACK_DURATION=5 \ +DMESG_SCAN=1 \ +RES_SUFFIX=DisplayPort \ +LAVA_TESTCASE_ID=AudioRoutePlayback_DisplayPort \ +./run.sh +``` + +## Results + +`PASS` requires both: + +- A route-specific PCM discovered from runtime names or selected by an explicit + user override. +- A successful bounded `aplay` operation through the derived `plughw:CARD,DEVICE` endpoint. + +The playback payload is a generated 1 kHz unsigned 8-bit stereo signal at +48 kHz. This gives the CI fixture a deterministic non-silent signal to detect. +The runner proves that ALSA accepted and completed the transfer. Physical +connector output remains the responsibility of the fixture-side measurement. + +`SKIP` is used when required image-provided ALSA utilities are absent. + +`FAIL` is used for malformed input, missing explicitly requested route topology, mixer preparation failure, playback failure, or failure to restore the mixer value changed by the test. + +## State Restoration + +When a mixer control is used, the test snapshots its current value before +enabling it. The original value is restored on normal completion and from the +exit trap. A route discovered directly from a named PCM does not require mixer +mutation. The test does not install packages, restart audio services, or alter +unrelated mixer controls. + +## Artifacts + +Artifacts are retained in a collision-safe +`results/AudioRoutePlayback[_SUFFIX]/run-TIMESTAMP-PID/` directory. The exact +path is printed when the suite starts. + +- `audio_route_inventory.log`: ALSA cards, PCMs, mixer controls, and UCM card inventory +- `route_mapping.txt`: selected card, device, mixer control, and PCM label +- `route_selection.tsv`: machine-readable selected-route details +- `route_discovery.log`: route discovery and preparation diagnostics +- `playback_tone_1khz_u8_stereo.raw`: generated fixture-detectable signal +- `playback_signal_generation.log`: signal-generation diagnostics +- `aplay_ROUTE.log`: playback command output +- `dmesg_snapshot.log` and `dmesg_errors.log`: shared kernel-log capture output when enabled + +## LAVA Definitions + +- `AudioRoutePlayback_HDMI.yaml` +- `AudioRoutePlayback_DisplayPort.yaml` +- `AudioRoutePlayback_Headphones.yaml` + +Each definition uses a unique result filename and testcase ID, allowing all three to be included in one fixture-aware plan without result collisions. + +Playback duration must be between 1 and 60 seconds. `--mixer-value` and +`AUDIO_MIXER_VALUE` require an accompanying mixer control. diff --git a/Runner/suites/Multimedia/Audio/AudioRoutePlayback/run.sh b/Runner/suites/Multimedia/Audio/AudioRoutePlayback/run.sh new file mode 100755 index 00000000..75ab9ccb --- /dev/null +++ b/Runner/suites/Multimedia/Audio/AudioRoutePlayback/run.sh @@ -0,0 +1,393 @@ +#!/bin/sh +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +# ---------- Repo env + helpers ---------- +SCRIPT_DIR="$( + cd "$(dirname "$0")" || exit 1 + pwd +)" +INIT_ENV="" +SEARCH="$SCRIPT_DIR" + +while [ "$SEARCH" != "/" ]; do + if [ -f "$SEARCH/init_env" ]; then + INIT_ENV="$SEARCH/init_env" + break + fi + SEARCH=$(dirname "$SEARCH") +done + +if [ -z "$INIT_ENV" ]; then + echo "[ERROR] Could not find init_env (starting at $SCRIPT_DIR)" >&2 + exit 1 +fi + +# Only source once (idempotent) +# NOTE: We intentionally **do not export** any new vars. They stay local to this shell. +if [ -z "${__INIT_ENV_LOADED:-}" ]; then + # shellcheck disable=SC1090 + . "$INIT_ENV" + __INIT_ENV_LOADED=1 +fi + +# shellcheck disable=SC1090 +. "$INIT_ENV" +# shellcheck disable=SC1091 +. "$TOOLS/functestlib.sh" +# shellcheck disable=SC1091 +. "$TOOLS/audio_common.sh" + +TESTNAME="AudioRoutePlayback" +RES_FILE="$SCRIPT_DIR/${TESTNAME}.res" + +ROUTE="${AUDIO_ROUTE:-}" +DYNAMIC_MIXER_CONTROL="${AUDIO_MIXER_CONTROL:-}" +DYNAMIC_MIXER_VALUE="${AUDIO_MIXER_VALUE:-}" +DYNAMIC_PCM_LABEL="${AUDIO_PCM_LABEL:-}" +DYNAMIC_ALSA_DEVICE="${AUDIO_ALSA_DEVICE:-}" +DURATION="${PLAYBACK_DURATION:-3}" +DMESG_SCAN="${DMESG_SCAN:-1}" +RES_SUFFIX="${RES_SUFFIX:-}" +RESULT_TESTNAME="${LAVA_TESTCASE_ID:-$TESTNAME}" +ARG_ERROR="" +LOGDIR="" +RESULT_SCOPE="" + +# Print the supported route-playback CLI and its environment equivalents. +usage() { + cat </dev/null 2>&1; then + test_result_record "SKIP" \ + "$TESTNAME SKIP - required ALSA playback utility is missing: aplay, provision alsa-utils in the image" + test_result_finish +fi + +route_discovery_log="$LOGDIR/route_discovery.log" +if ! audio_alsa_find_playback_route \ + "$ROUTE" \ + "$DYNAMIC_MIXER_CONTROL" \ + "$DYNAMIC_MIXER_VALUE" \ + "$DYNAMIC_PCM_LABEL" \ + "$DYNAMIC_ALSA_DEVICE" \ + >"$LOGDIR/route_mapping.txt" 2>"$route_discovery_log"; then + if ! command -v amixer >/dev/null 2>&1; then + test_result_record "SKIP" \ + "$TESTNAME SKIP - the requested $ROUTE route was not visible in the PCM inventory and amixer is unavailable for mixer discovery, provision alsa-utils or supply --alsa-device" + test_result_finish + fi + + route_reason="$(tail -n 1 "$route_discovery_log" 2>/dev/null)" + [ -n "$route_reason" ] || route_reason="no matching runtime route evidence" + test_result_record "FAIL" \ + "$TESTNAME FAIL - requested $ROUTE route was not discovered, reason='$route_reason', verify the runtime PCM and mixer inventory or supply --mixer-control, --mixer-value, --pcm-label, or --alsa-device, artifact=$route_discovery_log" + test_result_finish +fi + +test_result_record "PASS" \ + "Discovered the requested $ROUTE route, device=$AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE source=$AUDIO_ALSA_ROUTE_DISCOVERY" + +if ! audio_alsa_prepare_playback_route 2>>"$route_discovery_log"; then + route_reason="$(tail -n 1 "$route_discovery_log" 2>/dev/null)" + [ -n "$route_reason" ] || route_reason="mixer preparation failed without details" + test_result_record "FAIL" \ + "$TESTNAME FAIL - requested $ROUTE route was discovered but could not be enabled, reason='$route_reason', verify mixer permissions, control value, and route topology, artifact=$route_discovery_log" + test_result_finish +fi + +{ + printf 'route\t%s\n' "$ROUTE" + printf 'card\t%s\n' "$AUDIO_ALSA_ROUTE_CARD" + printf 'device\t%s\n' "$AUDIO_ALSA_ROUTE_DEVICE" + printf 'playback_device\t%s\n' "$AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE" + printf 'pcm_label\t%s\n' "$AUDIO_ALSA_ROUTE_PCM_LABEL" + printf 'mixer_control\t%s\n' "$AUDIO_ALSA_ROUTE_CONTROL" + printf 'mixer_value\t%s\n' "$AUDIO_ALSA_ROUTE_VALUE" + printf 'previous_mixer_value\t%s\n' "$AUDIO_ALSA_ROUTE_PREVIOUS_VALUE" + printf 'discovery\t%s\n' "$AUDIO_ALSA_ROUTE_DISCOVERY" +} >"$LOGDIR/route_selection.tsv" + +playback_log="$LOGDIR/aplay_${ROUTE}.log" +playback_signal="$LOGDIR/playback_tone_1khz_u8_stereo.raw" +playback_timeout=$((DURATION + 10)) +if ! audio_generate_u8_stereo_tone \ + "$playback_signal" "$DURATION" 48000 1000 \ + 2>"$LOGDIR/playback_signal_generation.log"; then + test_result_record "FAIL" \ + "$TESTNAME FAIL - could not generate the bounded 1 kHz playback signal, artifact=$LOGDIR/playback_signal_generation.log" + test_result_finish +fi + +log_info "Playing a 1 kHz stereo signal through $AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE, pcm=$AUDIO_ALSA_ROUTE_PCM_LABEL control='$AUDIO_ALSA_ROUTE_CONTROL'" + +audio_exec_with_timeout "${playback_timeout}s" \ + env LC_ALL=C \ + aplay \ + -D "$AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE" \ + -t raw \ + -f U8 \ + -r 48000 \ + -c 2 \ + -d "$DURATION" \ + "$playback_signal" >"$playback_log" 2>&1 +playback_rc=$? + +if [ "$playback_rc" -eq 0 ] && + grep -q 'Playing raw data' "$playback_log"; then + test_result_record "PASS" \ + "$ROUTE playback completed with a 1 kHz signal through $AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE" +else + test_result_record "FAIL" \ + "$TESTNAME FAIL - $ROUTE playback did not complete on $AUDIO_ALSA_ROUTE_PLAYBACK_DEVICE, rc=$playback_rc, verify the connected display or 3.5 mm fixture and inspect $playback_log" +fi + +if [ "$DMESG_SCAN" -eq 1 ]; then + scan_dmesg_errors \ + "$LOGDIR" \ + 'snd|asoc|audio|lpass|q6|display|hdmi|codec' \ + 'dummy regulator|supply [^ ]+ not found|using dummy regulator|probe deferred' || true + + if [ -s "$LOGDIR/dmesg_errors.log" ]; then + log_warn "Audio-related kernel errors were captured in $LOGDIR/dmesg_errors.log" + fi +fi + +if ! audio_alsa_restore_playback_route; then + test_result_record "FAIL" \ + "$TESTNAME FAIL - playback finished but the previous mixer state could not be restored, control='$AUDIO_ALSA_ROUTE_CONTROL'" +fi + +test_result_finish diff --git a/Runner/suites/Multimedia/Audio/AudioRouteRecord/AudioRouteRecord_HeadsetMic.yaml b/Runner/suites/Multimedia/Audio/AudioRouteRecord/AudioRouteRecord_HeadsetMic.yaml new file mode 100644 index 00000000..8203dcf4 --- /dev/null +++ b/Runner/suites/Multimedia/Audio/AudioRouteRecord/AudioRouteRecord_HeadsetMic.yaml @@ -0,0 +1,25 @@ +metadata: + name: audio-route-record-headset-microphone + format: "Lava-Test Test Definition 1.0" + description: "Validate capture through a dynamically discovered 3.5 mm headset microphone route" + os: + - linux + scope: + - functional + +params: + MIXER_CONTROL: "" + MIXER_VALUE: "" + PCM_LABEL: "" + ALSA_DEVICE: "" + CAPTURE_DURATION: "3" + STRICT_SIGNAL: "1" + MIN_RMS_DBFS: "-60" + DMESG_SCAN: "1" + +run: + steps: + - REPO_PATH=$PWD + - cd Runner/suites/Multimedia/Audio/AudioRouteRecord/ + - ./run.sh --source headset-mic --mixer-control "${MIXER_CONTROL}" --mixer-value "${MIXER_VALUE}" --pcm-label "${PCM_LABEL}" --alsa-device "${ALSA_DEVICE}" --duration "${CAPTURE_DURATION}" --strict-signal "${STRICT_SIGNAL}" --min-rms-dbfs "${MIN_RMS_DBFS}" --dmesg-scan "${DMESG_SCAN}" --res-suffix HeadsetMic --lava-testcase-id AudioRouteRecord_HeadsetMic || true + - $REPO_PATH/Runner/utils/send-to-lava.sh AudioRouteRecord_HeadsetMic.res diff --git a/Runner/suites/Multimedia/Audio/AudioRouteRecord/README.md b/Runner/suites/Multimedia/Audio/AudioRouteRecord/README.md new file mode 100644 index 00000000..ab496eae --- /dev/null +++ b/Runner/suites/Multimedia/Audio/AudioRouteRecord/README.md @@ -0,0 +1,183 @@ +# AudioRouteRecord + +## Purpose + +`AudioRouteRecord` validates capture from an explicitly requested wired headset +microphone on any SoC. It searches runtime ALSA capture PCM and mixer inventories +for generic headset-microphone names. When a platform uses opaque topology +names, the caller can supply the exact mixer control, mixer value, PCM label, or +ALSA device. + +This complements the generic `AudioRecord` suite. `AudioRecord` proves that a +usable microphone capture path works, while this suite requires the selected +3.5 mm headset-microphone path and does not fall back to an internal DMIC or +another capture source. HDMI, DisplayPort, and eDP are playback-only routes and +are therefore not accepted as recording sources. + +## Runtime Discovery + +The suite accepts `headset-mic`, `headset-microphone`, `headset`, `3.5mm-mic`, +and `3.5mm` as source names. It discovers the card and capture PCM from: + +- `arecord -l` +- `/proc/asound/pcm` +- `amixer -c CARD controls` + +Card and PCM device numbers are never hardcoded. A named capture PCM can be +used without changing mixer state. If the route is represented by a mixer +control, the test snapshots the current value, applies the requested value, and +restores the original value after capture. + +Automatic selection is accepted only when the headset microphone resolves to +one candidate. Multiple matching capture PCMs or mixer-to-PCM mappings fail +before state changes and require an exact PCM label or ALSA device override. + +## Prerequisites and Fixture + +- The image must provide `arecord`, normally from `alsa-utils`. +- `amixer` is needed when the capture PCM name alone does not identify the + headset-microphone route. +- The board must expose a wired headset-microphone input through its device + tree, codec, ASoC topology, firmware, and image configuration. +- CI must connect the headset microphone and inject a detectable audio signal + for the recording interval. + +The LAVA definition is opt-in. Add it only to jobs where the hardware fixture +is guaranteed. Once selected, a missing or nonfunctional headset-microphone +route is reported as `FAIL` rather than falling back to a different microphone. + +## Usage + +Run automatic discovery: + +```sh +cd Runner/suites/Multimedia/Audio/AudioRouteRecord +./run.sh --source headset-mic +``` + +Select an opaque topology by exact mixer control and value. The default mixer +value is `1`, which is suitable for switch controls. Enum controls normally +require an explicit value: + +```sh +./run.sh \ + --source headset-mic \ + --mixer-control "Capture Source" \ + --mixer-value "Headset Mic" \ + --pcm-label "MultiMedia1" +``` + +Provide the capture endpoint directly. The card and device numbers below are +examples and must come from the target's `arecord -l` output: + +```sh +./run.sh --source headset-mic --alsa-device plughw:0,2 +``` + +An explicit device can be combined with an exact mixer selection: + +```sh +./run.sh \ + --source headset-mic \ + --alsa-device plughw:0,2 \ + --mixer-control "Capture Source" \ + --mixer-value "Headset Mic" +``` + +Use a longer recording interval: + +```sh +./run.sh --source headset-mic --duration 10 +``` + +Signal checking is strict by default. The default requires an RMS level of at +least `-60` dBFS in addition to the shared WAV activity checks. A fixture may +tighten this threshold when its injected level is known: + +```sh +./run.sh \ + --source headset-mic \ + --strict-signal 1 \ + --min-rms-dbfs -45 +``` + +The same RMS gate is enforced by the image-provided Python validator and by +the bounded `od`/`dd` fallback used on smaller images without Python. + +Create a unique result for a LAVA job: + +```sh +./run.sh \ + --source headset-mic \ + --res-suffix HeadsetMic \ + --lava-testcase-id AudioRouteRecord_HeadsetMic +``` + +Disable kernel log capture: + +```sh +./run.sh --source headset-mic --no-dmesg +``` + +Environment equivalents are available for every public parameter: + +```sh +AUDIO_CAPTURE_SOURCE=headset-mic \ +AUDIO_CAPTURE_MIXER_CONTROL="" \ +AUDIO_CAPTURE_MIXER_VALUE="" \ +AUDIO_CAPTURE_PCM_LABEL="" \ +AUDIO_CAPTURE_ALSA_DEVICE="" \ +AUDIO_CAPTURE_DURATION=5 \ +AUDIO_CAPTURE_STRICT_SIGNAL=1 \ +AUDIO_CAPTURE_MIN_RMS_DBFS=-60 \ +DMESG_SCAN=1 \ +RES_SUFFIX=HeadsetMic \ +LAVA_TESTCASE_ID=AudioRouteRecord_HeadsetMic \ +./run.sh +``` + +## Result Policy + +`PASS` requires all of the following: + +- A route-specific capture PCM discovered from runtime names or selected by an + explicit user override. +- A bounded `arecord` operation through the selected endpoint. +- A structurally valid WAV with the expected duration, format, sample rate, + channel count, and real signal activity. +- Successful restoration of any mixer control changed by the test. + +`SKIP` is used when required image-provided ALSA utilities are absent. + +`FAIL` is used for malformed input, a missing explicitly requested headset +microphone route, mixer preparation failure, capture or signal-validation +failure, or failure to restore mixer state. + +## Artifacts + +Artifacts are retained in a collision-safe +`results/AudioRouteRecord[_SUFFIX]/run-TIMESTAMP-PID/` directory. The exact +path is printed when the suite starts. + +- `audio_capture_route_inventory.log`: ALSA cards, capture PCMs, mixer controls, + and UCM card inventory +- `route_mapping.txt`: selected card, device, mixer control, mixer value, and + PCM label +- `route_selection.tsv`: machine-readable selected-route details +- `route_discovery.log`: route discovery and preparation diagnostics +- `headset_microphone.wav`: recorded WAV payload +- `arecord_headset_microphone.log`: recorder and WAV-validation output +- `dmesg_snapshot.log` and `dmesg_errors.log`: shared kernel-log capture output + when enabled + +## LAVA Definition + +`AudioRouteRecord_HeadsetMic.yaml` exposes mixer, PCM, device, duration, signal +threshold, and dmesg parameters without tying the test to a particular SoC or +card number. + +`--mixer-value` and `AUDIO_CAPTURE_MIXER_VALUE` require an accompanying mixer +control so a supplied value cannot be applied to an unintended auto-discovered +control. Recording duration must be between 1 and 60 seconds. Empty YAML route +override parameters leave any corresponding `AUDIO_CAPTURE_*` environment +value in effect. diff --git a/Runner/suites/Multimedia/Audio/AudioRouteRecord/run.sh b/Runner/suites/Multimedia/Audio/AudioRouteRecord/run.sh new file mode 100755 index 00000000..81e69c74 --- /dev/null +++ b/Runner/suites/Multimedia/Audio/AudioRouteRecord/run.sh @@ -0,0 +1,442 @@ +#!/bin/sh +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +# ---------- Repo env + helpers ---------- +SCRIPT_DIR="$( + cd "$(dirname "$0")" || exit 1 + pwd +)" +INIT_ENV="" +SEARCH="$SCRIPT_DIR" + +while [ "$SEARCH" != "/" ]; do + if [ -f "$SEARCH/init_env" ]; then + INIT_ENV="$SEARCH/init_env" + break + fi + SEARCH=$(dirname "$SEARCH") +done + +if [ -z "$INIT_ENV" ]; then + echo "[ERROR] Could not find init_env (starting at $SCRIPT_DIR)" >&2 + exit 1 +fi + +# Only source once (idempotent) +# NOTE: We intentionally **do not export** any new vars. They stay local to this shell. +if [ -z "${__INIT_ENV_LOADED:-}" ]; then + # shellcheck disable=SC1090 + . "$INIT_ENV" + __INIT_ENV_LOADED=1 +fi + +# shellcheck disable=SC1090 +. "$INIT_ENV" +# shellcheck disable=SC1091 +. "$TOOLS/functestlib.sh" +# shellcheck disable=SC1091 +. "$TOOLS/audio_common.sh" + +TESTNAME="AudioRouteRecord" +RES_FILE="$SCRIPT_DIR/${TESTNAME}.res" + +SOURCE="${AUDIO_CAPTURE_SOURCE:-headset-mic}" +DYNAMIC_MIXER_CONTROL="${AUDIO_CAPTURE_MIXER_CONTROL:-}" +DYNAMIC_MIXER_VALUE="${AUDIO_CAPTURE_MIXER_VALUE:-}" +DYNAMIC_PCM_LABEL="${AUDIO_CAPTURE_PCM_LABEL:-}" +DYNAMIC_ALSA_DEVICE="${AUDIO_CAPTURE_ALSA_DEVICE:-}" +DURATION="${AUDIO_CAPTURE_DURATION:-3}" +STRICT_SIGNAL="${AUDIO_CAPTURE_STRICT_SIGNAL:-1}" +MIN_RMS_DBFS="${AUDIO_CAPTURE_MIN_RMS_DBFS:--60}" +DMESG_SCAN="${DMESG_SCAN:-1}" +RES_SUFFIX="${RES_SUFFIX:-}" +RESULT_TESTNAME="${LAVA_TESTCASE_ID:-$TESTNAME}" +ARG_ERROR="" +LOGDIR="" +RESULT_SCOPE="" + +# Print the supported route-recording CLI and its environment equivalents. +usage() { + cat </dev/null 2>&1; then + test_result_record "SKIP" \ + "$TESTNAME SKIP - required ALSA capture utility is missing: arecord, provision alsa-utils in the image" + test_result_finish +fi + +route_discovery_log="$LOGDIR/route_discovery.log" +if ! audio_alsa_find_capture_route \ + "$SOURCE" \ + "$DYNAMIC_MIXER_CONTROL" \ + "$DYNAMIC_MIXER_VALUE" \ + "$DYNAMIC_PCM_LABEL" \ + "$DYNAMIC_ALSA_DEVICE" \ + >"$LOGDIR/route_mapping.txt" 2>"$route_discovery_log"; then + if ! command -v amixer >/dev/null 2>&1; then + test_result_record "SKIP" \ + "$TESTNAME SKIP - the requested headset microphone was not visible in the capture PCM inventory and amixer is unavailable for mixer discovery, provision alsa-utils or supply --alsa-device" + test_result_finish + fi + + route_reason="$(tail -n 1 "$route_discovery_log" 2>/dev/null)" + [ -n "$route_reason" ] || route_reason="no matching runtime route evidence" + test_result_record "FAIL" \ + "$TESTNAME FAIL - requested headset microphone route was not discovered, reason='$route_reason', verify the capture PCM and mixer inventory or supply --mixer-control, --mixer-value, --pcm-label, or --alsa-device, artifact=$route_discovery_log" + test_result_finish +fi + +test_result_record "PASS" \ + "Discovered the requested headset microphone route, device=$AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME source=$AUDIO_ALSA_CAPTURE_ROUTE_DISCOVERY" + +if ! audio_alsa_prepare_capture_route 2>>"$route_discovery_log"; then + route_reason="$(tail -n 1 "$route_discovery_log" 2>/dev/null)" + [ -n "$route_reason" ] || route_reason="mixer preparation failed without details" + test_result_record "FAIL" \ + "$TESTNAME FAIL - headset microphone route was discovered but could not be enabled, reason='$route_reason', verify mixer permissions, control value, and route topology, artifact=$route_discovery_log" + test_result_finish +fi + +{ + printf 'source\t%s\n' "$SOURCE" + printf 'card\t%s\n' "$AUDIO_ALSA_CAPTURE_ROUTE_CARD" + printf 'device\t%s\n' "$AUDIO_ALSA_CAPTURE_ROUTE_DEVICE" + printf 'capture_device\t%s\n' "$AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME" + printf 'pcm_label\t%s\n' "$AUDIO_ALSA_CAPTURE_ROUTE_PCM_LABEL" + printf 'mixer_control\t%s\n' "$AUDIO_ALSA_CAPTURE_ROUTE_CONTROL" + printf 'mixer_value\t%s\n' "$AUDIO_ALSA_CAPTURE_ROUTE_VALUE" + printf 'previous_mixer_value\t%s\n' "$AUDIO_ALSA_CAPTURE_ROUTE_PREVIOUS_VALUE" + printf 'discovery\t%s\n' "$AUDIO_ALSA_CAPTURE_ROUTE_DISCOVERY" +} >"$LOGDIR/route_selection.tsv" + +capture_output="$LOGDIR/headset_microphone.wav" +capture_log="$LOGDIR/arecord_headset_microphone.log" +capture_timeout=$((DURATION + 10)) +log_info "Recording mono audio from $AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME, pcm=$AUDIO_ALSA_CAPTURE_ROUTE_PCM_LABEL control='$AUDIO_ALSA_CAPTURE_ROUTE_CONTROL'" + +rm -f "$capture_output" + +audio_exec_with_timeout "${capture_timeout}s" \ + env LC_ALL=C \ + arecord \ + -q \ + -D "$AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME" \ + -f S16_LE \ + -r 48000 \ + -c 1 \ + -d "$DURATION" \ + "$capture_output" >"$capture_log" 2>&1 +capture_rc=$? + +if audio_validate_recording_result \ + "$capture_output" \ + mic \ + 48000 \ + 1 \ + "$DURATION" \ + "$capture_rc" \ + "${capture_timeout}s" \ + "$capture_log"; then + test_result_record "PASS" \ + "Headset microphone capture produced a valid active WAV through $AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME" +else + test_result_record "FAIL" \ + "$TESTNAME FAIL - headset microphone capture validation failed on $AUDIO_ALSA_CAPTURE_ROUTE_DEVICE_NAME, rc=$capture_rc, verify the connected headset microphone and injected audio signal, validation='$AUDIO_WAV_VALIDATION_SUMMARY', artifact=$capture_log" +fi + +if [ "$DMESG_SCAN" -eq 1 ]; then + scan_dmesg_errors \ + "$LOGDIR" \ + 'snd|asoc|audio|lpass|q6|codec|jack|headset|mic' \ + 'dummy regulator|supply [^ ]+ not found|using dummy regulator|probe deferred' || true + + if [ -s "$LOGDIR/dmesg_errors.log" ]; then + log_warn "Audio-related kernel errors were captured in $LOGDIR/dmesg_errors.log" + fi +fi + +if ! audio_alsa_restore_capture_route; then + test_result_record "FAIL" \ + "$TESTNAME FAIL - capture finished but the previous mixer state could not be restored, control='$AUDIO_ALSA_CAPTURE_ROUTE_CONTROL'" +fi + +test_result_finish diff --git a/Runner/suites/Multimedia/Audio/Audio_Card_Registration/README_Audio_Card_Registration.md b/Runner/suites/Multimedia/Audio/Audio_Card_Registration/README_Audio_Card_Registration.md index cc4d287a..14925b08 100644 --- a/Runner/suites/Multimedia/Audio/Audio_Card_Registration/README_Audio_Card_Registration.md +++ b/Runner/suites/Multimedia/Audio/Audio_Card_Registration/README_Audio_Card_Registration.md @@ -49,7 +49,10 @@ This test intentionally does not: - Change mixer controls - Download or use audio clips -Playback and record functionality should remain covered by the existing `AudioPlayback` and `AudioRecord` tests. +Generic playback and record functionality remains covered by `AudioPlayback` +and `AudioRecord`. Connector-specific HDMI, DisplayPort/eDP, and 3.5 mm +playback routing is covered by `AudioRoutePlayback`. Wired headset-microphone +capture routing is covered by `AudioRouteRecord`. ## Default Behavior