From 0497e59ba3ac30ad2f68ebe056d3cea748b1d9db Mon Sep 17 00:00:00 2001 From: Teja Swaroop Moida Date: Fri, 2 Jan 2026 18:42:40 +0530 Subject: [PATCH] Audio: Increase pre-merge test coverage to 10 test cases Expanded audio testing from 2 to 10 test cases in the pre-merge CI pipeline to improve validation coverage across different audio formats and configurations. Changes: - AudioRecord: Added support for 10 recording configurations - AudioPlayback: Added support for 20 playback configurations - meta-ar-ci-premerge.yaml: Updated to run 7 playback + 3 record tests AudioRecord enhancements: * Implemented config discovery mode with 10 predefined test configurations * Added --config-name and --config-filter CLI options for flexible test selection * Updated documentation with configuration table and usage examples * Modified YAML to support new config parameters (defaults to record_config1) AudioPlayback enhancements: * Implemented clip discovery mode with 20 predefined test configurations * Added --clip-name and --clip-filter CLI options for flexible test selection * Updated documentation with configuration table and usage examples * Modified YAML to support new clip parameters (defaults to Config1) Pre-merge CI updates: * 7 playback tests: Config1, Config7, Config13, Config15, Config18, Config20, Config5 * 3 record tests: record_config1, record_config7, record_config10 * Uses pre-staged clips at /home/AudioClips/ for faster execution * 10-second recording duration for efficient CI runtime Test coverage now includes: - Sample rates: 8KHz to 384KHz - Bit depths: 8-bit to 32-bit - Channel configs: Mono, Stereo, 5.1, 7.1 surround Signed-off-by: Teja Swaroop Moida --- Runner/plans/meta-ar-ci-premerge.yaml | 55 +- .../Audio/AudioPlayback/AudioPlayback.yaml | 8 +- .../Multimedia/Audio/AudioPlayback/Read_me.md | 174 ++++- .../Multimedia/Audio/AudioPlayback/run.sh | 268 ++++++- .../Audio/AudioRecord/AudioRecord.yaml | 14 +- .../Multimedia/Audio/AudioRecord/Read_me.md | 250 +++++-- .../Multimedia/Audio/AudioRecord/run.sh | 678 +++++++++++++----- Runner/utils/audio_common.sh | 523 +++++++++++++- 8 files changed, 1669 insertions(+), 301 deletions(-) diff --git a/Runner/plans/meta-ar-ci-premerge.yaml b/Runner/plans/meta-ar-ci-premerge.yaml index 83811ffa..eba2c475 100644 --- a/Runner/plans/meta-ar-ci-premerge.yaml +++ b/Runner/plans/meta-ar-ci-premerge.yaml @@ -1,22 +1,61 @@ metadata: format: Lava-Test Test Definition 1.0 name: SmokeSanity - description: "Pre-merge LAVA plan to run AudioRecord and AudioPlayback on every PR" + description: "Pre-merge LAVA plan to run AudioPlayback and AudioRecord test cases on every PR" maintainer: - - abbajaj@qti.qualcomm.com + - tmoida@qti.qualcomm.com os: - openembedded scope: - functional - devices: - - rb3gen2 run: steps: - cd Runner - - $PWD/suites/Multimedia/Audio/AudioPlayback/run.sh || true - - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioPlayback/AudioPlayback.res || true - - $PWD/suites/Multimedia/Audio/AudioRecord/run.sh || true + + # ========== AudioPlayback Test Cases (7 configs) ========== + + # Playback Test 1: Config1 (16KHz, 16-bit, 2ch) + - $PWD/suites/Multimedia/Audio/AudioPlayback/run.sh --clip-name "Config1" --audio-clips-path /home/AudioClips/ --no-extract-assets || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioPlayback/AudioPlayback.res || true + + # Playback Test 2: Config7 (24KHz, 24-bit, 6ch) + - $PWD/suites/Multimedia/Audio/AudioPlayback/run.sh --clip-name "Config7" --audio-clips-path /home/AudioClips/ --no-extract-assets || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioPlayback/AudioPlayback.res || true + + # Playback Test 3: Config13 (44.1KHz, 16-bit, 1ch) + - $PWD/suites/Multimedia/Audio/AudioPlayback/run.sh --clip-name "Config13" --audio-clips-path /home/AudioClips/ --no-extract-assets || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioPlayback/AudioPlayback.res || true + + # Playback Test 4: Config15 (48KHz, 8-bit, 2ch) + - $PWD/suites/Multimedia/Audio/AudioPlayback/run.sh --clip-name "Config15" --audio-clips-path /home/AudioClips/ --no-extract-assets || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioPlayback/AudioPlayback.res || true + + # Playback Test 5: Config18 (88.2KHz, 24-bit, 2ch) + - $PWD/suites/Multimedia/Audio/AudioPlayback/run.sh --clip-name "Config18" --audio-clips-path /home/AudioClips/ --no-extract-assets || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioPlayback/AudioPlayback.res || true + + # Playback Test 6: Config20 (96KHz, 24-bit, 6ch) + - $PWD/suites/Multimedia/Audio/AudioPlayback/run.sh --clip-name "Config20" --audio-clips-path /home/AudioClips/ --no-extract-assets || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioPlayback/AudioPlayback.res || true + + # Playback Test 7: Config5 (192KHz, 32-bit, 8ch) + - $PWD/suites/Multimedia/Audio/AudioPlayback/run.sh --clip-name "Config5" --audio-clips-path /home/AudioClips/ --no-extract-assets || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioPlayback/AudioPlayback.res || true + + # ========== AudioRecord Test Cases (3 configs) ========== + + # Record Test 1: record_config1 (8KHz, 1ch) + - $PWD/suites/Multimedia/Audio/AudioRecord/run.sh --config-name "record_config1" --record-seconds 10s || true - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioRecord/AudioRecord.res || true + + # Record Test 2: record_config7 (48KHz, 2ch) + - $PWD/suites/Multimedia/Audio/AudioRecord/run.sh --config-name "record_config7" --record-seconds 10s || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioRecord/AudioRecord.res || true + + # Record Test 3: record_config10 (96KHz, 6ch) + - $PWD/suites/Multimedia/Audio/AudioRecord/run.sh --config-name "record_config10" --record-seconds 10s || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioRecord/AudioRecord.res || true + + # Parse and report results - $PWD/utils/result_parse.sh - diff --git a/Runner/suites/Multimedia/Audio/AudioPlayback/AudioPlayback.yaml b/Runner/suites/Multimedia/Audio/AudioPlayback/AudioPlayback.yaml index a8dbf238..af45c8ce 100644 --- a/Runner/suites/Multimedia/Audio/AudioPlayback/AudioPlayback.yaml +++ b/Runner/suites/Multimedia/Audio/AudioPlayback/AudioPlayback.yaml @@ -8,8 +8,10 @@ metadata: - functional params: - AUDIO_BACKEND: "" # Selects backend: pipewire or pulseaudio, default: auto-detect + AUDIO_BACKEND: "" # Selects backend: pipewire or pulseaudio, default: auto-detect SINK_CHOICE: "speakers" # Playback sink: speakers or null, default: speakers + CLIP_NAMES: "Config1" # Test specific clips (e.g., "Config1 Config2" or "play_48KHz_8b_2ch"), default: Config1 + CLIP_FILTER: "" # Filter clips by pattern (e.g., "48KHz" or "16b" or "2ch"), default: unset FORMATS: "wav" # Audio formats: e.g. wav, default: wav DURATIONS: "short" # Playback durations: short, medium, long, default: short LOOPS: 1 # Number of playback loops, default: 1 @@ -29,5 +31,5 @@ run: steps: - REPO_PATH=$PWD - cd Runner/suites/Multimedia/Audio/AudioPlayback/ - - ./run.sh --backend "${AUDIO_BACKEND}" --sink "${SINK_CHOICE}" --formats "${FORMATS}" --durations "${DURATIONS}" --loops "${LOOPS}" --timeout "${TIMEOUT}" --strict "${STRICT}" --audio-clips-path "${AUDIO_CLIPS_BASE_DIR}" --ssid "${SSID}" --password "${PASSWORD}" || true - - $REPO_PATH/Runner/utils/send-to-lava.sh AudioPlayback.res || true \ No newline at end of file + - ./run.sh --backend "${AUDIO_BACKEND}" --sink "${SINK_CHOICE}" --clip-name "${CLIP_NAMES}" --clip-filter "${CLIP_FILTER}" --formats "${FORMATS}" --durations "${DURATIONS}" --loops "${LOOPS}" --timeout "${TIMEOUT}" --strict "${STRICT}" --audio-clips-path "${AUDIO_CLIPS_BASE_DIR}" --ssid "${SSID}" --password "${PASSWORD}" || true + - $REPO_PATH/Runner/utils/send-to-lava.sh AudioPlayback.res || true diff --git a/Runner/suites/Multimedia/Audio/AudioPlayback/Read_me.md b/Runner/suites/Multimedia/Audio/AudioPlayback/Read_me.md index a7c0dcf8..b0325122 100644 --- a/Runner/suites/Multimedia/Audio/AudioPlayback/Read_me.md +++ b/Runner/suites/Multimedia/Audio/AudioPlayback/Read_me.md @@ -8,6 +8,12 @@ This suite automates the validation of audio playback capabilities on Qualcomm L ## Features - Supports **PipeWire** and **PulseAudio** backends +- **20-clip test coverage**: Comprehensive validation across diverse audio formats (sample rates: 8KHz-352.8KHz, bit depths: 8b-32b, channels: 1ch-8ch) +- **Flexible clip selection**: + - Use generic config names (Config1-Config20) for easy selection + - Use descriptive names (e.g., play_48KHz_16b_2ch) for specific formats + - Auto-discovery mode tests all available clips +- **Clip filtering**: Filter tests by sample rate, bit rate, or channel configuration - Plays audio clips with configurable format, duration, and loop count - **Network operations are optional**: By default, no network connection is attempted. Use `--enable-network-download` to enable downloading missing audio files - Automatically downloads and extracts audio assets if missing @@ -20,6 +26,38 @@ This suite automates the validation of audio playback capabilities on Qualcomm L - Generates `.res` result file and optional JUnit XML output +## Audio Clip Configurations + +The test suite includes 20 diverse audio clip configurations covering various sample rates, bit depths, and channel configurations: + +Config Descriptive Name Sample Rate Bit Rate Channels +Config1 play_16KHz_16b_2ch 16 KHz 16-bit 2ch +Config2 play_176.4KHz_24b_1ch 176.4 KHz 24-bit 1ch +Config3 play_176.4KHz_32b_6ch 176.4 KHz 32-bit 6ch +Config4 play_192KHz_16b_6ch 192 KHz 16-bit 6ch +Config5 play_192KHz_32b_8ch 192 KHz 32-bit 8ch +Config6 play_22.050KHz_8b_1ch 22.05 KHz 8-bit 1ch +Config7 play_24KHz_24b_6ch 24 KHz 24-bit 6ch +Config8 play_24KHz_32b_8ch 24 KHz 32-bit 8ch +Config9 play_32KHz_16b_2ch 32 KHz 16-bit 2ch +Config10 play_32KHz_8b_8ch 32 KHz 8-bit 8ch +Config11 play_352.8KHz_32b_1ch 352.8 KHz 32-bit 1ch +Config12 play_384KHz_32b_2ch 384 KHz 32-bit 2ch +Config13 play_44.1KHz_16b_1ch 44.1 KHz 16-bit 1ch +Config14 play_44.1KHz_8b_6ch 44.1 KHz 8-bit 6ch +Config15 play_48KHz_8b_2ch 48 KHz 8-bit 2ch +Config16 play_48KHz_8b_8ch 48 KHz 8-bit 8ch +Config17 play_88.2KHz_16b_8ch 88.2 KHz 16-bit 8ch +Config18 play_88.2KHz_24b_2ch 88.2 KHz 24-bit 2ch +Config19 play_8KHz_8b_1ch 8 KHz 8-bit 1ch +Config20 play_96KHz_24b_6ch 96 KHz 24-bit 6ch + +Coverage Summary: +- Sample Rates: 8 KHz, 16 KHz, 22.05 KHz, 24 KHz, 32 KHz, 44.1 KHz, 48 KHz, 88.2 KHz, 96 KHz, 176.4 KHz, 192 KHz, 352.8 KHz, 384 KHz +- Bit Depths: 8-bit, 16-bit, 24-bit, 32-bit +- Channel Configurations: 1ch (Mono), 2ch (Stereo), 6ch (5.1 Surround), 8ch (7.1 Surround) +- Total Configurations: 20 unique audio format combinations + ## Prerequisites Ensure the following components are present in the target Yocto build: @@ -112,6 +150,33 @@ AUDIO_CLIPS_BASE_DIR="/tmp/ci-audio-staging/AudioClips" ./run-test.sh AudioPlayb **Directly from Test Directory** cd Runner/suites/Multimedia/Audio/AudioPlayback + +# Test all 20 clips (auto-discovery mode) +./run.sh --no-extract-assets + +# Test specific clips using Config naming (Config1 to Config20) +./run.sh --no-extract-assets --clip-name "Config1" +./run.sh --no-extract-assets --clip-name "Config1 Config5 Config10" + +# Test specific clips using descriptive names +./run.sh --no-extract-assets --clip-name "play_48KHz_8b_2ch" +./run.sh --no-extract-assets --clip-name "play_8KHz_8b_1ch" +./run.sh --no-extract-assets --clip-name "play_192KHz_32b_8ch" + +# Filter clips by sample rate +./run.sh --no-extract-assets --clip-filter "48KHz" +./run.sh --no-extract-assets --clip-filter "192KHz" + +# Filter clips by bit depth +./run.sh --no-extract-assets --clip-filter "16b" +./run.sh --no-extract-assets --clip-filter "24b" + +# Filter clips by channel configuration +./run.sh --no-extract-assets --clip-filter "2ch" +./run.sh --no-extract-assets --clip-filter "8ch" + +# Combine filters (tests clips matching any pattern) +./run.sh --no-extract-assets --clip-filter "48KHz 16b" # Show usage/help ./run.sh --help @@ -130,31 +195,33 @@ cd Runner/suites/Multimedia/Audio/AudioPlayback Environment Variables: -Variable Description Default -AUDIO_BACKEND Selects backend: pipewire or pulseaudio auto-detect -SINK_CHOICE Playback sink: speakers or null speakers -FORMATS Audio formats: e.g. wav wav -DURATIONS Playback durations: short, medium, long short -LOOPS Number of playback loops 1 -TIMEOUT Playback timeout per loop (e.g., 15s, 0=none) 0 -STRICT Enable strict mode (fail on any error) 0 -DMESG_SCAN Scan dmesg for errors after playback 1 -VERBOSE Enable verbose logging 0 -EXTRACT_AUDIO_ASSETS Download/extract audio assets if missing true -ENABLE_NETWORK_DOWNLOAD Enable network download of missing audio files false -AUDIO_CLIPS_BASE_DIR Custom path to pre-staged audio clips (CI use) unset -JUNIT_OUT Path to write JUnit XML output unset -SSID Wi-Fi SSID for network connection unset -PASSWORD Wi-Fi password for network connection unset -NET_PROBE_ROUTE_IP IP used for route probing (default: 1.1.1.1) 1.1.1.1 -NET_PING_HOST Host used for ping reachability check 8.8.8.8 +Variable Description Default +AUDIO_BACKEND Selects backend: pipewire or pulseaudio auto-detect +SINK_CHOICE Playback sink: speakers or null speakers +FORMATS Audio formats: e.g. wav wav +DURATIONS Playback durations: short, medium, long short +LOOPS Number of playback loops 1 +TIMEOUT Playback timeout per loop (e.g., 15s, 0=none) 0 +STRICT Enable strict mode (fail on any error) 0 +DMESG_SCAN Scan dmesg for errors after playback 1 +VERBOSE Enable verbose logging 0 +EXTRACT_AUDIO_ASSETS Download/extract audio assets if missing true +ENABLE_NETWORK_DOWNLOAD Enable network download of missing audio files false +AUDIO_CLIPS_BASE_DIR Custom path to pre-staged audio clips (CI use) unset +JUNIT_OUT Path to write JUnit XML output unset +SSID Wi-Fi SSID for network connection unset +PASSWORD Wi-Fi password for network connection unset +NET_PROBE_ROUTE_IP IP used for route probing (default: 1.1.1.1) 1.1.1.1 +NET_PING_HOST Host used for ping reachability check 8.8.8.8 CLI Options Option Description --backend Select backend: pipewire or pulseaudio --sink Playback sink: speakers or null ---formats Audio formats (space/comma separated): e.g. wav +--clip-name Test specific clips using Config1-Config20 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 --durations Playback durations: short, medium, long --loops Number of playback loops --timeout Playback timeout per loop (e.g., 15s) @@ -170,22 +237,59 @@ Option Description ``` Sample Output: + +**Example 1: Testing specific clip using Config naming** ``` -sh-5.3# ./run.sh --backend pipewire -[INFO] 2025-09-12 05:24:47 - ---------------- Starting AudioPlayback ---------------- -[INFO] 2025-09-12 05:24:47 - SoC: 498 -[INFO] 2025-09-12 05:24:47 - Args: backend=pipewire sink=speakers loops=1 timeout=0 formats='wav' durations='short' strict=0 dmesg=1 extract=true -[INFO] 2025-09-12 05:24:47 - Using backend: pipewire -[INFO] 2025-09-12 05:24:47 - Routing to sink: id=72 name='Built-in Audio Speaker playback' choice=speakers -[INFO] 2025-09-12 05:24:47 - Watchdog/timeout: 0 -[INFO] 2025-09-12 05:24:47 - [play_wav_short] loop 1/1 start=2025-09-12T05:24:47Z clip=AudioClips/yesterday_48KHz.wav backend=pipewire sink=speakers(72) -[INFO] 2025-09-12 05:24:47 - [play_wav_short] exec: pw-play -v "AudioClips/yesterday_48KHz.wav" -[INFO] 2025-09-12 05:26:52 - [play_wav_short] evidence: pw_streaming=1 pa_streaming=0 alsa_running=1 asoc_path_on=1 pw_log=1 -[PASS] 2025-09-12 05:26:52 - [play_wav_short] loop 1 OK (rc=0, 125s) -[INFO] 2025-09-12 05:26:52 - Scanning dmesg for snd|audio|pipewire|pulseaudio: errors & success patterns -[INFO] 2025-09-12 05:26:52 - No snd|audio|pipewire|pulseaudio-related errors found (no OK pattern requested) -[INFO] 2025-09-12 05:26:52 - Summary: total=1 pass=1 fail=0 skip=0 -[PASS] 2025-09-12 05:26:52 - AudioPlayback PASS +sh-5.3# ./run.sh --no-extract-assets --clip-name "Config1" +[INFO] 2025-12-30 11:47:32 - ---------------- Starting AudioPlayback ---------------- +[INFO] 2025-12-30 11:47:32 - Platform Details: machine='Qualcomm Technologies, Inc. Robotics RB3gen2' target='Kodiak' kernel='6.18.0-00393-g27507852413b' arch='aarch64' +[INFO] 2025-12-30 11:47:32 - Args: backend=auto sink=speakers loops=1 timeout=0 formats='wav' durations='short' strict=0 dmesg=1 extract=false network_download=false clips_path=default +[INFO] 2025-12-30 11:47:32 - Using backend: pipewire +[INFO] 2025-12-30 11:47:32 - Routing to sink: id=52 name='Built-in Audio Speaker playback' choice=speakers +[INFO] 2025-12-30 11:47:32 - Using clip discovery mode +[INFO] 2025-12-30 11:47:32 - Discovered 1 clips to test +[INFO] 2025-12-30 11:47:32 - [play_16KHz_16b_2ch] Using clip: yesterday_16KHz_30s_16b_2ch.wav (1922036 bytes) +[INFO] 2025-12-30 11:47:32 - [play_16KHz_16b_2ch] loop 1/1 start=2025-12-30T11:47:32Z clip=yesterday_16KHz_30s_16b_2ch.wav backend=pipewire sink=speakers(52) +[INFO] 2025-12-30 11:47:32 - [play_16KHz_16b_2ch] exec: pw-play -v "AudioClips/yesterday_16KHz_30s_16b_2ch.wav" +[INFO] 2025-12-30 11:48:02 - [play_16KHz_16b_2ch] evidence: pw_streaming=1 pa_streaming=0 alsa_running=1 asoc_path_on=1 pw_log=1 +[PASS] 2025-12-30 11:48:02 - [play_16KHz_16b_2ch] loop 1 OK (rc=0, 30s) +[INFO] 2025-12-30 11:48:02 - Summary: total=1 pass=1 fail=0 skip=0 +[PASS] 2025-12-30 11:48:02 - AudioPlayback PASS +``` + +**Example 2: Testing multiple clips** +``` +sh-5.3# ./run.sh --no-extract-assets --clip-name "Config1 Config2 Config3" +[INFO] 2025-12-30 11:48:13 - Using clip discovery mode +[INFO] 2025-12-30 11:48:13 - Discovered 3 clips to test +[INFO] 2025-12-30 11:48:13 - [play_16KHz_16b_2ch] Using clip: yesterday_16KHz_30s_16b_2ch.wav (1922036 bytes) +[PASS] 2025-12-30 11:48:43 - [play_16KHz_16b_2ch] loop 1 OK (rc=0, 30s) +[INFO] 2025-12-30 11:48:43 - [play_176.4KHz_24b_1ch] Using clip: yesterday_176.4KHz_30s_24b_1ch.wav (15892062 bytes) +[PASS] 2025-12-30 11:49:14 - [play_176.4KHz_24b_1ch] loop 1 OK (rc=0, 31s) +[INFO] 2025-12-30 11:49:14 - [play_176.4KHz_32b_6ch] Using clip: yesterday_176.4KHz_30s_32b_6ch.wav (127135484 bytes) +[PASS] 2025-12-30 11:49:44 - [play_176.4KHz_32b_6ch] loop 1 OK (rc=0, 30s) +[INFO] 2025-12-30 11:49:44 - Summary: total=3 pass=3 fail=0 skip=0 +[PASS] 2025-12-30 11:49:44 - AudioPlayback PASS +``` + +**Example 3: Filtering clips by sample rate** +``` +sh-5.3# ./run.sh --no-extract-assets --clip-filter "48KHz" +[INFO] 2025-12-30 12:00:08 - Using clip discovery mode +[INFO] 2025-12-30 12:00:08 - Discovered 2 clips to test +[INFO] 2025-12-30 12:00:08 - [play_48KHz_8b_2ch] Using clip: yesterday_48KHz_30s_8b_2ch.wav (2883002 bytes) +[PASS] 2025-12-30 12:00:38 - [play_48KHz_8b_2ch] loop 1 OK (rc=0, 30s) +[INFO] 2025-12-30 12:00:38 - [play_48KHz_8b_8ch] Using clip: yesterday_48KHz_30s_8b_8ch.wav (11531688 bytes) +[PASS] 2025-12-30 12:01:08 - [play_48KHz_8b_8ch] loop 1 OK (rc=0, 30s) +[INFO] 2025-12-30 12:01:08 - Summary: total=2 pass=2 fail=0 skip=0 +[PASS] 2025-12-30 12:01:08 - AudioPlayback PASS +``` + +**Example 4: Invalid config name (shows helpful error)** +``` +sh-5.3# ./run.sh --no-extract-assets --clip-name "Config0" +[INFO] 2025-12-30 11:59:52 - Using clip discovery mode +[SKIP] 2025-12-30 11:59:52 - AudioPlayback SKIP - Invalid clip/config name(s) provided. Available range: Config1 to Config20 ``` Results: @@ -211,5 +315,3 @@ Diagnostic logs: dmesg snapshots, mixer dumps, playback logs per test case SPDX-License-Identifier: BSD-3-Clause-Clear (C) Qualcomm Technologies, Inc. and/or its subsidiaries. - - diff --git a/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh b/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh index af5d4d72..051c4c55 100755 --- a/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh +++ b/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh @@ -70,6 +70,11 @@ EXTRACT_AUDIO_ASSETS="${EXTRACT_AUDIO_ASSETS:-true}" ENABLE_NETWORK_DOWNLOAD="${ENABLE_NETWORK_DOWNLOAD:-false}" # Default: no network operations AUDIO_CLIPS_BASE_DIR="${AUDIO_CLIPS_BASE_DIR:-}" # Custom path for audio clips (CI use) +# New clip-based testing options +CLIP_NAMES="" # Explicit clip names to test (e.g., "play_48KHz_16b_2ch play_8KHz_8b_1ch") +CLIP_FILTER="" # Filter pattern for clips (e.g., "48KHz" or "16b") +USE_CLIP_DISCOVERY="${USE_CLIP_DISCOVERY:-auto}" # auto|true|false + # Network bring-up knobs (match video behavior) if [ -z "${NET_STABILIZE_SLEEP:-}" ]; then NET_STABILIZE_SLEEP="5" @@ -86,8 +91,11 @@ usage() { Usage: $0 [options] --backend {pipewire|pulseaudio} --sink {speakers|null} - --formats "wav" - --durations "short|short medium|short medium long" + --formats "wav" # DEPRECATED: Use clip discovery instead + --durations "short|short medium" # DEPRECATED: Use clip discovery instead + --clip-name "play_48KHz_16b_2ch" # Test specific clip(s) by name (space-separated) + # Also supports Config1, Config2, ..., Config20 + --clip-filter "48KHz" # Filter clips by pattern --loops N --timeout SECS # set 0 to disable watchdog --enable-network-download @@ -99,6 +107,19 @@ Usage: $0 [options] --password PASS --verbose --help + +Examples: + # Test all discovered clips + $0 + + # Test specific clips by descriptive name + $0 --clip-name "play_48KHz_16b_2ch play_8KHz_8b_1ch" + + # Test specific clips by config number + $0 --clip-name "Config1 Config2 Config3" + + # Filter clips by pattern + $0 --clip-filter "48KHz" EOF } @@ -114,10 +135,22 @@ while [ $# -gt 0 ]; do ;; --formats) FORMATS="$2" + USE_CLIP_DISCOVERY=false # Explicit formats = use old matrix mode shift 2 ;; --durations) DURATIONS="$2" + USE_CLIP_DISCOVERY=false # Explicit durations = use old matrix mode + shift 2 + ;; + --clip-name) + CLIP_NAMES="$2" + USE_CLIP_DISCOVERY=true + shift 2 + ;; + --clip-filter) + CLIP_FILTER="$2" + USE_CLIP_DISCOVERY=true shift 2 ;; --loops) @@ -179,6 +212,49 @@ if [ -n "$SSID" ] && [ -n "$PASSWORD" ]; then ENABLE_NETWORK_DOWNLOAD=true fi +# ------------- Mode Detection and Validation ------------- +# Determine whether to use clip discovery or legacy matrix mode +if [ "$USE_CLIP_DISCOVERY" = "auto" ]; then + # Auto mode: use clip discovery if AudioClips directory exists, otherwise legacy + clips_dir="${AUDIO_CLIPS_BASE_DIR:-AudioClips}" + if [ -d "$clips_dir" ] && [ -n "$(find "$clips_dir" -maxdepth 1 -name "*.wav" -type f 2>/dev/null | head -n1)" ]; then + USE_CLIP_DISCOVERY=true + log_info "Auto-detected clip discovery mode (found clips in $clips_dir)" + else + USE_CLIP_DISCOVERY=false + log_info "Auto-detected legacy matrix mode (no clips found in $clips_dir)" + fi +fi + +# Show deprecation warnings for legacy options when using clip discovery +if [ "$USE_CLIP_DISCOVERY" = "true" ]; then + if [ "$FORMATS" != "wav" ] || [ "$DURATIONS" != "short" ]; then + log_warn "DEPRECATION WARNING: --formats and --durations are deprecated" + log_info "Use --clip-name or --clip-filter for clip-based testing" + log_info "Legacy options will be ignored in clip discovery mode" + fi +fi + +# Validate CLI option conflicts +if [ -n "$CLIP_NAMES" ] && [ -n "$CLIP_FILTER" ]; then + log_warn "Both --clip-name and --clip-filter specified" + log_info "Using --clip-name (ignoring --clip-filter)" + CLIP_FILTER="" +fi + +# Validate numeric parameters +case "$LOOPS" in + ''|*[!0-9]*) + log_error "Invalid --loops value: $LOOPS (must be positive integer)" + exit 1 + ;; +esac + +if [ "$LOOPS" -le 0 ] 2>/dev/null; then + log_error "Invalid --loops value: $LOOPS (must be positive)" + exit 1 +fi + # Ensure we run from the testcase dir test_path="$(find_test_case_by_name "$TESTNAME" 2>/dev/null || echo "$SCRIPT_DIR")" if ! cd "$test_path"; then @@ -350,15 +426,194 @@ else log_info "Watchdog/timeout: disabled (no timeout)" fi -# ------------- Matrix execution ------------- +# ------------- Test Execution (Matrix or Clip Discovery) ------------- total=0 pass=0 fail=0 skip=0 suite_rc=0 -for fmt in $FORMATS; do - for dur in $DURATIONS; do +if [ "$USE_CLIP_DISCOVERY" = "true" ]; then + # ========== NEW: Clip Discovery Mode ========== + log_info "Using clip discovery mode" + + # Discover and filter clips + clips_dir="${AUDIO_CLIPS_BASE_DIR:-AudioClips}" + + # Get list of clips to test + if [ -n "$CLIP_NAMES" ] || [ -n "$CLIP_FILTER" ]; then + # Use discover_and_filter_clips helper - capture both stdout and stderr + CLIPS_OUTPUT="$(discover_and_filter_clips "$CLIP_NAMES" "$CLIP_FILTER" 2>&1)" + CLIPS_RC=$? + + # Extract just the clip list (non-log lines) + CLIPS_TO_TEST="$(printf '%s\n' "$CLIPS_OUTPUT" | grep -v "^\[[A-Z]*\]")" + + if [ $CLIPS_RC -ne 0 ] || [ -z "$CLIPS_TO_TEST" ]; then + # Extract error message with range information if available + ERROR_MSG="$(printf '%s\n' "$CLIPS_OUTPUT" | grep "^\[ERROR\].*Available range" | head -1)" + if [ -z "$ERROR_MSG" ]; then + # No range info found, get count and show generic message + AVAILABLE_CLIPS="$(discover_audio_clips 2>/dev/null | wc -l)" + log_skip "$TESTNAME SKIP - Invalid clip/config name(s) provided. Available range: Config1 to Config$AVAILABLE_CLIPS" + else + log_skip "$TESTNAME SKIP - $ERROR_MSG" + fi + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + fi + else + # Discover all clips + CLIPS_TO_TEST="$(discover_audio_clips 2>&1)" || { + # Error messages already printed, just skip + log_skip "$TESTNAME SKIP - No audio clips found in $clips_dir" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + } + fi + + # Count clips + clip_count=0 + for clip_file in $CLIPS_TO_TEST; do + clip_count=$((clip_count + 1)) + done + + log_info "Discovered $clip_count clips to test" + + # Test each clip + for clip_file in $CLIPS_TO_TEST; do + # Generate test case name from clip filename + case_name="$(generate_clip_testcase_name "$clip_file")" || { + log_warn "Skipping clip with unparseable name: $clip_file" + continue + } + + # Resolve full path + clip_path="$clips_dir/$clip_file" + + # Validate clip file + if ! validate_clip_file "$clip_path"; then + log_skip "[$case_name] SKIP: Invalid clip file: $clip_path" + echo "$case_name SKIP (invalid file)" >> "$LOGDIR/summary.txt" + skip=$((skip + 1)) + continue + fi + + total=$((total + 1)) + logf="$LOGDIR/${case_name}.log" + : > "$logf" + export AUDIO_LOGCTX="$logf" + + CLIP_BYTES="$(stat -c '%s' "$clip_path" 2>/dev/null || wc -c < "$clip_path" 2>/dev/null || echo 0)" + log_info "[$case_name] Using clip: $clip_file (${CLIP_BYTES} bytes)" + + i=1 + ok_runs=0 + last_elapsed=0 + + while [ "$i" -le "$LOOPS" ]; do + iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + + if [ "$AUDIO_BACKEND" = "pipewire" ]; then + loop_hdr="sink=$SINK_CHOICE($SINK_ID)" + else + loop_hdr="sink=$SINK_CHOICE($SINK_NAME)" + fi + + log_info "[$case_name] loop $i/$LOOPS start=$iso clip=$clip_file backend=$AUDIO_BACKEND $loop_hdr" + + start_s="$(date +%s 2>/dev/null || echo 0)" + + if [ "$AUDIO_BACKEND" = "pipewire" ]; then + log_info "[$case_name] exec: pw-play -v \"$clip_path\"" + audio_exec_with_timeout "$TIMEOUT" pw-play -v "$clip_path" >>"$logf" 2>&1 + rc=$? + else + log_info "[$case_name] exec: paplay --device=\"$SINK_NAME\" \"$clip_path\"" + audio_exec_with_timeout "$TIMEOUT" paplay --device="$SINK_NAME" "$clip_path" >>"$logf" 2>&1 + rc=$? + fi + + end_s="$(date +%s 2>/dev/null || echo 0)" + last_elapsed=$((end_s - start_s)) + if [ "$last_elapsed" -lt 0 ]; then + last_elapsed=0 + fi + + # Evidence collection + pw_ev="$(audio_evidence_pw_streaming || echo 0)" + pa_ev="$(audio_evidence_pa_streaming || echo 0)" + + # Minimal PulseAudio fallback + if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 0 ]; then + if [ "$rc" -eq 0 ] || { [ "$rc" -eq 124 ] && [ "$dur_s" -gt 0 ] 2>/dev/null && [ "$last_elapsed" -ge "$min_ok" ]; }; then + pa_ev=1 + fi + fi + + alsa_ev="$(audio_evidence_alsa_running_any || echo 0)" + asoc_ev="$(audio_evidence_asoc_path_on || echo 0)" + pwlog_ev="$(audio_evidence_pw_log_seen || echo 0)" + if [ "$AUDIO_BACKEND" = "pulseaudio" ]; then + pwlog_ev=0 + fi + + # Fast teardown fallback + if [ "$alsa_ev" -eq 0 ]; then + if [ "$AUDIO_BACKEND" = "pipewire" ] && [ "$pw_ev" -eq 1 ]; then + alsa_ev=1 + fi + if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 1 ]; then + alsa_ev=1 + fi + fi + + if [ "$asoc_ev" -eq 0 ] && [ "$alsa_ev" -eq 1 ]; then + asoc_ev=1 + fi + + log_info "[$case_name] evidence: pw_streaming=$pw_ev pa_streaming=$pa_ev alsa_running=$alsa_ev asoc_path_on=$asoc_ev pw_log=$pwlog_ev" + + # Determine result + if [ "$rc" -eq 0 ]; then + log_pass "[$case_name] loop $i OK (rc=0, ${last_elapsed}s)" + ok_runs=$((ok_runs + 1)) + elif [ "$rc" -eq 124 ] && [ "$dur_s" -gt 0 ] 2>/dev/null && [ "$last_elapsed" -ge "$min_ok" ]; then + log_warn "[$case_name] TIMEOUT ($TIMEOUT) - PASS (ran ~${last_elapsed}s)" + ok_runs=$((ok_runs + 1)) + elif [ "$rc" -ne 0 ] && { [ "$pw_ev" -eq 1 ] || [ "$pa_ev" -eq 1 ] || [ "$alsa_ev" -eq 1 ] || [ "$asoc_ev" -eq 1 ]; }; then + log_warn "[$case_name] nonzero rc=$rc but evidence indicates playback - PASS" + ok_runs=$((ok_runs + 1)) + else + log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s) - see $logf" + fi + + i=$((i + 1)) + done + + # Aggregate result for this clip + if [ "$ok_runs" -ge 1 ]; then + pass=$((pass + 1)) + echo "$case_name PASS" >> "$LOGDIR/summary.txt" + else + fail=$((fail + 1)) + echo "$case_name FAIL" >> "$LOGDIR/summary.txt" + suite_rc=1 + fi + done + + # Collect evidence once at end (not per clip) + if [ "$DMESG_SCAN" -eq 1 ]; then + scan_audio_dmesg "$LOGDIR" + dump_mixers "$LOGDIR/mixer_dump.txt" + fi + +else + # ========== LEGACY: Matrix Mode ========== + log_info "Using legacy matrix mode (formats × durations)" + + for fmt in $FORMATS; do + for dur in $DURATIONS; do clip="$(resolve_clip "$fmt" "$dur")" case_name="play_${fmt}_${dur}" total=$((total + 1)) @@ -488,8 +743,9 @@ for fmt in $FORMATS; do echo "$case_name FAIL" >> "$LOGDIR/summary.txt" suite_rc=1 fi + done done -done +fi log_info "Summary: total=$total pass=$pass fail=$fail skip=$skip" diff --git a/Runner/suites/Multimedia/Audio/AudioRecord/AudioRecord.yaml b/Runner/suites/Multimedia/Audio/AudioRecord/AudioRecord.yaml index e6d72a43..e6e6488e 100644 --- a/Runner/suites/Multimedia/Audio/AudioRecord/AudioRecord.yaml +++ b/Runner/suites/Multimedia/Audio/AudioRecord/AudioRecord.yaml @@ -8,19 +8,21 @@ metadata: - functional params: - AUDIO_BACKEND: "" # Selects backend: pipewire or pulseaudio, default: auto-detect + AUDIO_BACKEND: "" # Selects backend: pipewire or pulseaudio, default: auto-detect SOURCE_CHOICE: "mic" # Recording source: mic or null, default: mic - DURATIONS: "short" # Playback durations: short, medium, long, default: short + CONFIG_NAMES: "record_config1" # Test specific configs (e.g., "record_config1 record_config2"), default: record_config1 + CONFIG_FILTER: "" # Filter configs by pattern (e.g., "48KHz" or "2ch"), default: unset + DURATIONS: "short" # Recording durations: short, medium, long, default: short RECORD_SECONDS: 5 # Number of seconds to record (numeric or mapped), default: 5 - LOOPS: 1 # Number of playback loops, default: 1 - TIMEOUT: 0 # Playback timeout per loop (e.g., 15s, 0=none), default: 0 + LOOPS: 1 # Number of recording loops, default: 1 + TIMEOUT: 0 # Recording timeout per loop (e.g., 15s, 0=none), default: 0 STRICT: 0 # Enable strict mode (fail on any error), default: 0 - DMESG_SCAN: 1 # Scan dmesg for errors after playback, default: 1 + DMESG_SCAN: 1 # Scan dmesg for errors after recording, default: 1 VERBOSE: 0 # Enable verbose logging, default: 0 run: steps: - REPO_PATH=$PWD - cd Runner/suites/Multimedia/Audio/AudioRecord/ - - ./run.sh --backend "${AUDIO_BACKEND}" --source "${SOURCE_CHOICE}" --durations "${DURATIONS}" --record-seconds "${RECORD_SECONDS}" --loops "${LOOPS}" --timeout "${TIMEOUT}" --strict "${STRICT}" || true + - ./run.sh --backend "${AUDIO_BACKEND}" --source "${SOURCE_CHOICE}" --config-name "${CONFIG_NAMES}" --config-filter "${CONFIG_FILTER}" --durations "${DURATIONS}" --record-seconds "${RECORD_SECONDS}" --loops "${LOOPS}" --timeout "${TIMEOUT}" --strict "${STRICT}" || true - $REPO_PATH/Runner/utils/send-to-lava.sh AudioRecord.res || true diff --git a/Runner/suites/Multimedia/Audio/AudioRecord/Read_me.md b/Runner/suites/Multimedia/Audio/AudioRecord/Read_me.md index 349b2613..84f6b890 100644 --- a/Runner/suites/Multimedia/Audio/AudioRecord/Read_me.md +++ b/Runner/suites/Multimedia/Audio/AudioRecord/Read_me.md @@ -6,18 +6,43 @@ This suite automates the validation of audio recording capabilities on Qualcomm ## Features - - Supports **PipeWire** and **PulseAudio** backends -- Records audio clips with configurable duration and loop count +- **10-config test coverage**: Comprehensive validation across diverse audio formats (sample rates: 8KHz-96KHz, channels: 1ch-6ch) +- **Flexible config selection**: + - Use generic config names (record_config1-record_config10) for easy selection + - Use descriptive names (e.g., record_48KHz_2ch) for specific formats + - Auto-discovery mode tests all available configs +- **Config filtering**: Filter tests by sample rate or channel configuration +- Records audio with configurable duration and loop count - Automatically detects and routes to appropriate source (e.g., mic, null) - Validates recording using multiple evidence sources: - PipeWire/PulseAudio streaming state - ALSA and ASoC runtime status - Kernel logs (`dmesg`) -- Diagnostic logs: dmesg scan, mixer dumps, playback logs +- Diagnostic logs: dmesg scan, mixer dumps, recording logs - Evidence-based validation (user-space, ALSA, ASoC, dmesg) - Generates `.res` result file and optional JUnit XML output +## Audio Record Configurations + +The test suite includes 10 diverse audio record configurations covering various sample rates and channel configurations: + +Config Descriptive Name Sample Rate Channels +record_config1 record_8KHz_1ch 8 KHz 1ch +record_config2 record_16KHz_1ch 16 KHz 1ch +record_config3 record_16KHz_2ch 16 KHz 2ch +record_config4 record_24KHz_1ch 24 KHz 1ch +record_config5 record_32KHz_2ch 32 KHz 2ch +record_config6 record_44.1KHz_2ch 44.1 KHz 2ch +record_config7 record_48KHz_2ch 48 KHz 2ch +record_config8 record_48KHz_6ch 48 KHz 6ch +record_config9 record_96KHz_2ch 96 KHz 2ch +record_config10 record_96KHz_6ch 96 KHz 6ch + +**Coverage Summary:** +- **Sample Rates**: 8 KHz, 16 KHz, 24 KHz, 32 KHz, 44.1 KHz, 48 KHz, 96 KHz +- **Channel Configurations**: 1ch (Mono), 2ch (Stereo), 6ch (5.1 Surround) +- **Total Configurations**: 10 unique audio format combinations ## Prerequisites @@ -25,6 +50,7 @@ Ensure the following components are present in the target Yocto build: - PipeWire: `pw-record`, `wpctl` - PulseAudio: `parecord`, `pactl` +- ALSA: `arecord` - Common tools: `pgrep`, `timeout`, `grep`, `sed` - Daemon: `pipewire` or `pulseaudio` must be running @@ -36,7 +62,7 @@ For overlay builds using audioreach kernel modules, the test automatically: - Restarts PipeWire service - Waits for the service to be ready -This happens transparently before tests run. No manual configuration needed. +This happens transparently before tests run. No manual configuration needed. ## Directory Structure @@ -71,20 +97,44 @@ scp -r Runner user@target_device_ip: ssh user@target_device_ip **Using Unified Runner** -cd Runner +cd /Runner -# Run Audiorecord using PipeWire (auto-detects backend if not specified) -./run-test.sh Audiorecord +# Run AudioRecord using PipeWire (auto-detects backend if not specified) +./run-test.sh AudioRecord # Force PulseAudio backend -AUDIO_BACKEND=pulseaudio ./run-test.sh Audiorecord +AUDIO_BACKEND=pulseaudio ./run-test.sh AudioRecord # Custom options via environment variables -AUDIO_BACKEND=pipewire RECORD_TIMEOUT=20s RECORD_LOOPS=2 RECORD_VOLUME=0.5 ./run-test.sh Audiorecord +AUDIO_BACKEND=pipewire RECORD_SECONDS=10s LOOPS=2 ./run-test.sh AudioRecord **Directly from Test Directory** -cd Runner/suites/Multimedia/Audio/Audiorecord +cd Runner/suites/Multimedia/Audio/AudioRecord + +# Test all 10 configs (auto-discovery mode) +./run.sh + +# Test specific configs using config naming (record_config1 to record_config10) +./run.sh --config-name "record_config1" +./run.sh --config-name "record_config1 record_config2 record_config3" + +# Test specific configs using descriptive names +./run.sh --config-name "record_48KHz_2ch" +./run.sh --config-name "record_8KHz_1ch" +./run.sh --config-name "record_96KHz_6ch" + +# Filter configs by sample rate +./run.sh --config-filter "48KHz" +./run.sh --config-filter "96KHz" + +# Filter configs by channel configuration +./run.sh --config-filter "1ch" +./run.sh --config-filter "2ch" +./run.sh --config-filter "6ch" + +# Combine filters (tests configs matching any pattern) +./run.sh --config-filter "48KHz 2ch" # Show usage/help ./run.sh --help @@ -95,59 +145,152 @@ cd Runner/suites/Multimedia/Audio/Audiorecord # Run with PulseAudio, null source, strict mode, verbose ./run.sh --backend pulseaudio --source null --strict --verbose +# Provide JUnit output and disable dmesg scan +./run.sh --junit results.xml --no-dmesg + Environment Variables: -Variable Description Default -AUDIO_BACKEND Selects backend: pipewire or pulseaudio auto-detect -SOURCE_CHOICE Recording source: mic or null mic -DURATIONS Recording durations: short, medium, long short -RECORD_SECONDS Number of seconds to record (numeric or mapped), default: 30s 30 -LOOPS Number of recording loops 1 -TIMEOUT Recording timeout per loop (e.g., 15s, 0=none) 0 -STRICT Strict mode (0=disabled, 1=enabled, fail on any error) 0 -DMESG_SCAN Scan dmesg for errors after recording 1 -VERBOSE Enable verbose logging 0 -JUNIT_OUT Path to write JUnit XML output unset +Variable Description Default +AUDIO_BACKEND Selects backend: pipewire or pulseaudio auto-detect +SOURCE_CHOICE Recording source: mic or null mic +DURATIONS Recording durations: short, medium, long (deprecated) short +RECORD_SECONDS Number of seconds to record (e.g., 5s, 10s) 30s +LOOPS Number of recording loops 1 +TIMEOUT Recording timeout per loop (e.g., 15s, 0=none) 0 +STRICT Strict mode (0=disabled, 1=enabled, fail on any error) 0 +DMESG_SCAN Scan dmesg for errors after recording 1 +VERBOSE Enable verbose logging 0 +JUNIT_OUT Path to write JUnit XML output unset CLI Options: -Option Description ---backend Select backend: pipewire or pulseaudio ---source Recording source: mic or null ---durations Recording durations: short, medium, long ---record-seconds Number of seconds to record (numeric or mapped) ---loops Number of recording loops ---timeout Recording timeout per loop (e.g., 15s) ---strict [0|1] Enable strict mode (0=disabled, 1=enabled) ---no-dmesg Disable dmesg scan ---junit Write JUnit XML output ---verbose Enable verbose logging ---help Show usage instructions +Option Description +--backend Select backend: pipewire or pulseaudio +--source Recording source: 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) +--durations Recording durations: short, medium, long (deprecated, use --config-name instead) +--loops Number of recording loops +--timeout Recording timeout per loop (e.g., 15s) +--strict [0|1] Enable strict mode (0=disabled, 1=enabled) +--no-dmesg Disable dmesg scan +--junit Write JUnit XML output +--verbose Enable verbose logging +--help Show usage instructions ``` Sample Output: + +**Example 1: Testing specific config using config naming** +``` +sh-5.3# ./run.sh --config-name "record_config1" +[INFO] 2026-01-02 12:00:46 - Base build detected (no audioreach modules), skipping overlay setup +[INFO] 2026-01-02 12:00:46 - ---------------- Starting AudioRecord ---------------- +[INFO] 2026-01-02 12:00:46 - Platform Details: machine='Qualcomm Technologies, Inc. Robotics RB3gen2' target='Kodiak' kernel='6.18.0-00393-g27507852413b' arch='aarch64' +[INFO] 2026-01-02 12:00:46 - Args: backend=auto source=mic loops=1 durations='short' record_seconds=30s timeout=0 strict=0 dmesg=1 +[INFO] 2026-01-02 12:00:46 - Backend fallback chain: pipewire pulseaudio alsa +[INFO] 2026-01-02 12:00:46 - Using backend: pipewire +[INFO] 2026-01-02 12:00:46 - Routing to source: id/name=45 label='Built-in Audio internal Mic' choice=mic +[INFO] 2026-01-02 12:00:46 - Watchdog/timeout: disabled (no timeout) +[INFO] 2026-01-02 12:00:46 - Using config discovery mode +[INFO] 2026-01-02 12:00:46 - Discovered 1 configs to test +[INFO] 2026-01-02 12:00:46 - [record_8KHz_1ch] Using config: record_config1 (rate=8000Hz channels=1) +[INFO] 2026-01-02 12:00:46 - [record_8KHz_1ch] loop 1/1 start=2026-01-02T12:00:46Z rate=8000Hz channels=1 backend=pipewire source=mic(45) +[INFO] 2026-01-02 12:00:46 - [record_8KHz_1ch] exec: pw-record -v --rate=8000 --channels=1 "results/AudioRecord/record_8KHz_1ch.wav" +[WARN] 2026-01-02 12:01:16 - [record_8KHz_1ch] nonzero rc=124 but recording looks valid (bytes=482634) - PASS +[INFO] 2026-01-02 12:01:16 - [record_8KHz_1ch] evidence: pw_streaming=1 pa_streaming=0 alsa_running=1 asoc_path_on=1 bytes=482634 pw_log=1 +[PASS] 2026-01-02 12:01:16 - [record_8KHz_1ch] loop 1 OK (rc=0, 30s, bytes=482634) +[INFO] 2026-01-02 12:01:16 - No relevant, non-benign errors for modules [results/AudioRecord] in recent dmesg. +[INFO] 2026-01-02 12:01:16 - Summary: total=1 pass=1 fail=0 skip=0 +[PASS] 2026-01-02 12:01:16 - AudioRecord PASS +``` + +**Example 2: Testing multiple configs** +``` +sh-5.3# ./run.sh --config-name "record_config1 record_config2" +[INFO] 2026-01-02 11:47:53 - Using config discovery mode +[INFO] 2026-01-02 11:47:53 - Discovered 2 configs to test +[INFO] 2026-01-02 11:47:53 - [record_8KHz_1ch] Using config: record_config1 (rate=8000Hz channels=1) +[PASS] 2026-01-02 11:48:23 - [record_8KHz_1ch] loop 1 OK (rc=0, 30s, bytes=482292) +[INFO] 2026-01-02 11:48:23 - [record_16KHz_1ch] Using config: record_config2 (rate=16000Hz channels=1) +[PASS] 2026-01-02 11:48:53 - [record_16KHz_1ch] loop 1 OK (rc=0, 30s, bytes=957768) +[INFO] 2026-01-02 11:48:53 - Summary: total=2 pass=2 fail=0 skip=0 +[PASS] 2026-01-02 11:48:53 - AudioRecord PASS +``` + +**Example 3: Filtering configs by sample rate** +``` +sh-5.3# ./run.sh --config-filter "48KHz" +[INFO] 2026-01-02 11:52:22 - Using config discovery mode +[INFO] 2026-01-02 11:52:22 - Discovered 2 configs to test +[INFO] 2026-01-02 11:52:22 - [record_48KHz_2ch] Using config: record_config7 (rate=48000Hz channels=2) +[PASS] 2026-01-02 11:52:53 - [record_48KHz_2ch] loop 1 OK (rc=0, 31s, bytes=5791788) +[INFO] 2026-01-02 11:52:53 - [record_48KHz_6ch] Using config: record_config8 (rate=48000Hz channels=6) +[PASS] 2026-01-02 11:53:23 - [record_48KHz_6ch] loop 1 OK (rc=0, 30s, bytes=17240144) +[INFO] 2026-01-02 11:53:23 - Summary: total=2 pass=2 fail=0 skip=0 +[PASS] 2026-01-02 11:53:23 - AudioRecord PASS +``` + +**Example 4: Filtering configs by channel configuration** +``` +sh-5.3# ./run.sh --config-filter "2ch" +[INFO] 2026-01-02 11:53:38 - Using config discovery mode +[INFO] 2026-01-02 11:53:38 - Discovered 5 configs to test +[INFO] 2026-01-02 11:53:38 - [record_16KHz_2ch] Using config: record_config3 (rate=16000Hz channels=2) +[PASS] 2026-01-02 11:54:08 - [record_16KHz_2ch] loop 1 OK (rc=0, 30s, bytes=1930512) +[INFO] 2026-01-02 11:54:08 - [record_32KHz_2ch] Using config: record_config5 (rate=32000Hz channels=2) +[PASS] 2026-01-02 11:54:38 - [record_32KHz_2ch] loop 1 OK (rc=0, 30s, bytes=3833788) +[INFO] 2026-01-02 11:54:38 - [record_44.1KHz_2ch] Using config: record_config6 (rate=44100Hz channels=2) +[PASS] 2026-01-02 11:55:08 - [record_44.1KHz_2ch] loop 1 OK (rc=0, 30s, bytes=5283464) +[INFO] 2026-01-02 11:55:08 - [record_48KHz_2ch] Using config: record_config7 (rate=48000Hz channels=2) +[PASS] 2026-01-02 11:55:38 - [record_48KHz_2ch] loop 1 OK (rc=0, 30s, bytes=5746732) +[INFO] 2026-01-02 11:55:38 - [record_96KHz_2ch] Using config: record_config9 (rate=96000Hz channels=2) +[PASS] 2026-01-02 11:56:09 - [record_96KHz_2ch] loop 1 OK (rc=0, 30s, bytes=11509556) +[INFO] 2026-01-02 11:56:09 - Summary: total=5 pass=5 fail=0 skip=0 +[PASS] 2026-01-02 11:56:09 - AudioRecord PASS +``` + +**Example 5: Invalid config name (shows helpful error)** +``` +sh-5.3# ./run.sh --config-name "record_config99" +[INFO] 2026-01-02 11:59:34 - Using config discovery mode +[SKIP] 2026-01-02 11:59:34 - AudioRecord SKIP - [ERROR] 2026-01-02 11:59:34 - Available range: record_config1 to record_config10 +``` + +**Example 6: Testing all 10 configs with short duration** ``` -sh-5.2# ./run.sh --backend pipewire -[INFO] 2025-09-12 06:06:04 - ---------------- Starting AudioRecord ---------------- -[INFO] 2025-09-12 06:06:04 - SoC: 498 -[INFO] 2025-09-12 06:06:04 - Args: backend=pipewire source=mic loops=1 durations='short' record_seconds=30s timeout=0 strict=0 dmesg=1 -[INFO] 2025-09-12 06:06:04 - Using backend: pipewire -[INFO] 2025-09-12 06:06:04 - Routing to source: id/name=48 label='pal source handset mic' choice=mic -[INFO] 2025-09-12 06:06:04 - Watchdog/timeout: 0 -[INFO] 2025-09-12 06:06:04 - [record_short] loop 1/1 start=2025-09-12T06:06:04Z secs=30s backend=pipewire source=mic(48) -[INFO] 2025-09-12 06:06:04 - [record_short] exec: pw-record -v "results/AudioRecord/record_short.wav" -[WARN] 2025-09-12 06:06:34 - [record_short] nonzero rc=124 but recording looks valid (bytes=5738540) - PASS -[INFO] 2025-09-12 06:06:34 - [record_short] evidence: pw_streaming=1 pa_streaming=0 alsa_running=1 asoc_path_on=1 bytes=5738540 pw_log=1 -[PASS] 2025-09-12 06:06:34 - [record_short] loop 1 OK (rc=0, 30s, bytes=5738540) -[INFO] 2025-09-12 06:06:34 - Scanning dmesg for snd|audio|pipewire|pulseaudio: errors & success patterns -[INFO] 2025-09-12 06:06:34 - No snd|audio|pipewire|pulseaudio-related errors found (no OK pattern requested) -[INFO] 2025-09-12 06:06:34 - Summary: total=1 pass=1 fail=0 skip=0 -[PASS] 2025-09-12 06:06:34 - AudioRecord PASS +sh-5.3# ./run.sh --record-seconds 3s +[INFO] 2026-01-02 12:05:26 - Auto-detected config discovery mode (testing all 10 record configs) +[INFO] 2026-01-02 12:05:26 - Using config discovery mode +[INFO] 2026-01-02 12:05:26 - Discovered 10 configs to test +[INFO] 2026-01-02 12:05:26 - [record_8KHz_1ch] Using config: record_config1 (rate=8000Hz channels=1) +[PASS] 2026-01-02 12:05:30 - [record_8KHz_1ch] loop 1 OK (rc=0, 3s, bytes=49822) +[INFO] 2026-01-02 12:05:30 - [record_16KHz_1ch] Using config: record_config2 (rate=16000Hz channels=1) +[PASS] 2026-01-02 12:05:33 - [record_16KHz_1ch] loop 1 OK (rc=0, 3s, bytes=96242) +[INFO] 2026-01-02 12:05:33 - [record_16KHz_2ch] Using config: record_config3 (rate=16000Hz channels=2) +[PASS] 2026-01-02 12:05:36 - [record_16KHz_2ch] loop 1 OK (rc=0, 3s, bytes=186980) +[INFO] 2026-01-02 12:05:36 - [record_24KHz_1ch] Using config: record_config4 (rate=24000Hz channels=1) +[PASS] 2026-01-02 12:05:39 - [record_24KHz_1ch] loop 1 OK (rc=0, 3s, bytes=142322) +[INFO] 2026-01-02 12:05:39 - [record_32KHz_2ch] Using config: record_config5 (rate=32000Hz channels=2) +[PASS] 2026-01-02 12:05:42 - [record_32KHz_2ch] loop 1 OK (rc=0, 3s, bytes=376764) +[INFO] 2026-01-02 12:05:42 - [record_44.1KHz_2ch] Using config: record_config6 (rate=44100Hz channels=2) +[PASS] 2026-01-02 12:05:46 - [record_44.1KHz_2ch] loop 1 OK (rc=0, 4s, bytes=523016) +[INFO] 2026-01-02 12:05:46 - [record_48KHz_2ch] Using config: record_config7 (rate=48000Hz channels=2) +[PASS] 2026-01-02 12:05:49 - [record_48KHz_2ch] loop 1 OK (rc=0, 3s, bytes=565292) +[INFO] 2026-01-02 12:05:49 - [record_48KHz_6ch] Using config: record_config8 (rate=48000Hz channels=6) +[PASS] 2026-01-02 12:05:52 - [record_48KHz_6ch] loop 1 OK (rc=0, 3s, bytes=1695824) +[INFO] 2026-01-02 12:05:52 - [record_96KHz_2ch] Using config: record_config9 (rate=96000Hz channels=2) +[PASS] 2026-01-02 12:05:55 - [record_96KHz_2ch] loop 1 OK (rc=0, 3s, bytes=1138484) +[INFO] 2026-01-02 12:05:55 - [record_96KHz_6ch] Using config: record_config10 (rate=96000Hz channels=6) +[PASS] 2026-01-02 12:05:59 - [record_96KHz_6ch] loop 1 OK (rc=0, 3s, bytes=3415400) +[INFO] 2026-01-02 12:05:59 - Summary: total=10 pass=10 fail=0 skip=0 +[PASS] 2026-01-02 12:05:59 - AudioRecord PASS ``` Results: -- Results are stored in: results/Audiorecord/ -- Summary result file: Audiorecord.res +- Results are stored in: results/AudioRecord/ +- Summary result file: AudioRecord.res - JUnit XML (if enabled): .xml - Diagnostic logs: dmesg snapshots, mixer dumps, record logs per test case @@ -158,8 +301,11 @@ Results: - If any critical tool is missing, the script exits with an error message. - Logs include dmesg snapshots, mixer dumps, and record logs. - Evidence-based PASS/FAIL logic ensures reliability even if backend quirks occur. +- **Config discovery mode** is enabled by default, testing all 10 configurations automatically. +- Use `--config-name` to test specific configurations or `--config-filter` to filter by sample rate or channels. +- The `--durations` option is deprecated; use `--config-name` or `--config-filter` for better control. ## License SPDX-License-Identifier: BSD-3-Clause-Clear -(C) Qualcomm Technologies, Inc. and/or its subsidiaries. \ No newline at end of file +(C) Qualcomm Technologies, Inc. and/or its subsidiaries. diff --git a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh index 9db8fe2a..ad9f8a07 100755 --- a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh +++ b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh @@ -53,11 +53,19 @@ DMESG_SCAN="${DMESG_SCAN:-1}" VERBOSE=0 JUNIT_OUT="" +# New config-based testing options +CONFIG_NAMES="" # Explicit config names to test (e.g., "record_config1 record_config2") +CONFIG_FILTER="" # Filter pattern for configs (e.g., "48KHz" or "2ch") +USE_CONFIG_DISCOVERY="${USE_CONFIG_DISCOVERY:-auto}" # auto|true|false + usage() { cat < "$logf" - export AUDIO_LOGCTX="$logf" - - secs="$RECORD_SECONDS" - if [ "$secs" = "auto" ]; then - tok="$(printf '%s' "$dur" | tr '[:upper:]' '[:lower:]')" - tok_secs="$(printf '%s' "$tok" | sed -n 's/^\([0-9][0-9]*\)\(s\|sec\|secs\|seconds\)$/\1s/p')" - if [ -n "$tok_secs" ]; then - secs="$tok_secs" +if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then + # ========== NEW: Config Discovery Mode ========== + log_info "Using config discovery mode" + + # Discover and filter configs + CONFIGS_OUTPUT="$(discover_and_filter_record_configs "$CONFIG_NAMES" "$CONFIG_FILTER" 2>&1)" + CONFIGS_RC=$? + + # Extract just the config list (non-log lines) + CONFIGS_TO_TEST="$(printf '%s\n' "$CONFIGS_OUTPUT" | grep -v "^\[[A-Z]*\]")" + + if [ $CONFIGS_RC -ne 0 ] || [ -z "$CONFIGS_TO_TEST" ]; then + # Extract error message with range information if available + ERROR_MSG="$(printf '%s\n' "$CONFIGS_OUTPUT" | grep "^\[ERROR\].*Available range" | head -1)" + if [ -z "$ERROR_MSG" ]; then + log_skip "$TESTNAME SKIP - No valid record configs found" else - secs="$(auto_secs_for "$dur")" + log_skip "$TESTNAME SKIP - $ERROR_MSG" fi + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 2 fi - - i=1 - ok_runs=0 - last_elapsed=0 - - while [ "$i" -le "$LOOPS" ]; do - iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - effective_timeout="$secs" - if [ -n "$TIMEOUT" ] && [ "$TIMEOUT" != "0" ]; then - effective_timeout="$TIMEOUT" + + # Count configs + config_count=0 + for config in $CONFIGS_TO_TEST; do + config_count=$((config_count + 1)) + done + + log_info "Discovered $config_count configs to test" + + # Test each config + for config in $CONFIGS_TO_TEST; do + # Generate test case name + case_name="$(generate_record_testcase_name "$config")" || { + log_warn "Skipping config with invalid name: $config" + continue + } + + # Get recording parameters + params="$(get_record_config_params "$config")" || { + log_warn "Skipping config with invalid parameters: $config" + continue + } + + rate="$(printf '%s' "$params" | awk '{print $1}')" + channels="$(printf '%s' "$params" | awk '{print $2}')" + + total=$((total + 1)) + logf="$LOGDIR/${case_name}.log" + : > "$logf" + export AUDIO_LOGCTX="$logf" + + log_info "[$case_name] Using config: $config (rate=${rate}Hz channels=$channels)" + + # Determine recording duration + secs="$RECORD_SECONDS" + if [ "$secs" = "auto" ]; then + secs="5s" # Default for config discovery mode fi - - loop_hdr="source=$SRC_CHOICE" - if [ "$AUDIO_BACKEND" = "pipewire" ]; then - if [ -n "$SRC_ID" ]; then - loop_hdr="$loop_hdr($SRC_ID)" + + i=1 + ok_runs=0 + last_elapsed=0 + + while [ "$i" -le "$LOOPS" ]; do + iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + effective_timeout="$secs" + if [ -n "$TIMEOUT" ] && [ "$TIMEOUT" != "0" ]; then + effective_timeout="$TIMEOUT" + fi + + loop_hdr="source=$SRC_CHOICE" + if [ "$AUDIO_BACKEND" = "pipewire" ]; then + if [ -n "$SRC_ID" ]; then + loop_hdr="$loop_hdr($SRC_ID)" + else + loop_hdr="$loop_hdr(default)" + fi else - loop_hdr="$loop_hdr(default)" + loop_hdr="$loop_hdr($SRC_LABEL)" fi - else - loop_hdr="$loop_hdr($SRC_LABEL)" - fi - - log_info "[$case_name] loop $i/$LOOPS start=$iso secs=$secs backend=$AUDIO_BACKEND $loop_hdr" - - out="$LOGDIR/${case_name}.wav" - : > "$out" - - start_s="$(date +%s 2>/dev/null || echo 0)" - - if [ "$AUDIO_BACKEND" = "pipewire" ]; then - log_info "[$case_name] exec: pw-record -v \"$out\"" - audio_exec_with_timeout "$effective_timeout" pw-record -v "$out" >> "$logf" 2>&1 - rc=$? - bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" - - # If we already got real audio, accept and skip fallbacks - if [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - if [ "$rc" -ne 0 ]; then - log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" + + log_info "[$case_name] loop $i/$LOOPS start=$iso rate=${rate}Hz channels=$channels backend=$AUDIO_BACKEND $loop_hdr" + + out="$LOGDIR/${case_name}.wav" + : > "$out" + + start_s="$(date +%s 2>/dev/null || echo 0)" + + if [ "$AUDIO_BACKEND" = "pipewire" ]; then + log_info "[$case_name] exec: pw-record -v --rate=$rate --channels=$channels \"$out\"" + audio_exec_with_timeout "$effective_timeout" pw-record -v --rate="$rate" --channels="$channels" "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + + # If we already got real audio, accept and skip fallbacks + if [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + if [ "$rc" -ne 0 ]; then + log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" + rc=0 + fi + else + # Only if output is tiny/empty do we try a virtual PCM (pipewire/pulse/default) + if command -v arecord >/dev/null 2>&1; then + pcm="$(alsa_pick_virtual_pcm || true)" + if [ -n "$pcm" ]; then + secs_int="$(audio_parse_secs "$secs" 2>/dev/null || echo 0)"; [ -z "$secs_int" ] && secs_int=0 + : > "$out" + log_info "[$case_name] fallback: arecord -D $pcm -f S16_LE -r $rate -c $channels -d $secs_int \"$out\"" + audio_exec_with_timeout "$effective_timeout" \ + arecord -D "$pcm" -f S16_LE -r "$rate" -c "$channels" -d "$secs_int" "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + fi + fi + + # As a last resort, retry pw-record with --target (only if we have a source id) + if { [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; } && [ -n "$SRC_ID" ]; then + : > "$out" + log_info "[$case_name] exec: pw-record -v --rate=$rate --channels=$channels --target \"$SRC_ID\" \"$out\"" + audio_exec_with_timeout "$effective_timeout" pw-record -v --rate="$rate" --channels="$channels" --target "$SRC_ID" "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + fi + fi + + # (Optional safety) If nonzero rc but output is clearly valid, accept. + if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + log_warn "[$case_name] nonzero rc==$rc but recording looks valid (bytes=$bytes) - PASS" rc=0 fi else - # Only if output is tiny/empty do we try a virtual PCM (pipewire/pulse/default) - if command -v arecord >/dev/null 2>&1; then - pcm="$(alsa_pick_virtual_pcm || true)" - if [ -n "$pcm" ]; then - secs_int="$(audio_parse_secs "$secs" 2>/dev/null || echo 0)"; [ -z "$secs_int" ] && secs_int=0 + if [ "$AUDIO_BACKEND" = "alsa" ]; then + secs_int="$(audio_parse_secs "$secs" 2>/dev/null || echo 0)" + [ -z "$secs_int" ] && secs_int=0 + log_info "[$case_name] exec: arecord -D \"$SRC_ID\" -f S16_LE -r $rate -c $channels -d $secs_int \"$out\"" + audio_exec_with_timeout "$effective_timeout" \ + arecord -D "$SRC_ID" -f S16_LE -r "$rate" -c "$channels" -d "$secs_int" "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + + if [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then + if printf '%s\n' "$SRC_ID" | grep -q '^hw:'; then + alt_dev="plughw:${SRC_ID#hw:}" + else + alt_dev="$SRC_ID" + fi + + # Try with the specific config parameters : > "$out" - log_info "[$case_name] fallback: arecord -D $pcm -f S16_LE -r 48000 -c 2 -d $secs_int \"$out\"" + log_info "[$case_name] retry: arecord -D \"$alt_dev\" -f S16_LE -r $rate -c $channels -d $secs_int \"$out\"" audio_exec_with_timeout "$effective_timeout" \ - arecord -D "$pcm" -f S16_LE -r 48000 -c 2 -d "$secs_int" "$out" >> "$logf" 2>&1 + arecord -D "$alt_dev" -f S16_LE -r "$rate" -c "$channels" -d "$secs_int" "$out" >> "$logf" 2>&1 rc=$? bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + + # If still failing, try fallback combinations + if [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then + for combo in "S16_LE 48000 2" "S16_LE 44100 2" "S16_LE 16000 1"; do + fmt=$(printf '%s\n' "$combo" | awk '{print $1}') + fallback_rate=$(printf '%s\n' "$combo" | awk '{print $2}') + fallback_ch=$(printf '%s\n' "$combo" | awk '{print $3}') + [ -z "$fmt" ] || [ -z "$fallback_rate" ] || [ -z "$fallback_ch" ] && continue + : > "$out" + log_info "[$case_name] fallback: arecord -D \"$alt_dev\" -f $fmt -r $fallback_rate -c $fallback_ch -d $secs_int \"$out\"" + audio_exec_with_timeout "$effective_timeout" \ + arecord -D "$alt_dev" -f "$fmt" -r "$fallback_rate" -c "$fallback_ch" -d "$secs_int" "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + break + fi + done + fi fi - fi - - # As a last resort, retry pw-record with --target (only if we have a source id) - if { [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; } && [ -n "$SRC_ID" ]; then - : > "$out" - log_info "[$case_name] exec: pw-record -v --target \"$SRC_ID\" \"$out\"" - audio_exec_with_timeout "$effective_timeout" pw-record -v --target "$SRC_ID" "$out" >> "$logf" 2>&1 + + if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" + rc=0 + fi + else + # PulseAudio + log_info "[$case_name] exec: parecord --rate=$rate --channels=$channels --file-format=wav \"$out\"" + audio_exec_with_timeout "$effective_timeout" parecord --rate="$rate" --channels="$channels" --file-format=wav "$out" >> "$logf" 2>&1 rc=$? bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" + rc=0 + fi + fi + fi + + end_s="$(date +%s 2>/dev/null || echo 0)" + last_elapsed=$((end_s - start_s)) + [ "$last_elapsed" -lt 0 ] && last_elapsed=0 + + # Evidence + pw_ev=$(audio_evidence_pw_streaming || echo 0) + pa_ev=$(audio_evidence_pa_streaming || echo 0) + + if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 0 ]; then + if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + pa_ev=1 + fi + fi + + alsa_ev=$(audio_evidence_alsa_running_any || echo 0) + asoc_ev=$(audio_evidence_asoc_path_on || echo 0) + pwlog_ev=$(audio_evidence_pw_log_seen || echo 0) + if [ "$AUDIO_BACKEND" = "pulseaudio" ]; then + pwlog_ev=0 + fi + + if [ "$alsa_ev" -eq 0 ]; then + if [ "$AUDIO_BACKEND" = "pipewire" ] && [ "$pw_ev" -eq 1 ]; then + alsa_ev=1 + fi + if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 1 ]; then + alsa_ev=1 fi fi + + if [ "$asoc_ev" -eq 0 ] && [ "$alsa_ev" -eq 1 ]; then + asoc_ev=1 + fi + + log_info "[$case_name] evidence: pw_streaming=$pw_ev pa_streaming=$pa_ev alsa_running=$alsa_ev asoc_path_on=$asoc_ev bytes=${bytes:-0} pw_log=$pwlog_ev" + + if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + log_pass "[$case_name] loop $i OK (rc=0, ${last_elapsed}s, bytes=$bytes)" + ok_runs=$((ok_runs + 1)) + else + log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s, bytes=${bytes:-0}) - see $logf" + fi + + i=$((i + 1)) + done + + # Collect evidence once per config + if [ "$DMESG_SCAN" -eq 1 ]; then + scan_audio_dmesg "$LOGDIR" + dump_mixers "$LOGDIR/mixer_dump.txt" + fi + + # Aggregate result for this config + status="FAIL" + if [ "$ok_runs" -ge 1 ]; then + status="PASS" + fi + + append_junit "$case_name" "$last_elapsed" "$status" "$logf" + + case "$status" in + PASS) + pass=$((pass + 1)) + echo "$case_name PASS" >> "$LOGDIR/summary.txt" + ;; + SKIP) + skip=$((skip + 1)) + echo "$case_name SKIP" >> "$LOGDIR/summary.txt" + ;; + FAIL) + fail=$((fail + 1)) + echo "$case_name FAIL" >> "$LOGDIR/summary.txt" + suite_rc=1 + ;; + esac + done +else + # ========== LEGACY: Matrix Mode ========== + for dur in $DURATIONS; do + case_name="record_${dur}" + total=$((total + 1)) + + logf="$LOGDIR/${case_name}.log" + : > "$logf" + export AUDIO_LOGCTX="$logf" + + secs="$RECORD_SECONDS" + if [ "$secs" = "auto" ]; then + tok="$(printf '%s' "$dur" | tr '[:upper:]' '[:lower:]')" + tok_secs="$(printf '%s' "$tok" | sed -n 's/^\([0-9][0-9]*\)\(s\|sec\|secs\|seconds\)$/\1s/p')" + if [ -n "$tok_secs" ]; then + secs="$tok_secs" + else + secs="$(auto_secs_for "$dur")" + fi + fi + + i=1 + ok_runs=0 + last_elapsed=0 - # (Optional safety) If nonzero rc but output is clearly valid, accept. - if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - log_warn "[$case_name] nonzero rc==$rc but recording looks valid (bytes=$bytes) - PASS" - rc=0 + while [ "$i" -le "$LOOPS" ]; do + iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + effective_timeout="$secs" + if [ -n "$TIMEOUT" ] && [ "$TIMEOUT" != "0" ]; then + effective_timeout="$TIMEOUT" fi - else - if [ "$AUDIO_BACKEND" = "alsa" ]; then - secs_int="$(audio_parse_secs "$secs" 2>/dev/null || echo 0)" - [ -z "$secs_int" ] && secs_int=0 - log_info "[$case_name] exec: arecord -D \"$SRC_ID\" -f S16_LE -r 48000 -c 2 -d $secs_int \"$out\"" - audio_exec_with_timeout "$effective_timeout" \ - arecord -D "$SRC_ID" -f S16_LE -r 48000 -c 2 -d "$secs_int" "$out" >> "$logf" 2>&1 + + loop_hdr="source=$SRC_CHOICE" + if [ "$AUDIO_BACKEND" = "pipewire" ]; then + if [ -n "$SRC_ID" ]; then + loop_hdr="$loop_hdr($SRC_ID)" + else + loop_hdr="$loop_hdr(default)" + fi + else + loop_hdr="$loop_hdr($SRC_LABEL)" + fi + + log_info "[$case_name] loop $i/$LOOPS start=$iso secs=$secs backend=$AUDIO_BACKEND $loop_hdr" + + out="$LOGDIR/${case_name}.wav" + : > "$out" + + start_s="$(date +%s 2>/dev/null || echo 0)" + + if [ "$AUDIO_BACKEND" = "pipewire" ]; then + log_info "[$case_name] exec: pw-record -v \"$out\"" + audio_exec_with_timeout "$effective_timeout" pw-record -v "$out" >> "$logf" 2>&1 rc=$? bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" - if [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then - if printf '%s\n' "$SRC_ID" | grep -q '^hw:'; then - alt_dev="plughw:${SRC_ID#hw:}" - else - alt_dev="$SRC_ID" + # If we already got real audio, accept and skip fallbacks + if [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + if [ "$rc" -ne 0 ]; then + log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" + rc=0 fi - for combo in "S16_LE 48000 2" "S16_LE 44100 2" "S16_LE 16000 1"; do - fmt=$(printf '%s\n' "$combo" | awk '{print $1}') - rate=$(printf '%s\n' "$combo" | awk '{print $2}') - ch=$(printf '%s\n' "$combo" | awk '{print $3}') - [ -z "$fmt" ] || [ -z "$rate" ] || [ -z "$ch" ] && continue + else + # Only if output is tiny/empty do we try a virtual PCM (pipewire/pulse/default) + if command -v arecord >/dev/null 2>&1; then + pcm="$(alsa_pick_virtual_pcm || true)" + if [ -n "$pcm" ]; then + secs_int="$(audio_parse_secs "$secs" 2>/dev/null || echo 0)"; [ -z "$secs_int" ] && secs_int=0 + : > "$out" + log_info "[$case_name] fallback: arecord -D $pcm -f S16_LE -r 48000 -c 2 -d $secs_int \"$out\"" + audio_exec_with_timeout "$effective_timeout" \ + arecord -D "$pcm" -f S16_LE -r 48000 -c 2 -d "$secs_int" "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + fi + fi + + # As a last resort, retry pw-record with --target (only if we have a source id) + if { [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; } && [ -n "$SRC_ID" ]; then : > "$out" - log_info "[$case_name] retry: arecord -D \"$alt_dev\" -f $fmt -r $rate -c $ch -d $secs_int \"$out\"" - audio_exec_with_timeout "$effective_timeout" \ - arecord -D "$alt_dev" -f "$fmt" -r "$rate" -c "$ch" -d "$secs_int" "$out" >> "$logf" 2>&1 + log_info "[$case_name] exec: pw-record -v --target \"$SRC_ID\" \"$out\"" + audio_exec_with_timeout "$effective_timeout" pw-record -v --target "$SRC_ID" "$out" >> "$logf" 2>&1 rc=$? bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" - if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - break - fi - done + fi fi + # (Optional safety) If nonzero rc but output is clearly valid, accept. if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" + log_warn "[$case_name] nonzero rc==$rc but recording looks valid (bytes=$bytes) - PASS" rc=0 fi else - log_info "[$case_name] exec: parecord --file-format=wav \"$out\"" - audio_exec_with_timeout "$effective_timeout" parecord --file-format=wav "$out" >> "$logf" 2>&1 - rc=$? - bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" - if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" - rc=0 + if [ "$AUDIO_BACKEND" = "alsa" ]; then + secs_int="$(audio_parse_secs "$secs" 2>/dev/null || echo 0)" + [ -z "$secs_int" ] && secs_int=0 + log_info "[$case_name] exec: arecord -D \"$SRC_ID\" -f S16_LE -r 48000 -c 2 -d $secs_int \"$out\"" + audio_exec_with_timeout "$effective_timeout" \ + arecord -D "$SRC_ID" -f S16_LE -r 48000 -c 2 -d "$secs_int" "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + + if [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then + if printf '%s\n' "$SRC_ID" | grep -q '^hw:'; then + alt_dev="plughw:${SRC_ID#hw:}" + else + alt_dev="$SRC_ID" + fi + for combo in "S16_LE 48000 2" "S16_LE 44100 2" "S16_LE 16000 1"; do + fmt=$(printf '%s\n' "$combo" | awk '{print $1}') + rate=$(printf '%s\n' "$combo" | awk '{print $2}') + ch=$(printf '%s\n' "$combo" | awk '{print $3}') + [ -z "$fmt" ] || [ -z "$rate" ] || [ -z "$ch" ] && continue + : > "$out" + log_info "[$case_name] retry: arecord -D \"$alt_dev\" -f $fmt -r $rate -c $ch -d $secs_int \"$out\"" + audio_exec_with_timeout "$effective_timeout" \ + arecord -D "$alt_dev" -f "$fmt" -r "$rate" -c "$ch" -d "$secs_int" "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + break + fi + done + fi + + if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" + rc=0 + fi + else + log_info "[$case_name] exec: parecord --file-format=wav \"$out\"" + audio_exec_with_timeout "$effective_timeout" parecord --file-format=wav "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(stat -c '%s' "$out" 2>/dev/null || wc -c < "$out")" + if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" + rc=0 + fi fi fi - fi - end_s="$(date +%s 2>/dev/null || echo 0)" - last_elapsed=$((end_s - start_s)) - [ "$last_elapsed" -lt 0 ] && last_elapsed=0 + end_s="$(date +%s 2>/dev/null || echo 0)" + last_elapsed=$((end_s - start_s)) + [ "$last_elapsed" -lt 0 ] && last_elapsed=0 - # Evidence - pw_ev=$(audio_evidence_pw_streaming || echo 0) - pa_ev=$(audio_evidence_pa_streaming || echo 0) + # Evidence + pw_ev=$(audio_evidence_pw_streaming || echo 0) + pa_ev=$(audio_evidence_pa_streaming || echo 0) - if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 0 ]; then - if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - pa_ev=1 + if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 0 ]; then + if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + pa_ev=1 + fi fi - fi - alsa_ev=$(audio_evidence_alsa_running_any || echo 0) - asoc_ev=$(audio_evidence_asoc_path_on || echo 0) - pwlog_ev=$(audio_evidence_pw_log_seen || echo 0) - if [ "$AUDIO_BACKEND" = "pulseaudio" ]; then - pwlog_ev=0 - fi - - if [ "$alsa_ev" -eq 0 ]; then - if [ "$AUDIO_BACKEND" = "pipewire" ] && [ "$pw_ev" -eq 1 ]; then - alsa_ev=1 - fi - if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 1 ]; then - alsa_ev=1 + alsa_ev=$(audio_evidence_alsa_running_any || echo 0) + asoc_ev=$(audio_evidence_asoc_path_on || echo 0) + pwlog_ev=$(audio_evidence_pw_log_seen || echo 0) + if [ "$AUDIO_BACKEND" = "pulseaudio" ]; then + pwlog_ev=0 fi - fi - if [ "$asoc_ev" -eq 0 ] && [ "$alsa_ev" -eq 1 ]; then - asoc_ev=1 - fi + if [ "$alsa_ev" -eq 0 ]; then + if [ "$AUDIO_BACKEND" = "pipewire" ] && [ "$pw_ev" -eq 1 ]; then + alsa_ev=1 + fi + if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 1 ]; then + alsa_ev=1 + fi + fi - log_info "[$case_name] evidence: pw_streaming=$pw_ev pa_streaming=$pa_ev alsa_running=$alsa_ev asoc_path_on=$asoc_ev bytes=${bytes:-0} pw_log=$pwlog_ev" + if [ "$asoc_ev" -eq 0 ] && [ "$alsa_ev" -eq 1 ]; then + asoc_ev=1 + fi - if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - log_pass "[$case_name] loop $i OK (rc=0, ${last_elapsed}s, bytes=$bytes)" - ok_runs=$((ok_runs + 1)) - else - log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s, bytes=${bytes:-0}) - see $logf" - fi + log_info "[$case_name] evidence: pw_streaming=$pw_ev pa_streaming=$pa_ev alsa_running=$alsa_ev asoc_path_on=$asoc_ev bytes=${bytes:-0} pw_log=$pwlog_ev" - i=$((i + 1)) - done + if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + log_pass "[$case_name] loop $i OK (rc=0, ${last_elapsed}s, bytes=$bytes)" + ok_runs=$((ok_runs + 1)) + else + log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s, bytes=${bytes:-0}) - see $logf" + fi - if [ "$DMESG_SCAN" -eq 1 ]; then - scan_audio_dmesg "$LOGDIR" - dump_mixers "$LOGDIR/mixer_dump.txt" - fi + i=$((i + 1)) + done - status="FAIL" - if [ "$ok_runs" -ge 1 ]; then - status="PASS" - fi + # Collect evidence once per duration + if [ "$DMESG_SCAN" -eq 1 ]; then + scan_audio_dmesg "$LOGDIR" + dump_mixers "$LOGDIR/mixer_dump.txt" + fi - append_junit "$case_name" "$last_elapsed" "$status" "$logf" + # Aggregate result for this duration + status="FAIL" + if [ "$ok_runs" -ge 1 ]; then + status="PASS" + fi - case "$status" in - PASS) - pass=$((pass + 1)) - echo "$case_name PASS" >> "$LOGDIR/summary.txt" - ;; - SKIP) - skip=$((skip + 1)) - echo "$case_name SKIP" >> "$LOGDIR/summary.txt" - ;; - FAIL) - fail=$((fail + 1)) - echo "$case_name FAIL" >> "$LOGDIR/summary.txt" - suite_rc=1 - ;; - esac -done + append_junit "$case_name" "$last_elapsed" "$status" "$logf" -log_info "Summary: total=$total pass=$pass fail=$fail skip=$skip" + case "$status" in + PASS) + pass=$((pass + 1)) + echo "$case_name PASS" >> "$LOGDIR/summary.txt" + ;; + SKIP) + skip=$((skip + 1)) + echo "$case_name SKIP" >> "$LOGDIR/summary.txt" + ;; + FAIL) + fail=$((fail + 1)) + echo "$case_name FAIL" >> "$LOGDIR/summary.txt" + suite_rc=1 + ;; + esac + done +fi +# JUnit finalize (optional) if [ -n "$JUNIT_OUT" ]; then - tests=$((pass + fail + skip)) - failures="$fail" - skipped="$skip" { - printf '\n' "$TESTNAME" "$tests" "$failures" "$skipped" + printf '\n' + printf '\n' + printf '\n' "Audio.Record" "$total" "$fail" "$skip" cat "$JUNIT_TMP" printf '\n' + printf '\n' } > "$JUNIT_OUT" - log_info "Wrote JUnit: $JUNIT_OUT" + rm -f "$JUNIT_TMP" fi -# Exit codes: PASS=0, FAIL=1, SKIP=2 +log_info "Summary: total=$total pass=$pass fail=$fail skip=$skip" + +# --- Proper exit codes: PASS=0, FAIL=1, SKIP-only=0 --- if [ "$pass" -eq 0 ] && [ "$fail" -eq 0 ] && [ "$skip" -gt 0 ]; then log_skip "$TESTNAME SKIP" echo "$TESTNAME SKIP" > "$RES_FILE" - exit 2 + exit 0 fi if [ "$suite_rc" -eq 0 ]; then diff --git a/Runner/utils/audio_common.sh b/Runner/utils/audio_common.sh index d7e77c5f..03f406e6 100755 --- a/Runner/utils/audio_common.sh +++ b/Runner/utils/audio_common.sh @@ -359,22 +359,6 @@ pa_resolve_mic_fallback() { printf '%s\n' "$s" } -# PipeWire sink label by ID (tries description, then node.name, then status line) -pw_sink_name_safe() { - id="$1"; [ -n "$id" ] || return 1 - name="$(wpctl inspect "$id" 2>/dev/null | grep -m1 'node.description' | cut -d'"' -f2)" - [ -n "$name" ] || name="$(wpctl inspect "$id" 2>/dev/null | grep -m1 'node.name' | cut -d'"' -f2)" - if [ -z "$name" ]; then - name="$(wpctl status 2>/dev/null \ - | sed -n '/^[[:space:]]*Sinks:/,/^[[:space:]]*$/p' \ - | grep -E "^[[:space:]]*\*?[[:space:]]*${id}[.][[:space:]]" \ - | sed 's/^[[:space:]]*\*\?[[:space:]]*[0-9]\+[.][[:space:]]\+//' \ - | sed 's/[[:space:]]*\[vol:.*$//' \ - | head -n1)" - fi - printf '%s\n' "$name" -} - # ----------- PulseAudio Source Helpers ----------- pa_default_mic() { def="$(pactl info 2>/dev/null | sed -n 's/^Default Source:[[:space:]]*//p' | head -n1)" @@ -754,4 +738,509 @@ audio_check_clips_available() { done done return 0 # All clips present and non-empty -} \ No newline at end of file +} + +# ---------- New Clip Discovery Functions (for 20-clip enhancement) ---------- + +# Discover all audio clip files in the clips directory +# Returns: space-separated list of clip filenames (basenames only) +# Exit codes: 0=success, 1=directory not found or no clips +discover_audio_clips() { + clips_dir="${AUDIO_CLIPS_BASE_DIR:-AudioClips}" + + # Check directory exists + if [ ! -d "$clips_dir" ]; then + log_error "Clips directory not found: $clips_dir" + return 1 + fi + + # Find .wav files (only in top level, not recursive) + clips="$(find "$clips_dir" -maxdepth 1 -name "*.wav" -type f 2>/dev/null | sort)" + + # Check if any clips found + if [ -z "$clips" ]; then + log_error "No .wav files found in $clips_dir" + return 1 + fi + + # Return basenames only + for clip in $clips; do + basename "$clip" + done + return 0 +} + +# Parse clip filename to extract metadata +# Input: yesterday_48KHz_30s_16b_2ch.wav +# Output: rate=48KHz bits=16b channels=2ch (space-separated key=value pairs) +# Returns: 0=success, 1=parse failure +parse_clip_metadata() { + filename="$1" + + # Expected pattern: yesterday_{rate}_{duration}_{bits}_{channels}.wav + # Extract components using sed + rate="$(printf '%s' "$filename" | sed -n 's/.*_\([0-9.]\+KHz\)_.*/\1/p')" + bits="$(printf '%s' "$filename" | sed -n 's/.*_\([0-9]\+b\)_.*/\1/p')" + channels="$(printf '%s' "$filename" | sed -n 's/.*_\([0-9]\+ch\)\.wav$/\1/p')" + + # Validate all components extracted + if [ -z "$rate" ] || [ -z "$bits" ] || [ -z "$channels" ]; then + log_warn "Cannot parse metadata from: $filename (skipping)" + return 1 + fi + + printf 'rate=%s bits=%s channels=%s\n' "$rate" "$bits" "$channels" + return 0 +} + +# Generate test case name from clip filename +# Input: yesterday_48KHz_30s_16b_2ch.wav +# Output: play_48KHz_16b_2ch +# Returns: 0=success, 1=parse failure +generate_clip_testcase_name() { + filename="$1" + + # Parse metadata + metadata="$(parse_clip_metadata "$filename")" || return 1 + + # Extract values + rate="$(printf '%s' "$metadata" | sed -n 's/.*rate=\([^ ]*\).*/\1/p')" + bits="$(printf '%s' "$metadata" | sed -n 's/.*bits=\([^ ]*\).*/\1/p')" + channels="$(printf '%s' "$metadata" | sed -n 's/.*channels=\([^ ]*\).*/\1/p')" + + # Generate test case name + printf 'play_%s_%s_%s\n' "$rate" "$bits" "$channels" + return 0 +} + +# Resolve clip file path from test case name or clip name +# Input: play_48KHz_16b_2ch OR 48KHz_16b_2ch OR yesterday_48KHz_30s_16b_2ch.wav +# Output: AudioClips/yesterday_48KHz_30s_16b_2ch.wav +# Returns: 0=success, 1=not found +resolve_clip_by_name() { + name="$1" + clips_dir="${AUDIO_CLIPS_BASE_DIR:-AudioClips}" + + # If name already looks like a filename, try direct path + if printf '%s' "$name" | grep -q '\.wav$'; then + clip_path="$clips_dir/$name" + if [ -f "$clip_path" ]; then + printf '%s\n' "$clip_path" + return 0 + fi + fi + + # Strip "play_" prefix if present + search_name="$(printf '%s' "$name" | sed 's/^play_//')" + + # Search for matching clip + for clip_file in "$clips_dir"/*.wav; do + [ -f "$clip_file" ] || continue + clip_basename="$(basename "$clip_file")" + + # Check if clip contains the search pattern + if printf '%s' "$clip_basename" | grep -q "$search_name"; then + printf '%s\n' "$clip_file" + return 0 + fi + done + + return 1 +} + +# Validate clip name against available clips +# Input: requested_name (e.g., play_48KHz_16b_2ch OR Config1), available_clips (list) +# Output: matching clip filename +# Returns: 0=found, 1=not found (with helpful error message) +validate_clip_name() { + requested_name="$1" + available_clips="$2" + + # Check if requested_name is a generic config name (Config1, Config2, etc.) + # Support both "Config1" and "config1" (case-insensitive) + config_num="$(printf '%s' "$requested_name" | sed -n 's/^[Cc]onfig\([0-9]\+\)$/\1/p')" + + if [ -n "$config_num" ]; then + # Generic config name - map to clip by index (1-based) + # Count total clips first + idx=0 + for clip in $available_clips; do + idx=$((idx + 1)) + done + + # Validate config number is positive and within range + if [ "$config_num" -le 0 ] 2>/dev/null || [ "$config_num" -gt "$idx" ] 2>/dev/null; then + log_error "Invalid config number: $requested_name. Available range: Config1 to Config$idx. Please check again." + return 1 + fi + + # Get clip by index (1-based) + current_idx=0 + for clip in $available_clips; do + current_idx=$((current_idx + 1)) + if [ "$current_idx" -eq "$config_num" ]; then + printf '%s\n' "$clip" + return 0 + fi + done + + # This shouldn't happen, but just in case + log_error "Invalid config number: $requested_name. Available range: Config1 to Config$idx. Please check again." + return 1 + fi + + # Try exact match for specific clip names (play_48KHz_16b_2ch format) + for clip in $available_clips; do + test_name="$(generate_clip_testcase_name "$clip" 2>/dev/null)" || continue + if [ "$test_name" = "$requested_name" ]; then + printf '%s\n' "$clip" + return 0 + fi + done + + # No match found - count available clips for helpful message + idx=0 + for clip in $available_clips; do + idx=$((idx + 1)) + done + + # No match found - provide helpful error message with range + log_error "Wrong clip name: '$requested_name'. Available range: Config1 to Config$idx. Please check again." + return 1 +} + +# Input: filter (space-separated patterns), available_clips (list) +# Output: filtered clip list +# Returns: 0=success, 1=no matches +apply_clip_filter() { + filter="$1" + available_clips="$2" + + # If no filter, return all clips + if [ -z "$filter" ]; then + printf '%s\n' "$available_clips" + return 0 + fi + + # Apply filter + filtered="" + for clip in $available_clips; do + for pattern in $filter; do + # Match against filename or test case name + test_name="$(generate_clip_testcase_name "$clip" 2>/dev/null)" || continue + if printf '%s %s' "$clip" "$test_name" | grep -q "$pattern"; then + filtered="$filtered $clip" + break + fi + done + done + + # Remove leading space + filtered="$(printf '%s' "$filtered" | sed 's/^ //')" + + # Check if filter matched anything + if [ -z "$filtered" ]; then + log_error "Filter '$filter' matched no clips" + log_info "Available clips:" + for clip in $available_clips; do + log_info " - $(basename "$clip")" + done + return 1 + fi + + printf '%s\n' "$filtered" + return 0 +} + +# Validate clip file is accessible and non-empty +# Input: clip_path +# Returns: 0=valid, 1=invalid +validate_clip_file() { + clip_path="$1" + + # Check exists + if [ ! -f "$clip_path" ]; then + log_error "Clip file not found: $clip_path" + return 1 + fi + + # Check readable + if [ ! -r "$clip_path" ]; then + log_error "Clip file not readable: $clip_path" + return 1 + fi + + # Check not empty + size="$(stat -c '%s' "$clip_path" 2>/dev/null || wc -c < "$clip_path" 2>/dev/null)" + if [ -z "$size" ] || [ "$size" -le 0 ] 2>/dev/null; then + log_error "Clip file is empty: $clip_path" + return 1 + fi + + return 0 +} + +# Discover and filter clips based on user input +# Input: clip_names (explicit list), clip_filter (pattern filter) +# Output: final list of clip filenames to test +# Returns: 0=success, 1=no valid clips +discover_and_filter_clips() { + clip_names="$1" + clip_filter="$2" + + # Discover all available clips + available_clips="$(discover_audio_clips 2>&1)" || { + log_error "Failed to discover audio clips" >&2 + return 1 + } + + # If explicit clip names provided, validate and use them + if [ -n "$clip_names" ]; then + validated="" + failed_names="" + + for name in $clip_names; do + # Validate clip name - let error messages display to stderr + if clip="$(validate_clip_name "$name" "$available_clips")"; then + validated="$validated $clip" + else + failed_names="$failed_names $name" + fi + done + + validated="$(printf '%s' "$validated" | sed 's/^ //')" + failed_names="$(printf '%s' "$failed_names" | sed 's/^ //')" + + if [ -z "$validated" ]; then + # Don't repeat the error - validate_clip_name already showed it + return 1 + fi + + # Warn about any failed names (only if there are some valid ones) + if [ -n "$failed_names" ]; then + log_warn "Invalid clip/config names skipped: $failed_names" >&2 + fi + + printf '%s\n' "$validated" + return 0 + fi + + # Apply filter if provided + if [ -n "$clip_filter" ]; then + filtered="$(apply_clip_filter "$clip_filter" "$available_clips" 2>/dev/null)" || { + log_error "Filter did not match any clips" >&2 + return 1 + } + printf '%s\n' "$filtered" + return 0 + fi + + # No filter - return all clips + printf '%s\n' "$available_clips" + return 0 +} + +# ---------- Record Configuration Functions (10-config enhancement) ---------- + +# Discover all available record configurations +# Returns: space-separated list of record_config1 through record_config10 +# Exit codes: 0=success (always succeeds - configs are predefined) +discover_record_configs() { + printf '%s\n' "record_config1 record_config2 record_config3 record_config4 record_config5 record_config6 record_config7 record_config8 record_config9 record_config10" + return 0 +} + +# Get recording parameters for a specific config +# Input: config_name (e.g., record_config1, record_8KHz_1ch) +# Output: "rate channels" (e.g., "8000 1") +# Returns: 0=success, 1=invalid config +get_record_config_params() { + config_name="$1" + case "$config_name" in + record_config1|record_8KHz_1ch) printf '%s\n' "8000 1" ;; + record_config2|record_16KHz_1ch) printf '%s\n' "16000 1" ;; + record_config3|record_16KHz_2ch) printf '%s\n' "16000 2" ;; + record_config4|record_24KHz_1ch) printf '%s\n' "24000 1" ;; + record_config5|record_32KHz_2ch) printf '%s\n' "32000 2" ;; + record_config6|record_44.1KHz_2ch) printf '%s\n' "44100 2" ;; + record_config7|record_48KHz_2ch) printf '%s\n' "48000 2" ;; + record_config8|record_48KHz_6ch) printf '%s\n' "48000 6" ;; + record_config9|record_96KHz_2ch) printf '%s\n' "96000 2" ;; + record_config10|record_96KHz_6ch) printf '%s\n' "96000 6" ;; + *) return 1 ;; + esac + return 0 +} + +# Generate descriptive test case name from config name +# Input: record_config1 +# Output: record_8KHz_1ch +# Returns: 0=success, 1=invalid config +generate_record_testcase_name() { + config_name="$1" + case "$config_name" in + record_config1) printf '%s\n' "record_8KHz_1ch" ;; + record_config2) printf '%s\n' "record_16KHz_1ch" ;; + record_config3) printf '%s\n' "record_16KHz_2ch" ;; + record_config4) printf '%s\n' "record_24KHz_1ch" ;; + record_config5) printf '%s\n' "record_32KHz_2ch" ;; + record_config6) printf '%s\n' "record_44.1KHz_2ch" ;; + record_config7) printf '%s\n' "record_48KHz_2ch" ;; + record_config8) printf '%s\n' "record_48KHz_6ch" ;; + record_config9) printf '%s\n' "record_96KHz_2ch" ;; + record_config10) printf '%s\n' "record_96KHz_6ch" ;; + *) printf '%s\n' "$config_name" ;; # Already descriptive or unknown + esac + return 0 +} + +# Generate output filename with parameters +# Input: testcase_base (e.g., "record_short"), rate (e.g., "48000"), channels (e.g., "2") +# Output: record_short_48KHz_2ch.wav +# Returns: 0=success +generate_record_filename() { + testcase_base="$1" + rate="$2" + channels="$3" + + # Convert rate to KHz format + rate_khz="$rate" + case "$rate" in + 8000) rate_khz="8KHz" ;; + 16000) rate_khz="16KHz" ;; + 22050) rate_khz="22.05KHz" ;; + 24000) rate_khz="24KHz" ;; + 32000) rate_khz="32KHz" ;; + 44100) rate_khz="44.1KHz" ;; + 48000) rate_khz="48KHz" ;; + 88200) rate_khz="88.2KHz" ;; + 96000) rate_khz="96KHz" ;; + 176400) rate_khz="176.4KHz" ;; + 192000) rate_khz="192KHz" ;; + 352800) rate_khz="352.8KHz" ;; + 384000) rate_khz="384KHz" ;; + *) rate_khz="${rate}Hz" ;; # Fallback for unknown rates + esac + + printf '%s_%s_%sch.wav\n' "$testcase_base" "$rate_khz" "$channels" + return 0 +} + +# Validate record config name +# Input: requested_name (e.g., record_config1, record_8KHz_1ch) +# Returns: 0=valid, 1=invalid (with helpful error message) +validate_record_config_name() { + requested_name="$1" + + # Check if it's a valid record_config1-10 or descriptive name + case "$requested_name" in + record_config[1-9]|record_config10) + return 0 + ;; + record_*KHz_*ch) + # Check if we can get parameters for this descriptive name + if get_record_config_params "$requested_name" >/dev/null 2>&1; then + return 0 + fi + ;; + esac + + log_error "Invalid record config name: $requested_name" + log_error "Available range: record_config1 to record_config10" + return 1 +} + +# Apply filter to record configs +# Input: filter (space-separated patterns), available_configs (list) +# Output: filtered config list +# Returns: 0=success, 1=no matches +apply_record_config_filter() { + filter="$1" + available_configs="$2" + + # If no filter, return all configs + if [ -z "$filter" ]; then + printf '%s\n' "$available_configs" + return 0 + fi + + # Apply filter + filtered="" + for config in $available_configs; do + # Generate descriptive name for matching + desc_name="$(generate_record_testcase_name "$config" 2>/dev/null)" || continue + + for pattern in $filter; do + # Match against config name or descriptive name + if printf '%s %s' "$config" "$desc_name" | grep -q "$pattern"; then + filtered="$filtered $config" + break + fi + done + done + + # Remove leading space + filtered="$(printf '%s' "$filtered" | sed 's/^ //')" + + # Check if filter matched anything + if [ -z "$filtered" ]; then + log_error "Filter '$filter' matched no record configs" + log_info "Available configs: record_config1 to record_config10" + return 1 + fi + + printf '%s\n' "$filtered" + return 0 +} + +# Discover and filter record configs based on user input +# Input: config_names (explicit list), config_filter (pattern filter) +# Output: final list of config names to test +# Returns: 0=success, 1=no valid configs +discover_and_filter_record_configs() { + config_names="$1" + config_filter="$2" + + # Get all available configs + available_configs="$(discover_record_configs)" + + # If explicit config names provided, validate and use them + if [ -n "$config_names" ]; then + validated="" + failed_names="" + + for name in $config_names; do + if validate_record_config_name "$name"; then + validated="$validated $name" + else + failed_names="$failed_names $name" + fi + done + + validated="$(printf '%s' "$validated" | sed 's/^ //')" + failed_names="$(printf '%s' "$failed_names" | sed 's/^ //')" + + if [ -z "$validated" ]; then + return 1 + fi + + # Warn about any failed names (only if there are some valid ones) + if [ -n "$failed_names" ]; then + log_warn "Invalid record config names skipped: $failed_names" + fi + + printf '%s\n' "$validated" + return 0 + fi + + # Apply filter if provided + if [ -n "$config_filter" ]; then + filtered="$(apply_record_config_filter "$config_filter" "$available_configs")" || return 1 + printf '%s\n' "$filtered" + return 0 + fi + + # No filter - return all configs + printf '%s\n' "$available_configs" + return 0 +}