From 853f3948bf253250cee1acbe48ec86fc45700d65 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 - Added --res-suffix parameter for unique result files in CI/LAVA environments - Updated documentation with CI/LAVA workflow examples AudioRecord enhancements: * Implemented config discovery mode with 10 predefined test configurations * Added --config-name and --config-filter CLI options for flexible test selection * Added --res-suffix option to generate unique result files (e.g., AudioRecord_Config1.res) * Updated documentation with configuration table and usage examples * Added CI/LAVA workflow examples with validated output * Modified YAML to support new config parameters (defaults to record_config1) * Updated YAML with RES_SUFFIX parameter for parallel test execution AudioPlayback enhancements: * Implemented clip discovery mode with 20 predefined test configurations * Added --clip-name and --clip-filter CLI options for flexible test selection * Added --res-suffix option to generate unique result files (e.g., AudioPlayback_Config1.res) * Updated documentation with configuration table and usage examples * Added CI/LAVA workflow examples with validated output * Modified YAML to support new clip parameters (defaults to Config1) * Updated YAML with RES_SUFFIX parameter for parallel test execution 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 * Unique result files prevent overwriting in parallel execution Test coverage now includes: - Sample rates: 8KHz to 384KHz - Bit depths: 8-bit to 32-bit - Channel configs: Mono, Stereo Signed-off-by: Teja Swaroop Moida --- Runner/plans/meta-ar-ci-premerge.yaml | 57 +- .../Audio/AudioPlayback/AudioPlayback.yaml | 9 +- .../Multimedia/Audio/AudioPlayback/Read_me.md | 207 ++++- .../Multimedia/Audio/AudioPlayback/run.sh | 361 ++++++++- .../Audio/AudioRecord/AudioRecord.yaml | 15 +- .../Multimedia/Audio/AudioRecord/Read_me.md | 284 +++++-- .../Multimedia/Audio/AudioRecord/run.sh | 743 +++++++++++++----- Runner/utils/audio_common.sh | 676 +++++++++++++++- 8 files changed, 1982 insertions(+), 370 deletions(-) diff --git a/Runner/plans/meta-ar-ci-premerge.yaml b/Runner/plans/meta-ar-ci-premerge.yaml index 83811ffa..6d2ee93c 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 - - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioRecord/AudioRecord.res || true + + # ========== AudioPlayback Test Cases (7 configs) ========== + + # Playback Test 1: Config1 (16KHz, 16-bit, 2ch) + - $PWD/suites/Multimedia/Audio/AudioPlayback/run.sh --clip-name "Config1" --res-suffix "Config1" --audio-clips-path /home/AudioClips/ --no-extract-assets || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioPlayback/AudioPlayback_Config1.res || true + + # Playback Test 2: Config7 (24KHz, 24-bit, 6ch) + - $PWD/suites/Multimedia/Audio/AudioPlayback/run.sh --clip-name "Config7" --res-suffix "Config7" --audio-clips-path /home/AudioClips/ --no-extract-assets || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioPlayback/AudioPlayback_Config7.res || true + + # Playback Test 3: Config13 (44.1KHz, 16-bit, 1ch) + - $PWD/suites/Multimedia/Audio/AudioPlayback/run.sh --clip-name "Config13" --res-suffix "Config13" --audio-clips-path /home/AudioClips/ --no-extract-assets || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioPlayback/AudioPlayback_Config13.res || true + + # Playback Test 4: Config15 (48KHz, 8-bit, 2ch) + - $PWD/suites/Multimedia/Audio/AudioPlayback/run.sh --clip-name "Config15" --res-suffix "Config15" --audio-clips-path /home/AudioClips/ --no-extract-assets || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioPlayback/AudioPlayback_Config15.res || true + + # Playback Test 5: Config18 (88.2KHz, 24-bit, 2ch) + - $PWD/suites/Multimedia/Audio/AudioPlayback/run.sh --clip-name "Config18" --res-suffix "Config18" --audio-clips-path /home/AudioClips/ --no-extract-assets || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioPlayback/AudioPlayback_Config18.res || true + + # Playback Test 6: Config20 (96KHz, 24-bit, 6ch) + - $PWD/suites/Multimedia/Audio/AudioPlayback/run.sh --clip-name "Config20" --res-suffix "Config20" --audio-clips-path /home/AudioClips/ --no-extract-assets || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioPlayback/AudioPlayback_Config20.res || true + + # Playback Test 7: Config5 (192KHz, 32-bit, 8ch) + - $PWD/suites/Multimedia/Audio/AudioPlayback/run.sh --clip-name "Config5" --res-suffix "Config5" --audio-clips-path /home/AudioClips/ --no-extract-assets || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioPlayback/AudioPlayback_Config5.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" --res-suffix "Config1" --record-seconds 10s || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioRecord/AudioRecord_Config1.res || true + + # Record Test 2: record_config7 (48KHz, 2ch) + - $PWD/suites/Multimedia/Audio/AudioRecord/run.sh --config-name "record_config7" --res-suffix "Config7" --record-seconds 10s || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioRecord/AudioRecord_Config7.res || true + + # Record Test 3: record_config10 (96KHz, 6ch) + - $PWD/suites/Multimedia/Audio/AudioRecord/run.sh --config-name "record_config10" --res-suffix "Config10" --record-seconds 10s || true + - $PWD/utils/send-to-lava.sh $PWD/suites/Multimedia/Audio/AudioRecord/AudioRecord_Config10.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..ec376bbd 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 @@ -24,10 +26,11 @@ params: PASSWORD: "" # Wi-Fi password for network connection, default: unset NET_PROBE_ROUTE_IP: "1.1.1.1" # IP used for route probing, default: 1.1.1.1 NET_PING_HOST: "8.8.8.8" # Host used for ping reachability check, default: 8.8.8.8 + RES_SUFFIX: "" # Suffix for unique result file (e.g., "Config1" generates AudioPlayback_Config1.res), default: unset 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}" --res-suffix "${RES_SUFFIX}" --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..d92206c5 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 @@ -127,34 +192,41 @@ cd Runner/suites/Multimedia/Audio/AudioPlayback # Provide JUnit output and disable dmesg scan ./run.sh --junit results.xml --no-dmesg +# CI/LAVA workflow: Generate unique result files for each test +./run.sh --clip-name "Config1" --res-suffix "Config1" --audio-clips-path /home/AudioClips/ --no-extract-assets +./run.sh --clip-name "Config7" --res-suffix "Config7" --audio-clips-path /home/AudioClips/ --no-extract-assets +# This generates AudioPlayback_Config1.res and AudioPlayback_Config7.res (no overwriting) + 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) @@ -163,6 +235,7 @@ Option Description --no-extract-assets Disable asset extraction entirely (skips all asset operations) --enable-network-download Enable network operations to download missing audio files (default: disabled) --audio-clips-path Custom location for audio clips (for CI with pre-staged clips) +--res-suffix Suffix for unique result file (e.g., "Config1" generates AudioPlayback_Config1.res instead of AudioPlayback.res) --junit Write JUnit XML output --verbose Enable verbose logging --help Show usage instructions @@ -170,22 +243,86 @@ 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 +``` + +**Example 5: CI/LAVA workflow with unique result files** +``` +sh-5.3# ./run.sh --clip-name "Config1" --res-suffix "Config1" --audio-clips-path /home/AudioClips/ --no-extract-assets +[INFO] 2026-01-12 06:56:47 - Using unique result file: ./AudioPlayback_Config1.res +[INFO] 2026-01-12 06:56:47 - ---------------- Starting AudioPlayback ---------------- +[INFO] 2026-01-12 06:56:48 - Using clip discovery mode +[INFO] 2026-01-12 06:56:48 - Discovered 1 clips to test +[INFO] 2026-01-12 06:56:48 - [play_16KHz_16b_2ch] Clip duration: 30s (timeout threshold: 29s) +[PASS] 2026-01-12 06:57:18 - [play_16KHz_16b_2ch] loop 1 OK (rc=0, 30s) +[PASS] 2026-01-12 06:57:18 - AudioPlayback PASS + +sh-5.3# cat AudioPlayback_Config1.res +AudioPlayback PASS + +sh-5.3# ./run.sh --clip-name "Config7" --res-suffix "Config7" --audio-clips-path /home/AudioClips/ --no-extract-assets +[INFO] 2026-01-12 06:57:42 - Using unique result file: ./AudioPlayback_Config7.res +[PASS] 2026-01-12 06:58:13 - AudioPlayback PASS + +sh-5.3# cat AudioPlayback_Config7.res +AudioPlayback PASS + +# Both result files exist without overwriting +sh-5.3# ls -1 AudioPlayback*.res +AudioPlayback_Config1.res +AudioPlayback_Config7.res ``` Results: @@ -211,5 +348,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..523ed1e2 100755 --- a/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh +++ b/Runner/suites/Multimedia/Audio/AudioPlayback/run.sh @@ -41,15 +41,19 @@ fi # shellcheck disable=SC1091 . "$TOOLS/lib_video.sh" +TESTNAME="AudioPlayback" +RES_SUFFIX="" # Optional suffix for unique result files (e.g., "Config1") +# RES_FILE will be set after parsing command-line arguments + if ! setup_overlay_audio_environment; then log_fail "Overlay audio environment setup failed" - echo "$TESTNAME FAIL" > "$RES_FILE" + # Use absolute path for result file (early failure before RES_FILE is set) + echo "$TESTNAME FAIL" > "$SCRIPT_DIR/${TESTNAME}.res" exit 1 fi -TESTNAME="AudioPlayback" -RES_FILE="./${TESTNAME}.res" -LOGDIR="results/${TESTNAME}" +# Use absolute paths for LOGDIR to work from any directory +LOGDIR="$SCRIPT_DIR/results/${TESTNAME}" mkdir -p "$LOGDIR" # ---- Assets ---- @@ -59,8 +63,8 @@ export AUDIO_TAR_URL # ------------- Defaults / CLI ------------- AUDIO_BACKEND="" SINK_CHOICE="${SINK_CHOICE:-speakers}" # speakers|null -FORMATS="${FORMATS:-wav}" -DURATIONS="${DURATIONS:-short}" # short|medium|long +FORMATS="" # Will be set to default only if using legacy mode +DURATIONS="" # Will be set to default only if using legacy mode LOOPS="${LOOPS:-1}" TIMEOUT="${TIMEOUT:-0}" # 0 = no timeout (recommended) STRICT="${STRICT:-0}" @@ -70,6 +74,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 +95,13 @@ usage() { Usage: $0 [options] --backend {pipewire|pulseaudio} --sink {speakers|null} - --formats "wav" - --durations "short|short medium|short medium long" + --formats "wav" # Legacy matrix mode only + --durations "short|short medium" # Legacy matrix mode only (not recommended for new tests) + --clip-name "play_48KHz_16b_2ch" # Test specific clip(s) by name (space-separated) + # Also supports Config1, Config2, ..., Config20 + --clip-filter "48KHz" # Filter clips by pattern + --res-suffix SUFFIX # Suffix for unique result file (e.g., "Config1") + # Generates AudioPlayback_SUFFIX.res instead of AudioPlayback.res --loops N --timeout SECS # set 0 to disable watchdog --enable-network-download @@ -99,6 +113,22 @@ Usage: $0 [options] --password PASS --verbose --help + +Testing Modes: + Clip Discovery Mode (Recommended): + - Auto-discovers clips from AudioClips directory + - Use --clip-name or --clip-filter to select specific clips + - Provides descriptive test case names based on audio format + - Examples: + $0 --clip-name "Config1 Config7" + $0 --clip-filter "48KHz" + $0 --clip-name "Config1" --res-suffix "Config1" # CI/LAVA use + + Legacy Matrix Mode: + - Uses --formats and --durations to generate test matrix + - Maintained for backward compatibility + - Example: + $0 --formats "wav" --durations "short medium" EOF } @@ -114,10 +144,26 @@ 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 + ;; + --res-suffix) + RES_SUFFIX="$2" shift 2 ;; --loops) @@ -179,6 +225,80 @@ if [ -n "$SSID" ] && [ -n "$PASSWORD" ]; then ENABLE_NETWORK_DOWNLOAD=true fi +# Generate result file path with optional suffix (after parsing CLI args) +# Use absolute path anchored to SCRIPT_DIR for consistency +if [ -n "$RES_SUFFIX" ]; then + RES_FILE="$SCRIPT_DIR/${TESTNAME}_${RES_SUFFIX}.res" + log_info "Using unique result file: $RES_FILE" +else + RES_FILE="$SCRIPT_DIR/${TESTNAME}.res" +fi + +# ------------- Mode Detection and Validation ------------- + +# Check for conflicting parameters (discovery vs legacy mode) +if { [ -n "$CLIP_NAMES" ] || [ -n "$CLIP_FILTER" ]; } && { [ -n "$FORMATS" ] || [ -n "$DURATIONS" ]; }; then + log_error "Cannot mix clip discovery parameters (--clip-name, --clip-filter) with legacy matrix parameters (--formats, --durations)" + log_error "Please use either clip discovery mode OR legacy matrix mode, not both" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 +fi + +# Set defaults for legacy mode parameters only if using legacy mode +if [ "$USE_CLIP_DISCOVERY" = "false" ]; then + FORMATS="${FORMATS:-wav}" + DURATIONS="${DURATIONS:-short}" +fi + +# 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 with .wav files + clips_dir="${AUDIO_CLIPS_BASE_DIR:-AudioClips}" + if [ -d "$clips_dir" ]; then + # Check for .wav files using shell glob pattern + wav_found=false + for wav_file in "$clips_dir"/*.wav; do + if [ -f "$wav_file" ]; then + # Found at least one .wav file + wav_found=true + break + fi + done + + if [ "$wav_found" = "true" ]; 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 + else + USE_CLIP_DISCOVERY=false + log_info "Auto-detected legacy matrix mode (no clips directory found)" + 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 @@ -341,7 +461,7 @@ fi min_ok=0 if [ "$dur_s" -gt 0 ] 2>/dev/null; then - min_ok=$((dur_s - 1)) + min_ok=$(expr $dur_s - 1) if [ "$min_ok" -lt 1 ]; then min_ok=1 fi @@ -350,18 +470,199 @@ 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 (logs go to stderr automatically) + CLIPS_TO_TEST="$(discover_and_filter_clips "$CLIP_NAMES" "$CLIP_FILTER")" || { + # Error messages already printed to stderr, just skip + log_skip "$TESTNAME SKIP - Invalid clip/config name(s) provided" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + } + else + # Discover all clips (logs go to stderr automatically) + CLIPS_TO_TEST="$(discover_audio_clips)" || { + # Error messages already printed to stderr, just skip + log_skip "$TESTNAME SKIP - No audio clips found in $clips_dir" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + } + fi + + # Count clips + clip_count=0 + for clip_file in $CLIPS_TO_TEST; do + clip_count=$(expr $clip_count + 1) + 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=$(expr $skip + 1) + continue + fi + + # Extract clip duration for accurate timeout handling + clip_duration="$(extract_clip_duration "$clip_file" 2>/dev/null || echo 0)" + if [ "$clip_duration" -gt 0 ] 2>/dev/null; then + # Use clip duration for timeout calculations + clip_dur_s="$clip_duration" + clip_min_ok=$(expr $clip_duration - 1) + if [ "$clip_min_ok" -lt 1 ]; then + clip_min_ok=1 + fi + log_info "[$case_name] Clip duration: ${clip_duration}s (timeout threshold: ${clip_min_ok}s)" + else + # Fallback to global timeout values if duration cannot be parsed + clip_dur_s="$dur_s" + clip_min_ok="$min_ok" + fi + + total=$(expr $total + 1) + logf="$LOGDIR/${case_name}.log" + : > "$logf" + export AUDIO_LOGCTX="$logf" + + CLIP_BYTES="$(file_size_bytes "$clip_path" 2>/dev/null || echo 0)" + log_info "[$case_name] Using clip: $clip_file (${CLIP_BYTES} bytes)" + + i=1 + ok_runs=0 + last_elapsed=0 + + while [ "$i" -le "$LOOPS" ]; do + iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + + if [ "$AUDIO_BACKEND" = "pipewire" ]; then + loop_hdr="sink=$SINK_CHOICE($SINK_ID)" + else + loop_hdr="sink=$SINK_CHOICE($SINK_NAME)" + fi + + log_info "[$case_name] loop $i/$LOOPS start=$iso clip=$clip_file backend=$AUDIO_BACKEND $loop_hdr" + + 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=$(expr $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 (use clip-specific timeout thresholds) + if [ "$rc" -eq 0 ]; then + log_pass "[$case_name] loop $i OK (rc=0, ${last_elapsed}s)" + ok_runs=$(expr $ok_runs + 1) + elif [ "$rc" -eq 124 ] && [ "$clip_dur_s" -gt 0 ] 2>/dev/null && [ "$last_elapsed" -ge "$clip_min_ok" ]; then + log_warn "[$case_name] TIMEOUT ($TIMEOUT) - PASS (ran ~${last_elapsed}s, expected ${clip_duration}s)" + ok_runs=$(expr $ok_runs + 1) + elif [ "$rc" -ne 0 ] && { [ "$pw_ev" -eq 1 ] || [ "$pa_ev" -eq 1 ] || [ "$alsa_ev" -eq 1 ] || [ "$asoc_ev" -eq 1 ]; }; then + log_warn "[$case_name] nonzero rc=$rc but evidence indicates playback - PASS" + ok_runs=$(expr $ok_runs + 1) + else + log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s) - see $logf" + fi + + i=$(expr $i + 1) + done + + # Aggregate result for this clip + if [ "$ok_runs" -ge 1 ]; then + pass=$(expr $pass + 1) + echo "$case_name PASS" >> "$LOGDIR/summary.txt" + else + fail=$(expr $fail + 1) + echo "$case_name FAIL" >> "$LOGDIR/summary.txt" + suite_rc=1 + fi + done + + # Collect evidence once at end (not per clip) + if [ "$DMESG_SCAN" -eq 1 ]; then + scan_audio_dmesg "$LOGDIR" + dump_mixers "$LOGDIR/mixer_dump.txt" + fi + +else + # ========== LEGACY: Matrix Mode ========== + + for fmt in $FORMATS; do + for dur in $DURATIONS; do clip="$(resolve_clip "$fmt" "$dur")" case_name="play_${fmt}_${dur}" - total=$((total + 1)) + total=$(expr $total + 1) logf="$LOGDIR/${case_name}.log" : > "$logf" export AUDIO_LOGCTX="$logf" @@ -369,14 +670,14 @@ for fmt in $FORMATS; do if [ -z "$clip" ]; then log_warn "[$case_name] No clip mapping for format=$fmt duration=$dur" echo "$case_name SKIP (no clip mapping)" >> "$LOGDIR/summary.txt" - skip=$((skip + 1)) + skip=$(expr $skip + 1) continue fi # Check if clip is available (should have been downloaded at top level if needed) if [ "${EXTRACT_AUDIO_ASSETS}" = "true" ]; then if [ -s "$clip" ]; then - CLIP_BYTES="$(wc -c < "$clip" 2>/dev/null || echo 0)" + CLIP_BYTES="$(file_size_bytes "$clip" 2>/dev/null || echo 0)" log_info "[$case_name] Using clip: $clip (${CLIP_BYTES} bytes)" else # Clip missing or empty - this shouldn't happen if top-level download succeeded @@ -387,7 +688,7 @@ for fmt in $FORMATS; do log_info "[$case_name] Hint: Run with --enable-network-download to download clips" fi echo "$case_name SKIP (clip unavailable)" >> "$LOGDIR/summary.txt" - skip=$((skip + 1)) + skip=$(expr $skip + 1) continue fi fi @@ -420,7 +721,7 @@ for fmt in $FORMATS; do fi end_s="$(date +%s 2>/dev/null || echo 0)" - last_elapsed=$((end_s - start_s)) + last_elapsed=$(expr $end_s - $start_s) if [ "$last_elapsed" -lt 0 ]; then last_elapsed=0 fi @@ -461,35 +762,37 @@ for fmt in $FORMATS; do if [ "$rc" -eq 0 ]; then log_pass "[$case_name] loop $i OK (rc=0, ${last_elapsed}s)" - ok_runs=$((ok_runs + 1)) + ok_runs=$(expr $ok_runs + 1) elif [ "$rc" -eq 124 ] && [ "$dur_s" -gt 0 ] 2>/dev/null && [ "$last_elapsed" -ge "$min_ok" ]; then log_warn "[$case_name] TIMEOUT ($TIMEOUT) - PASS (ran ~${last_elapsed}s)" - ok_runs=$((ok_runs + 1)) + ok_runs=$(expr $ok_runs + 1) elif [ "$rc" -ne 0 ] && { [ "$pw_ev" -eq 1 ] || [ "$pa_ev" -eq 1 ] || [ "$alsa_ev" -eq 1 ] || [ "$asoc_ev" -eq 1 ]; }; then log_warn "[$case_name] nonzero rc=$rc but evidence indicates playback - PASS" - ok_runs=$((ok_runs + 1)) + ok_runs=$(expr $ok_runs + 1) else log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s) - see $logf" fi - i=$((i + 1)) + i=$(expr $i + 1) done - if [ "$DMESG_SCAN" -eq 1 ]; then - scan_audio_dmesg "$LOGDIR" - dump_mixers "$LOGDIR/mixer_dump.txt" - fi - if [ "$ok_runs" -ge 1 ]; then - pass=$((pass + 1)) + pass=$(expr $pass + 1) echo "$case_name PASS" >> "$LOGDIR/summary.txt" else - fail=$((fail + 1)) + fail=$(expr $fail + 1) echo "$case_name FAIL" >> "$LOGDIR/summary.txt" suite_rc=1 fi + done done -done + + # Collect evidence once at end (not per test case) + if [ "$DMESG_SCAN" -eq 1 ]; then + scan_audio_dmesg "$LOGDIR" + dump_mixers "$LOGDIR/mixer_dump.txt" + fi +fi 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..f2a9ff20 100644 --- a/Runner/suites/Multimedia/Audio/AudioRecord/AudioRecord.yaml +++ b/Runner/suites/Multimedia/Audio/AudioRecord/AudioRecord.yaml @@ -8,19 +8,22 @@ 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 + RES_SUFFIX: "" # Suffix for unique result file (e.g., "Config1" generates AudioRecord_Config1.res), default: unset 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}" --res-suffix "${RES_SUFFIX}" || 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..f06014dc 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,186 @@ 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 + +# CI/LAVA workflow: Generate unique result files for each test +./run.sh --config-name "record_config1" --res-suffix "Config1" --record-seconds 10s +./run.sh --config-name "record_config7" --res-suffix "Config7" --record-seconds 10s +./run.sh --config-name "record_config10" --res-suffix "Config10" --record-seconds 10s +# This generates AudioRecord_Config1.res, AudioRecord_Config7.res, and AudioRecord_Config10.res (no overwriting) + 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(legacy matrix mode) 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 (legacy matrix mode only) +--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 +--res-suffix Suffix for unique result file (e.g., "Config1" generates AudioRecord_Config1.res instead of AudioRecord.res) +--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: CI/LAVA workflow with unique result files** +``` +sh-5.3# ./run.sh --config-name "record_config1" --res-suffix "Config1" --record-seconds 10s +[INFO] 2026-01-12 07:14:09 - Using unique result file: ./AudioRecord_Config1.res +[INFO] 2026-01-12 07:14:09 - ---------------- Starting AudioRecord ---------------- +[INFO] 2026-01-12 07:14:09 - Using config discovery mode +[INFO] 2026-01-12 07:14:09 - Discovered 1 configs to test +[INFO] 2026-01-12 07:14:09 - [record_8KHz_1ch] Using config: record_config1 (rate=8000Hz channels=1) +[PASS] 2026-01-12 07:14:19 - [record_8KHz_1ch] loop 1 OK (rc=0, 10s, bytes=162462) +[PASS] 2026-01-12 07:14:19 - AudioRecord PASS + +sh-5.3# cat AudioRecord_Config1.res +AudioRecord PASS + +sh-5.3# ./run.sh --config-name "record_config7" --res-suffix "Config7" --record-seconds 10s +[INFO] 2026-01-12 07:16:01 - Using unique result file: ./AudioRecord_Config7.res +[PASS] 2026-01-12 07:16:11 - AudioRecord PASS + +sh-5.3# cat AudioRecord_Config7.res +AudioRecord PASS + +# Both result files exist without overwriting +sh-5.3# ls -1 AudioRecord*.res +AudioRecord_Config1.res +AudioRecord_Config7.res +``` + +**Example 7: 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 +335,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 for legacy matrix mode only; use `--config-name` or `--config-filter` for config discovery mode (recommended). ## 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..19fd3165 100755 --- a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh +++ b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh @@ -30,21 +30,25 @@ fi # shellcheck disable=SC1091 . "$TOOLS/audio_common.sh" +TESTNAME="AudioRecord" +RES_SUFFIX="" # Optional suffix for unique result files (e.g., "Config1") +# RES_FILE will be set after parsing command-line arguments + if ! setup_overlay_audio_environment; then log_fail "Overlay audio environment setup failed" - echo "$TESTNAME FAIL" > "$RES_FILE" + # Use absolute path for result file (early failure before RES_FILE is set) + echo "$TESTNAME FAIL" > "$SCRIPT_DIR/${TESTNAME}.res" exit 1 fi -TESTNAME="AudioRecord" -RES_FILE="./${TESTNAME}.res" -LOGDIR="results/${TESTNAME}" +# Use absolute paths for LOGDIR to work from any directory +LOGDIR="$SCRIPT_DIR/results/${TESTNAME}" mkdir -p "$LOGDIR" # ---------------- Defaults / CLI ---------------- AUDIO_BACKEND="" SRC_CHOICE="${SRC_CHOICE:-mic}" # mic|null -DURATIONS="${DURATIONS:-short}" # label set OR numeric tokens when RECORD_SECONDS=auto +DURATIONS="" # Will be set to default only if using legacy mode RECORD_SECONDS="${RECORD_SECONDS:-30s}" # DEFAULT: 30s; 'auto' maps short/med/long LOOPS="${LOOPS:-1}" TIMEOUT="${TIMEOUT:-0}" # 0 = no watchdog @@ -53,11 +57,21 @@ 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 </dev/null || echo "$SCRIPT_DIR")" if ! cd "$test_path"; then @@ -148,6 +199,36 @@ else log_info "Platform Details: unknown" fi +# ------------- Mode Detection and Validation ------------- + +# Check for conflicting parameters (discovery vs legacy mode) +if { [ -n "$CONFIG_NAMES" ] || [ -n "$CONFIG_FILTER" ]; } && [ -n "$DURATIONS" ]; then + log_error "Cannot mix config discovery parameters (--config-name, --config-filter) with legacy matrix parameters (--durations)" + log_error "Please use either config discovery mode OR legacy matrix mode, not both" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 +fi + +# Set defaults for legacy mode parameters only if using legacy mode +if [ "$USE_CONFIG_DISCOVERY" = "false" ]; then + DURATIONS="${DURATIONS:-short}" # label set OR numeric tokens when RECORD_SECONDS=auto +fi + +# Determine whether to use config discovery or legacy matrix mode +if [ "$USE_CONFIG_DISCOVERY" = "auto" ]; then + # Auto mode: use config discovery by default (no external dependencies needed) + USE_CONFIG_DISCOVERY=true + log_info "Auto-detected config discovery mode (testing all 10 record configs)" +fi + + +# Validate CLI option conflicts +if [ -n "$CONFIG_NAMES" ] && [ -n "$CONFIG_FILTER" ]; then + log_warn "Both --config-name and --config-filter specified" + log_info "Using --config-name (ignoring --config-filter)" + CONFIG_FILTER="" +fi + log_info "Args: backend=${AUDIO_BACKEND:-auto} source=$SRC_CHOICE loops=$LOOPS durations='$DURATIONS' record_seconds=$RECORD_SECONDS timeout=$TIMEOUT strict=$STRICT dmesg=$DMESG_SCAN" # Resolve backend @@ -160,14 +241,14 @@ log_info "Backend fallback chain: $BACKENDS_TO_TRY" if [ -z "$AUDIO_BACKEND" ]; then log_skip "$TESTNAME SKIP - no audio backend running" echo "$TESTNAME SKIP" > "$RES_FILE" - exit 2 + exit 0 fi log_info "Using backend: $AUDIO_BACKEND" if ! check_audio_daemon "$AUDIO_BACKEND"; then log_skip "$TESTNAME SKIP - backend not available: $AUDIO_BACKEND" echo "$TESTNAME SKIP" > "$RES_FILE" - exit 2 + exit 0 fi # Dependencies per backend @@ -175,13 +256,13 @@ if [ "$AUDIO_BACKEND" = "pipewire" ]; then if ! check_dependencies wpctl pw-record; then log_skip "$TESTNAME SKIP - missing PipeWire utils" echo "$TESTNAME SKIP" > "$RES_FILE" - exit 2 + exit 0 fi else if ! check_dependencies pactl parecord; then log_skip "$TESTNAME SKIP - missing PulseAudio utils" echo "$TESTNAME SKIP" > "$RES_FILE" - exit 2 + exit 0 fi fi @@ -240,7 +321,7 @@ fi if [ -z "$SRC_ID" ] && [ "$AUDIO_BACKEND" != "pipewire" ]; then log_skip "$TESTNAME SKIP - requested source '$SRC_CHOICE' not available on any backend ($BACKENDS_TO_TRY)" echo "$TESTNAME SKIP" > "$RES_FILE" - exit 2 + exit 0 fi # ---- Normalize ALSA device id (fix "hw:0 1," → "hw:0,1") ---- @@ -274,7 +355,7 @@ if [ "$AUDIO_BACKEND" = "alsa" ]; then else log_skip "$TESTNAME SKIP - no valid ALSA capture device found" echo "$TESTNAME SKIP" > "$RES_FILE" - exit 2 + exit 0 fi ;; esac @@ -305,17 +386,17 @@ case "$AUDIO_BACKEND" in pipewire) if ! check_dependencies wpctl pw-record; then log_skip "$TESTNAME SKIP - missing PipeWire utils" - echo "$TESTNAME SKIP" > "$RES_FILE"; exit 2 + echo "$TESTNAME SKIP" > "$RES_FILE"; exit 0 fi ;; pulseaudio) if ! check_dependencies pactl parecord; then log_skip "$TESTNAME SKIP - missing PulseAudio utils" - echo "$TESTNAME SKIP" > "$RES_FILE"; exit 2 + echo "$TESTNAME SKIP" > "$RES_FILE"; exit 0 fi ;; alsa) if ! check_dependencies arecord; then log_skip "$TESTNAME SKIP - missing arecord" - echo "$TESTNAME SKIP" > "$RES_FILE"; exit 2 + echo "$TESTNAME SKIP" > "$RES_FILE"; exit 0 fi ;; esac @@ -390,246 +471,522 @@ alsa_pick_virtual_pcm() { return 1 } -# ---------------- Matrix execution ---------------- +# ------------- Test Execution (Matrix or Config Discovery) ------------- total=0 pass=0 fail=0 skip=0 suite_rc=0 -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 +if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then + # ========== NEW: Config Discovery Mode ========== + log_info "Using config discovery mode" + + # Discover and filter configs + if [ -n "$CONFIG_NAMES" ] || [ -n "$CONFIG_FILTER" ]; then + # Use discover_and_filter_record_configs helper (logs go to stderr automatically) + CONFIGS_TO_TEST="$(discover_and_filter_record_configs "$CONFIG_NAMES" "$CONFIG_FILTER")" || { + # Error messages already printed to stderr, just skip + log_skip "$TESTNAME SKIP - Invalid config name(s) provided" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + } + else + # Discover all configs (logs go to stderr automatically) + CONFIGS_TO_TEST="$(discover_record_configs)" || { + # Error messages already printed to stderr, just skip + log_skip "$TESTNAME SKIP - No record configs found" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + } fi - - 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" + + if [ -z "$CONFIGS_TO_TEST" ]; then + log_skip "$TESTNAME SKIP - No valid record configs found" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + fi + + # Count configs + config_count=0 + for config in $CONFIGS_TO_TEST; do + config_count=$(expr $config_count + 1) + 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=$(expr $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="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + + # If we already got real audio, accept and skip fallbacks + if [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + if [ "$rc" -ne 0 ]; then + 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="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + fi + fi + + # As a last resort, retry pw-record with --target (only if we have a source id) + if { [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; } && [ -n "$SRC_ID" ]; then + : > "$out" + 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="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + fi + fi + + # (Optional safety) If nonzero rc but output is clearly valid, accept. + if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + log_warn "[$case_name] nonzero rc==$rc but recording looks valid (bytes=$bytes) - PASS" 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="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + + if [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then + if printf '%s\n' "$SRC_ID" | grep -q '^hw:'; then + alt_dev="plughw:${SRC_ID#hw:}" + else + alt_dev="$SRC_ID" + fi + + # Try with the specific config parameters : > "$out" - log_info "[$case_name] 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")" + bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + + # If still failing, try fallback combinations + if [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then + for combo in "S16_LE 48000 2" "S16_LE 44100 2" "S16_LE 16000 1"; do + fmt=$(printf '%s\n' "$combo" | awk '{print $1}') + fallback_rate=$(printf '%s\n' "$combo" | awk '{print $2}') + fallback_ch=$(printf '%s\n' "$combo" | awk '{print $3}') + [ -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="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + 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")" + bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then + log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" + rc=0 + fi + fi + fi + + end_s="$(date +%s 2>/dev/null || echo 0)" + last_elapsed=$(expr $end_s - $start_s) + [ "$last_elapsed" -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=$(expr $ok_runs + 1) + else + log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s, bytes=${bytes:-0}) - see $logf" + fi + + i=$(expr $i + 1) + done + + # Aggregate result for this config + status="FAIL" + if [ "$ok_runs" -ge 1 ]; then + status="PASS" + fi + + append_junit "$case_name" "$last_elapsed" "$status" "$logf" + + case "$status" in + PASS) + pass=$(expr $pass + 1) + echo "$case_name PASS" >> "$LOGDIR/summary.txt" + ;; + SKIP) + skip=$(expr $skip + 1) + echo "$case_name SKIP" >> "$LOGDIR/summary.txt" + ;; + FAIL) + fail=$(expr $fail + 1) + echo "$case_name FAIL" >> "$LOGDIR/summary.txt" + suite_rc=1 + ;; + esac + done +else + # ========== LEGACY: Matrix Mode ========== + for dur in $DURATIONS; do + case_name="record_${dur}" + total=$(expr $total + 1) + + 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")" + bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" - if [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then - if printf '%s\n' "$SRC_ID" | grep -q '^hw:'; then - alt_dev="plughw:${SRC_ID#hw:}" - else - alt_dev="$SRC_ID" + # 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 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="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + fi 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 + + # 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 + bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + fi fi + # (Optional safety) If nonzero rc but output is clearly valid, accept. if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then - log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" + 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="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + + if [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then + if printf '%s\n' "$SRC_ID" | grep -q '^hw:'; then + alt_dev="plughw:${SRC_ID#hw:}" + else + alt_dev="$SRC_ID" + fi + 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="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + 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="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + 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=$(expr $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 + 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 [ "$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 + 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=$(expr $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=$(expr $i + 1) + done - status="FAIL" - if [ "$ok_runs" -ge 1 ]; then - status="PASS" - fi + # Aggregate result for this duration + status="FAIL" + if [ "$ok_runs" -ge 1 ]; then + status="PASS" + fi - append_junit "$case_name" "$last_elapsed" "$status" "$logf" + 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 + case "$status" in + PASS) + pass=$(expr $pass + 1) + echo "$case_name PASS" >> "$LOGDIR/summary.txt" + ;; + SKIP) + skip=$(expr $skip + 1) + echo "$case_name SKIP" >> "$LOGDIR/summary.txt" + ;; + FAIL) + fail=$(expr $fail + 1) + echo "$case_name FAIL" >> "$LOGDIR/summary.txt" + suite_rc=1 + ;; + esac + done +fi -log_info "Summary: total=$total pass=$pass fail=$fail skip=$skip" +# Collect evidence once at end +if [ "$DMESG_SCAN" -eq 1 ]; then + scan_audio_dmesg "$LOGDIR" + dump_mixers "$LOGDIR/mixer_dump.txt" +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..67d417ba 100755 --- a/Runner/utils/audio_common.sh +++ b/Runner/utils/audio_common.sh @@ -29,12 +29,39 @@ check_audio_daemon() { } # ---------- Assets / clips ---------- +# Resolve clip path for legacy matrix mode (formats × durations) +# Returns: clip path on stdout, 0=success, 1=no clip found +# Fallback: If hardcoded clip missing, uses first available .wav file resolve_clip() { fmt="$1"; dur="$2" base="${AUDIO_CLIPS_BASE_DIR:-AudioClips}" + case "$fmt:$dur" in - wav:short|wav:medium|wav:long) printf '%s\n' "$base/yesterday_48KHz.wav" ;; - *) printf '%s\n' "" ;; + wav:short|wav:medium|wav:long) + # Try hardcoded clip first (backward compatibility) + clip="$base/yesterday_48KHz.wav" + if [ -f "$clip" ]; then + printf '%s\n' "$clip" + return 0 + fi + + # Fallback: discover first available clip + first_clip="$(find "$base" -maxdepth 1 -name "*.wav" -type f 2>/dev/null | head -n1)" + if [ -n "$first_clip" ] && [ -f "$first_clip" ]; then + log_info "Using legacy matrix mode. Using fallback: $(basename "$first_clip")" >&2 + printf '%s\n' "$first_clip" + return 0 + fi + + # No clips available + log_error "No audio clips found in $base" >&2 + printf '%s\n' "" + return 1 + ;; + *) + printf '%s\n' "" + return 1 + ;; esac } @@ -152,7 +179,7 @@ audio_timeout_run() { wait "$pid" 2>/dev/null return 143 fi - sleep 1; t=$((t+1)) + sleep 1; t=$(expr $t + 1) done wait "$pid"; return $? } @@ -218,10 +245,10 @@ setup_overlay_audio_environment() { fi sleep $poll_interval - elapsed=$((elapsed + poll_interval)) + elapsed=$(expr $elapsed + $poll_interval) - if [ $((elapsed % 10)) -eq 0 ]; then - log_info "Still waiting for pipewire... (${elapsed}s/${max_wait}s)" + if [ "$(expr $elapsed % 10)" -eq 0 ]; then + log_info "Still waiting for pipewire... (${elapsed}s/${max_wait}s)" fi done @@ -359,22 +386,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)" @@ -428,7 +439,7 @@ audio_evidence_pa_streaming() { cookie="" [ -r "$d/pulse/cookie" ] && cookie="$d/pulse/cookie" # try to derive a home cookie for that uid as well - uid="$(stat -c %u "$d" 2>/dev/null || echo)" + uid="$(stat -c %u "$d" 2>/dev/null || stat -f %u "$d" 2>/dev/null || echo)" if [ -n "$uid" ]; then home="$(getent passwd "$uid" 2>/dev/null | awk -F: '{print $6}')" [ -n "$home" ] && [ -r "$home/.config/pulse/cookie" ] && cookie="$home/.config/pulse/cookie" @@ -598,7 +609,9 @@ audio_parse_secs() { 3) h=$1; m=$2; s=$3 ;; *) return 1 ;; esac - printf '%s\n' $(( ${h:-0}*3600 + ${m:-0}*60 + ${s:-0} )) + h_val=${h:-0}; m_val=${m:-0}; s_val=${s:-0} + result=$(expr $h_val \* 3600 + $m_val \* 60 + $s_val) + printf '%s\n' "$result" return 0 ;; *[!0-9]*) @@ -606,9 +619,9 @@ audio_parse_secs() { [0-9]*s|[0-9]*sec|[0-9]*secs|[0-9]*second|[0-9]*seconds) n=$(printf '%s' "$norm" | sed -n 's/^\([0-9][0-9]*\).*/\1/p'); printf '%s\n' "$n"; return 0 ;; [0-9]*m|[0-9]*min|[0-9]*mins|[0-9]*minute|[0-9]*minutes) - n=$(printf '%s' "$norm" | sed -n 's/^\([0-9][0-9]*\).*/\1/p'); printf '%s\n' $((n*60)); return 0 ;; + n=$(printf '%s' "$norm" | sed -n 's/^\([0-9][0-9]*\).*/\1/p'); printf '%s\n' "$(expr $n \* 60)"; return 0 ;; [0-9]*h|[0-9]*hr|[0-9]*hrs|[0-9]*hour|[0-9]*hours) - n=$(printf '%s' "$norm" | sed -n 's/^\([0-9][0-9]*\).*/\1/p'); printf '%s\n' $((n*3600)); return 0 ;; + n=$(printf '%s' "$norm" | sed -n 's/^\([0-9][0-9]*\).*/\1/p'); printf '%s\n' "$(expr $n \* 3600)"; return 0 ;; *) tokens=$(printf '%s' "$norm" | sed 's/\([0-9][0-9]*[a-z][a-z]*\)/\1 /g') total=0; ok=0 @@ -617,11 +630,11 @@ audio_parse_secs() { u=$(printf '%s' "$t" | sed -n 's/^[0-9][0-9]*\([a-z][a-z]*\)$/\1/p') case "$u" in s|sec|secs|second|seconds) add=$n ;; - m|min|mins|minute|minutes) add=$((n*60)) ;; - h|hr|hrs|hour|hours) add=$((n*3600)) ;; + m|min|mins|minute|minutes) add=$(expr $n \* 60) ;; + h|hr|hrs|hour|hours) add=$(expr $n \* 3600) ;; *) return 1 ;; esac - total=$((total+add)); ok=1 + total=$(expr $total + $add); ok=1 done [ "$ok" -eq 1 ] 2>/dev/null || return 1 printf '%s\n' "$total" @@ -676,6 +689,40 @@ audio_exec_with_timeout() { "$@" } +# -------------------------------------------------------------------- +# File size helper (portable across different stat implementations) +# -------------------------------------------------------------------- + +# Get file size in bytes using portable method +# Input: file path +# Output: file size in bytes to stdout +# Returns: 0=success, 1=file not found or not readable +file_size_bytes() { + file="$1" + [ -f "$file" ] || return 1 + [ -r "$file" ] || return 1 + wc -c < "$file" 2>/dev/null +} + +# Extract clip duration from filename +# Input: clip filename (e.g., "play_48KHz_30s_16b_2ch.wav") +# Output: duration in seconds (e.g., "30") to stdout +# Returns: 0=success, 1=unable to parse duration +extract_clip_duration() { + filename="$1" + + # Extract duration field from pattern: _RATE_DURATIONs_BITS_CHANNELS.wav + # Use sed to match the exact 4-field structure + duration_str="$(printf '%s' "$filename" | sed -n 's/.*_[0-9.]\+KHz_\([0-9]\+\)s_[0-9]\+b_[0-9]\+ch\.wav$/\1/p')" + + if [ -z "$duration_str" ]; then + return 1 + fi + + printf '%s\n' "$duration_str" + return 0 +} + # -------------------------------------------------------------------- # Backend chain + minimal ALSA capture picker (for fallback in run.sh) # -------------------------------------------------------------------- @@ -700,17 +747,6 @@ build_backend_chain() { # Pick a plausible ALSA capture device. # Returns something like hw:0,0 if available, else "default". -alsa_pick_capture() { - line="$(arecord -l 2>/dev/null | sed -n 's/^card \([0-9][0-9]*\):.*device \([0-9][0-9]*\):.*/\1 \2/p' | head -n1)" - if [ -n "$line" ]; then - set -- "$line" - printf 'hw:%s,%s\n' "$1" "$2" - return 0 - fi - printf '%s\n' "default" - return 0 -} - alsa_pick_capture() { command -v arecord >/dev/null 2>&1 || return 1 # Prefer the first real capture device from `arecord -l` @@ -748,10 +784,566 @@ audio_check_clips_available() { for fmt in $formats; do for dur in $durations; do clip="$(resolve_clip "$fmt" "$dur")" - if [ -n "$clip" ] && [ ! -s "$clip" ]; then + # If resolve_clip returns empty string or clip doesn't exist/is empty + if [ -z "$clip" ] || [ ! -s "$clip" ]; then return 1 # At least one clip missing or empty fi done done return 0 # All clips present and non-empty -} \ No newline at end of file +} + +# ---------- New Clip Discovery Functions (for 20-clip enhancement) ---------- + +# ---------- Config Mapping ---------- +# Provides stable, deterministic mapping from Config1-Config20 to specific +# audio format test cases. This ensures reproducible test coverage across +# different systems and releases. +# +# Config numbers map to specific sample rate, bit depth, and channel combinations: +# Config1 → 16 KHz, 16-bit, 2ch Config11 → 352.8 KHz, 32-bit, 1ch +# Config2 → 176.4 KHz, 24-bit, 1ch Config12 → 384 KHz, 32-bit, 2ch +# Config3 → 176.4 KHz, 32-bit, 6ch Config13 → 44.1 KHz, 16-bit, 1ch +# Config4 → 192 KHz, 16-bit, 6ch Config14 → 44.1 KHz, 8-bit, 6ch +# Config5 → 192 KHz, 32-bit, 8ch Config15 → 48 KHz, 8-bit, 2ch +# Config6 → 22.05 KHz, 8-bit, 1ch Config16 → 48 KHz, 8-bit, 8ch +# Config7 → 24 KHz, 24-bit, 6ch Config17 → 88.2 KHz, 16-bit, 8ch +# Config8 → 24 KHz, 32-bit, 8ch Config18 → 88.2 KHz, 24-bit, 2ch +# Config9 → 32 KHz, 16-bit, 2ch Config19 → 8 KHz, 8-bit, 1ch +# Config10 → 32 KHz, 8-bit, 8ch Config20 → 96 KHz, 24-bit, 6ch + +# Translate Config number to test case name +# Returns descriptive test case name for given config number +map_config_to_testcase() { + config_num="$1" + case "$config_num" in + 1) printf 'play_16KHz_16b_2ch\n' ;; + 2) printf 'play_176.4KHz_24b_1ch\n' ;; + 3) printf 'play_176.4KHz_32b_6ch\n' ;; + 4) printf 'play_192KHz_16b_6ch\n' ;; + 5) printf 'play_192KHz_32b_8ch\n' ;; + 6) printf 'play_22.05KHz_8b_1ch\n' ;; + 7) printf 'play_24KHz_24b_6ch\n' ;; + 8) printf 'play_24KHz_32b_8ch\n' ;; + 9) printf 'play_32KHz_16b_2ch\n' ;; + 10) printf 'play_32KHz_8b_8ch\n' ;; + 11) printf 'play_352.8KHz_32b_1ch\n' ;; + 12) printf 'play_384KHz_32b_2ch\n' ;; + 13) printf 'play_44.1KHz_16b_1ch\n' ;; + 14) printf 'play_44.1KHz_8b_6ch\n' ;; + 15) printf 'play_48KHz_8b_2ch\n' ;; + 16) printf 'play_48KHz_8b_8ch\n' ;; + 17) printf 'play_88.2KHz_16b_8ch\n' ;; + 18) printf 'play_88.2KHz_24b_2ch\n' ;; + 19) printf 'play_8KHz_8b_1ch\n' ;; + 20) printf 'play_96KHz_24b_6ch\n' ;; + *) return 1 ;; + esac + return 0 +} + +# Discover all audio clip files in the clips directory +# Outputs newline-separated list of clip filenames (basenames only) to stdout +# Logs diagnostic messages to stderr +# Exit codes: 0=success, 1=directory not found or no clips +discover_audio_clips() { + clips_dir="${AUDIO_CLIPS_BASE_DIR:-AudioClips}" + + # Check directory exists + if [ ! -d "$clips_dir" ]; then + log_error "Clips directory not found: $clips_dir" >&2 + return 1 + fi + + # Find .wav files (only in top level, not recursive) + clips="$(find "$clips_dir" -maxdepth 1 -name "*.wav" -type f 2>/dev/null | sort)" + + # Check if any clips found + if [ -z "$clips" ]; then + log_error "No .wav files found in $clips_dir" >&2 + return 1 + fi + + # Output basenames only to stdout + for clip in $clips; do + basename "$clip" + done + return 0 +} + +# Parse clip filename to extract metadata +# Input: yesterday_48KHz_30s_16b_2ch.wav +# Output: rate=48KHz bits=16b channels=2ch (space-separated key=value pairs) +# Returns: 0=success, 1=parse failure +parse_clip_metadata() { + filename="$1" + + # Extract rate, bits, and channels in one sed call + # Pattern matches exact 4-field structure from end: _RATE_DURATIONs_BITS_CHANNELS.wav + # Anchored to .wav extension to ensure we're matching the correct fields + metadata="$(printf '%s' "$filename" | sed -n 's/.*_\([0-9.]\+KHz\)_\([0-9]\+s\)_\([0-9]\+b\)_\([0-9]\+ch\)\.wav$/\1 \3 \4/p')" + + # Validate extraction succeeded + if [ -z "$metadata" ]; then + log_warn "Cannot parse metadata from: $filename (skipping)" + return 1 + fi + + # Split extracted fields (rate bits channels) + set -- $metadata + rate="$1"; bits="$2"; channels="$3" + + # Validate all components present + if [ -z "$rate" ] || [ -z "$bits" ] || [ -z "$channels" ]; then + log_warn "Cannot parse metadata from: $filename (skipping)" + return 1 + fi + + printf 'rate=%s bits=%s channels=%s\n' "$rate" "$bits" "$channels" + return 0 +} + +# Generate test case name from clip filename +# Input: yesterday_48KHz_30s_16b_2ch.wav +# Output: play_48KHz_16b_2ch +# Returns: 0=success, 1=parse failure +generate_clip_testcase_name() { + filename="$1" + + # Parse metadata (returns "rate=48KHz bits=16b channels=2ch") + metadata="$(parse_clip_metadata "$filename")" || return 1 + + # Extract values using positional parameters and prefix stripping + set -- $metadata + rate="${1#rate=}" + bits="${2#bits=}" + channels="${3#channels=}" + + # Generate test case name + printf 'play_%s_%s_%s\n' "$rate" "$bits" "$channels" + return 0 +} + +# Resolve clip file path from test case name or clip name +# Input: play_48KHz_16b_2ch OR 48KHz_16b_2ch OR yesterday_48KHz_30s_16b_2ch.wav +# Output: AudioClips/yesterday_48KHz_30s_16b_2ch.wav +# Returns: 0=success, 1=not found +resolve_clip_by_name() { + name="$1" + clips_dir="${AUDIO_CLIPS_BASE_DIR:-AudioClips}" + + # If name already looks like a filename, try direct path + if printf '%s' "$name" | grep -F -q -- '.wav'; then + clip_path="$clips_dir/$name" + if [ -f "$clip_path" ]; then + 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 using literal string matching + for clip_file in "$clips_dir"/*.wav; do + [ -f "$clip_file" ] || continue + clip_basename="$(basename "$clip_file")" + + # Check if clip contains the search pattern (literal string match) + if printf '%s' "$clip_basename" | grep -F -q -- "$search_name"; then + printf '%s\n' "$clip_file" + return 0 + fi + done + + return 1 +} + +# Validate clip name against available clips +# Input: requested_name (e.g., play_48KHz_16b_2ch OR Config1), available_clips (list) +# Output: matching clip filename to stdout +# Logs error messages to stderr +# Returns: 0=found, 1=not found +validate_clip_name() { + requested_name="$1" + available_clips="$2" + + # Check if requested_name is a generic config name (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 using POSIX-compliant approach + set -- $available_clips + idx=$# + + # 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." >&2 + return 1 + fi + + # Get clip by index (1-based) using POSIX-compliant approach + current_idx=0 + for clip in $available_clips; do + current_idx=$(expr $current_idx + 1) + if [ "$current_idx" -eq "$config_num" ]; then + printf '%s\n' "$clip" + return 0 + fi + done + + # This shouldn't happen, but just in case + log_error "Invalid config number: $requested_name. Available range: Config1 to Config$idx. Please check again." >&2 + return 1 + fi + + # Try exact match for specific clip names (play_48KHz_16b_2ch format) + for clip in $available_clips; do + test_name="$(generate_clip_testcase_name "$clip" 2>/dev/null)" || continue + if [ "$test_name" = "$requested_name" ]; then + printf '%s\n' "$clip" + return 0 + fi + done + + # No match found - count available clips for helpful message using POSIX-compliant approach + set -- $available_clips + idx=$# + + # No match found - provide helpful error message with range + log_error "Wrong clip name: '$requested_name'. Available range: Config1 to Config$idx. Please check again." >&2 + return 1 +} + +# Input: filter (space-separated patterns), available_clips (list) +# Output: filtered clip list +# Returns: 0=success, 1=no matches +apply_clip_filter() { + filter="$1" + available_clips="$2" + + # If no filter, return all clips + if [ -z "$filter" ]; then + printf '%s\n' "$available_clips" + return 0 + fi + + # Apply filter + filtered="" + for clip in $available_clips; do + for pattern in $filter; do + # Match against filename or test case name + test_name="$(generate_clip_testcase_name "$clip" 2>/dev/null)" || continue + if printf '%s %s' "$clip" "$test_name" | grep -F -q -- "$pattern"; then + filtered="$filtered $clip" + 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" >&2 + log_info "Available clips:" >&2 + for clip in $available_clips; do + log_info " - $(basename "$clip")" >&2 + done + return 1 + fi + + printf '%s\n' "$filtered" + return 0 +} + +# Validate clip file is accessible and non-empty +# Input: clip_path +# Returns: 0=valid, 1=invalid +validate_clip_file() { + clip_path="$1" + + # Check exists + if [ ! -f "$clip_path" ]; then + log_error "Clip file not found: $clip_path" + return 1 + fi + + # Check readable + if [ ! -r "$clip_path" ]; then + log_error "Clip file not readable: $clip_path" + return 1 + fi + + # Check not empty using portable file size helper + size="$(file_size_bytes "$clip_path")" + if [ -z "$size" ] || [ "$size" -le 0 ] 2>/dev/null; then + log_error "Clip file is empty: $clip_path" + return 1 + fi + + return 0 +} + +# Discover and filter clips based on user input +# Input: clip_names (explicit list), clip_filter (pattern filter) +# Output: final list of clip filenames to test (to stdout) +# Logs error messages to stderr +# Returns: 0=success, 1=no valid clips +discover_and_filter_clips() { + clip_names="$1" + clip_filter="$2" + + # Discover all available clips (logs go to stderr automatically) + available_clips="$(discover_audio_clips)" || { + log_error "Failed to discover audio clips" >&2 + return 1 + } + + # If explicit clip names provided, validate and use them + if [ -n "$clip_names" ]; then + validated="" + failed_names="" + + for name in $clip_names; do + # Validate clip name - let error messages display to stderr + if clip="$(validate_clip_name "$name" "$available_clips")"; then + validated="$validated $clip" + else + failed_names="$failed_names $name" + fi + done + + validated="$(printf '%s' "$validated" | sed 's/^ //')" + failed_names="$(printf '%s' "$failed_names" | sed 's/^ //')" + + if [ -z "$validated" ]; then + # Don't repeat the error - validate_clip_name already showed it + return 1 + fi + + # Warn about any failed names (only if there are some valid ones) + 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" + + # Validate by checking if get_record_config_params() supports it + # This eliminates redundant pattern matching that could be misleading + if get_record_config_params "$requested_name" >/dev/null 2>&1; then + return 0 + fi + + log_error "Invalid record config name: $requested_name" >&2 + log_error "Available configs: record_config1-record_config10, record_8KHz_1ch, record_16KHz_1ch, record_16KHz_2ch, record_24KHz_1ch, record_32KHz_2ch, record_44.1KHz_2ch, record_48KHz_2ch, record_48KHz_6ch, record_96KHz_2ch, record_96KHz_6ch" >&2 + return 1 +} + +# Apply filter to record configs +# Input: filter (space-separated patterns), available_configs (list) +# Output: filtered config list +# Returns: 0=success, 1=no matches +apply_record_config_filter() { + filter="$1" + available_configs="$2" + + # If no filter, return all configs + if [ -z "$filter" ]; then + printf '%s\n' "$available_configs" + return 0 + fi + + # Apply filter + filtered="" + for config in $available_configs; do + # Generate descriptive name for matching + desc_name="$(generate_record_testcase_name "$config" 2>/dev/null)" || continue + + for pattern in $filter; do + # Match against config name or descriptive name + if printf '%s %s' "$config" "$desc_name" | grep -F -q -- "$pattern"; then + filtered="$filtered $config" + break + fi + done + done + + # Remove leading space + filtered="$(printf '%s' "$filtered" | sed 's/^ //')" + + # Check if filter matched anything + if [ -z "$filtered" ]; then + log_error "Filter '$filter' matched no record configs" >&2 + log_info "Available configs: record_config1 to record_config10" >&2 + return 1 + fi + + printf '%s\n' "$filtered" + return 0 +} + +# Discover and filter record configs based on user input +# Input: config_names (explicit list), config_filter (pattern filter) +# Output: final list of config names to test (to stdout) +# Logs error messages to stderr +# Returns: 0=success, 1=no valid configs +discover_and_filter_record_configs() { + config_names="$1" + config_filter="$2" + + # Get all available configs + available_configs="$(discover_record_configs)" + + # If explicit config names provided, validate and use them + if [ -n "$config_names" ]; then + validated="" + failed_names="" + + for name in $config_names; do + if validate_record_config_name "$name"; then + validated="$validated $name" + 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" >&2 + fi + + printf '%s\n' "$validated" + return 0 + fi + + # Apply filter if provided + if [ -n "$config_filter" ]; then + filtered="$(apply_record_config_filter "$config_filter" "$available_configs")" || return 1 + printf '%s\n' "$filtered" + return 0 + fi + + # No filter - return all configs + printf '%s\n' "$available_configs" + return 0 +}