From 171681c193cd571ec26eb7050a82d5af854bcd06 Mon Sep 17 00:00:00 2001 From: Emilia Kurdybelska Date: Tue, 16 Sep 2025 09:30:58 +0200 Subject: [PATCH] Add automated tests for Chrome Next Generation Signed-off-by: Emilia Kurdybelska --- case-lib/lib.sh | 66 ++++++++++++++ python-deps.txt | 2 + test-case/check-8bit-play-rec.sh | 127 ++++++++++++++++++++++++++ test-case/check-float-play-rec.sh | 101 +++++++++++++++++++++ test-case/check-selector-play.sh | 142 ++++++++++++++++++++++++++++++ test-case/check-src-play.sh | 100 +++++++++++++++++++++ test-case/check-src-rec.sh | 97 ++++++++++++++++++++ tools/analyze-sound-fragments.py | 27 ++++++ tools/analyze-wav.py | 101 +++++++++++++++++++++ tools/test-sound-generator.py | 61 +++++++++++++ 10 files changed, 824 insertions(+) create mode 100755 test-case/check-8bit-play-rec.sh create mode 100755 test-case/check-float-play-rec.sh create mode 100755 test-case/check-selector-play.sh create mode 100755 test-case/check-src-play.sh create mode 100755 test-case/check-src-rec.sh create mode 100755 tools/analyze-sound-fragments.py create mode 100644 tools/analyze-wav.py create mode 100755 tools/test-sound-generator.py diff --git a/case-lib/lib.sh b/case-lib/lib.sh index e8d6a90d..901b050b 100644 --- a/case-lib/lib.sh +++ b/case-lib/lib.sh @@ -1477,3 +1477,69 @@ restore_topology() { sudo "$insert_script" check_topology } + +# Play sound and record it +# Arguments: 1-arecord options 2-aplay options +play_and_record() +{ + dlogi "Play [aplay $SOF_ALSA_OPTS $SOF_APLAY_OPTS $2] and capture sound [arecord $1]" + # shellcheck disable=SC2086 + arecord $SOF_ALSA_OPTS $SOF_ARECORD_OPTS $1 & PID=$! + # shellcheck disable=SC2086 + aplay $SOF_ALSA_OPTS $SOF_APLAY_OPTS $2 + wait $PID + sleep 1 +} + +# Analyze files to look for glitches. +# Returns exit code 0 if there are no glitches, 1 if there are. +# Arguments: the list of filenames +check_soundfile_for_glitches() +{ + glitched_files=0 + # shellcheck disable=SC2154 + for result_filename in "${all_result_files[@]}" + do + if [ -f "$result_filename" ]; then + dlogi "Analyzing $result_filename file..." + if python3 "$SCRIPT_HOME"/tools/analyze-wav.py "$result_filename"; then + dlogi "$result_filename file is correct" + else + dlogw "Found issues in $result_filename file" + glitched_files=$((glitched_files+1)) + fi + else + dlogw "$result_filename file not found, check for previous errors" + glitched_files=$((glitched_files+1)) + fi + done + + if [ $glitched_files -eq 0 ]; then + dlogi "Analysis finished, no issues found" + return 0 + else + dlogi "$glitched_files files corrupted" + return 1 + fi +} + +# Analyze files to check if they contain expected number of sound fragments. +# Used for testing channels mapping. +# Returns exit code 0 if they do, 1 if the don't or file doesn't exist. +# Arguments: 1-filename, 2-expected nr of fragments +analyze_mixed_sound() +{ + if [ -f "$1" ]; then + dlogi "Analyzing $1 file..." + if python3 "$SCRIPT_HOME"/tools/analyze-sound-fragments.py "$1" "$2"; then + dlogi "$1 file is correct" + return 0 + else + dlogw "Found issues in $1 file" + return 1 + fi + else + dlogw "$1 file not found, check for previous errors" + return 1 + fi +} diff --git a/python-deps.txt b/python-deps.txt index 2fc7e429..81ae5a2c 100644 --- a/python-deps.txt +++ b/python-deps.txt @@ -5,3 +5,5 @@ python3-graphviz python3-construct python3-pytest python3-pandas +python3-soundfile +python3-pydub diff --git a/test-case/check-8bit-play-rec.sh b/test-case/check-8bit-play-rec.sh new file mode 100755 index 00000000..6e1a6a9c --- /dev/null +++ b/test-case/check-8bit-play-rec.sh @@ -0,0 +1,127 @@ +#!/bin/bash + +## +## Case Name: check-8bit-play-rec +## Preconditions: +## - sox installed +## Description: +## This test verifies 8-bit audio playback and recording functionality using ALSA devices. +## It generates test chirp signals in multiple 8-bit formats (unsigned 8-bit, A-LAW, MU-LAW), plays them back, records the output, +## and checks the integrity of the recorded files. The test ensures that the pipeline correctly handles 8-bit audio data for both playback and capture. +## Case steps: +## 1. Generate chirp signals in unsigned 8-bit, A-LAW, MU-LAW, and S32_LE formats using sox. +## 2. Play each chirp file and record the output using arecord and aplay with the specified ALSA devices. +## 3. Convert raw recordings to WAV format for analysis. +## 4. Analyze the recorded files for integrity. +## Expected results: +## - All chirp files are played and recorded without errors. +## - The recorded files are successfully generated and converted. +## - No failures are reported during analysis. +## + +set -e + +# It is pointless to perf component in HDMI pipeline, so filter out HDMI pipelines +# shellcheck disable=SC2034 +NO_HDMI_MODE=true + +# shellcheck source=case-lib/lib.sh +source "$(dirname "${BASH_SOURCE[0]}")"/../case-lib/lib.sh + +OPT_NAME['t']='tplg' OPT_DESC['t']='tplg file, default value is env TPLG: $''TPLG' +OPT_HAS_ARG['t']=1 OPT_VAL['t']="$TPLG" + +OPT_NAME['p']='playback_device' OPT_DESC['p']='ALSA pcm playback device. Example: hw:0,1' +OPT_HAS_ARG['p']=1 OPT_VAL['p']='' + +OPT_NAME['c']='capture_device' OPT_DESC['c']='ALSA pcm capture device. Example: hw:0,1' +OPT_HAS_ARG['c']=1 OPT_VAL['c']='' + +OPT_NAME['s']='sof-logger' OPT_DESC['s']="Open sof-logger trace the data will store at $LOG_ROOT" +OPT_HAS_ARG['s']=0 OPT_VAL['s']=1 + +func_opt_parse_option "$@" + +init_globals() +{ + tplg=${OPT_VAL['t']} + playback_dev=${OPT_VAL['p']} + capture_dev=${OPT_VAL['c']} + + rec8_opt="-c 2 -r 48000 -d 7" + rec_opt="-f S32_LE -c 2 -r 48000 -d 7" + play_opt="-c 2 -r 48000" + + chirp_u8_filename="$LOG_ROOT/chirp_u8.wav" + chirp_alaw_filename="$LOG_ROOT/chirp_alaw.raw" + chirp_mulaw_filename="$LOG_ROOT/chirp_mulaw.raw" + chirp_s32_filename="$LOG_ROOT/chirp_s32.wav" + + u8_play_filename="$LOG_ROOT/rec_play_u8.wav" + alaw_play_filename="$LOG_ROOT/rec_play_alaw.wav" + mulaw_play_filename="$LOG_ROOT/rec_play_mulaw.wav" + + u8_rec_filename="$LOG_ROOT/rec_u8.wav" + alaw_rec_filename="$LOG_ROOT/rec_alaw.wav" + mulaw_rec_filename="$LOG_ROOT/rec_mulaw.wav" + + all_result_files=("$u8_play_filename" "$alaw_play_filename" "$mulaw_play_filename" "$u8_rec_filename" "$alaw_rec_filename" "$mulaw_rec_filename") +} + +generate_chirps() +{ + dlogi "Generating chirps" + sox -n --encoding unsigned-integer -b 8 -r 48000 -c 2 "$chirp_u8_filename" synth 5 sine 100+20000 norm -3 + sox -n --encoding a-law -b 8 -r 48000 -c 2 "$chirp_alaw_filename" synth 5 sine 100+20000 norm -3 + sox -n --encoding mu-law -b 8 -r 48000 -c 2 "$chirp_mulaw_filename" synth 5 sine 100+20000 norm -3 + sox -n --encoding signed-integer -b 32 -r 48000 -c 2 "$chirp_s32_filename" synth 5 sine 100+20000 norm -3 +} + +cleanup() +{ + if [ -f "tmp1.raw" ]; then sudo rm tmp1.raw; fi + if [ -f "tmp2.raw" ]; then sudo rm tmp2.raw; fi +} + +run_tests() +{ + generate_chirps + + set +e + play_and_record "-D$capture_dev $rec_opt $u8_play_filename" "-D$playback_dev $play_opt -t wav $chirp_u8_filename" + play_and_record "-D$capture_dev $rec_opt $alaw_play_filename" "-D$playback_dev $play_opt -t raw -f A_LAW $chirp_alaw_filename" + play_and_record "-D$capture_dev $rec_opt $mulaw_play_filename" "-D$playback_dev $play_opt -t raw -f MU_LAW $chirp_mulaw_filename" + + play_and_record "-D$capture_dev $rec8_opt -f U8 $u8_rec_filename" "-D$playback_dev $chirp_s32_filename" + play_and_record "-D$capture_dev $rec8_opt -f A_LAW -t raw tmp1.raw" "-D$playback_dev $chirp_s32_filename" + play_and_record "-D$capture_dev $rec8_opt -f MU_LAW -t raw tmp2.raw" "-D$playback_dev $chirp_s32_filename" + + sox --encoding a-law -r 48000 -c 2 tmp1.raw "$alaw_rec_filename" + sox --encoding u-law -r 48000 -c 2 tmp2.raw "$mulaw_rec_filename" + set -e + + if check_soundfile_for_glitches "${all_result_files[@]}"; then + dlogi "All files correct" + else + die "Detected corrupted files!" + fi +} + +main() +{ + init_globals + + start_test + logger_disabled || func_lib_start_log_collect + + setup_kernel_check_point + func_lib_check_sudo + func_pipeline_export "$tplg" "type:any" + + run_tests + cleanup +} + +{ + main "$@"; exit "$?" +} diff --git a/test-case/check-float-play-rec.sh b/test-case/check-float-play-rec.sh new file mode 100755 index 00000000..44371091 --- /dev/null +++ b/test-case/check-float-play-rec.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +## +## Case Name: check-float-play-rec +## Preconditions: +## - sox installed +## Description: +## Verify float audio playback and capture using ALSA devices. The test generates a float-encoded chirp and a 32-bit signed integer chirp, +## plays one while recording the other format and vice versa. This validates that the audio pipeline correctly handles FLOAT and S32_LE sample formats +## and that sample conversion between formats works as expected. +## Case steps: +## 1. Generate a 48 kHz stereo chirp in 32-bit float and 32-bit signed integer formats using sox. +## 2. Use arecord/aplay to play the float chirp and record with S32_LE format, then play the S32_LE chirp and record with FLOAT_LE format. +## 3. Save both recorded files into the log directory for later analysis. +## 4. Analyze the recorded files for integrity and correct format. +## Expected results: +## - Both playback and recording complete without errors. +## - The recorded files are created in the log directory and match expected sample formats. +## - No failures are reported during analysis. +## + +set -e + +# It is pointless to perf component in HDMI pipeline, so filter out HDMI pipelines +# shellcheck disable=SC2034 +NO_HDMI_MODE=true + +# shellcheck source=case-lib/lib.sh +source "$(dirname "${BASH_SOURCE[0]}")"/../case-lib/lib.sh + +OPT_NAME['t']='tplg' OPT_DESC['t']='tplg file, default value is env TPLG: $''TPLG' +OPT_HAS_ARG['t']=1 OPT_VAL['t']="$TPLG" + +OPT_NAME['p']='playback_device' OPT_DESC['p']='ALSA pcm playback device. Example: hw:0,1' +OPT_HAS_ARG['p']=1 OPT_VAL['p']='' + +OPT_NAME['c']='capture_device' OPT_DESC['c']='ALSA pcm capture device. Example: hw:0,1' +OPT_HAS_ARG['c']=1 OPT_VAL['c']='' + +OPT_NAME['s']='sof-logger' OPT_DESC['s']="Open sof-logger trace the data will store at $LOG_ROOT" +OPT_HAS_ARG['s']=0 OPT_VAL['s']=1 + +func_opt_parse_option "$@" + +init_globals() +{ + tplg=${OPT_VAL['t']} + playback_dev=${OPT_VAL['p']} + capture_dev=${OPT_VAL['c']} + + rec_opt="-c 2 -r 48000 -d 7" + + chirp_float_filename="$LOG_ROOT/chirp_float_48k.wav" + chirp_s32_filename="$LOG_ROOT/chirp_s32_48k.wav" + + rec_play_filename="$LOG_ROOT/rec_play_float.wav" + rec_filename="$LOG_ROOT/rec_float.wav" + + all_result_files=("$rec_play_filename" "$rec_filename") +} + +generate_chirps() +{ + dlogi "Generating chirps" + sox -n --encoding float -r 48000 -c 2 -b 32 "$chirp_float_filename" synth 5 sine 100+20000 norm -3 + sox -n --encoding signed-integer -L -r 48000 -c 2 -b 32 "$chirp_s32_filename" synth 5 sine 100+20000 norm -3 +} + +run_tests() +{ + generate_chirps + + set +e + play_and_record "-D$capture_dev $rec_opt -f S32_LE $rec_play_filename" "-D$playback_dev $chirp_float_filename" + play_and_record "-D$capture_dev $rec_opt -f FLOAT_LE $rec_filename" "-D$playback_dev $chirp_s32_filename" + set -e + + if check_soundfile_for_glitches "${all_result_files[@]}"; then + dlogi "All files correct" + else + die "Detected corrupted files!" + fi +} + +main() +{ + init_globals + + start_test + logger_disabled || func_lib_start_log_collect + + setup_kernel_check_point + func_lib_check_sudo + func_pipeline_export "$tplg" "type:any" + + run_tests +} + +{ + main "$@"; exit "$?" +} diff --git a/test-case/check-selector-play.sh b/test-case/check-selector-play.sh new file mode 100755 index 00000000..c70648c9 --- /dev/null +++ b/test-case/check-selector-play.sh @@ -0,0 +1,142 @@ +#!/bin/bash + +## +## Case Name: check-selector-play +## Preconditions: +## - hardware or software loopback is available +## Description: +## Verify the selector/mixing behavior of playback pipelines when routing audio to different speaker configurations +## (mono, stereo, 5.1/6ch, 7.1/8ch). The test plays coresponding test sound files and records +## the output from the capture device to validate channel routing. +## Case steps: +## 1. Play 1,2,6 and 8-channel sounds. +## 2. Record the output on 2 channels. +## 5. Save recorded files and run automated checks for channel presence. +## Expected result: +## - Playback and recording complete without errors for each tested channel configuration. +## - Recorded files exist for each test and contain the expected channel information (all the sounds from original file is present in output file). +## + +set -e + +# It is pointless to perf component in HDMI pipeline, so filter out HDMI pipelines +# shellcheck disable=SC2034 +NO_HDMI_MODE=true + +# shellcheck source=case-lib/lib.sh +source "$(dirname "${BASH_SOURCE[0]}")"/../case-lib/lib.sh + +OPT_NAME['t']='tplg' OPT_DESC['t']='tplg file, default value is env TPLG: $''TPLG' +OPT_HAS_ARG['t']=1 OPT_VAL['t']="$TPLG" + +OPT_NAME['p']='playback_device' OPT_DESC['p']='ALSA pcm playback device. Example: hw:0,1' +OPT_HAS_ARG['p']=1 OPT_VAL['p']='' + +OPT_NAME['c']='capture_device' OPT_DESC['c']='ALSA pcm capture device. Example: hw:0,1' +OPT_HAS_ARG['c']=1 OPT_VAL['c']='' + +OPT_NAME['s']='sof-logger' OPT_DESC['s']="Open sof-logger trace the data will store at $LOG_ROOT" +OPT_HAS_ARG['s']=0 OPT_VAL['s']=1 + +func_opt_parse_option "$@" + +init_globals() +{ + tplg=${OPT_VAL['t']} + playback_dev=${OPT_VAL['p']} + capture_dev=${OPT_VAL['c']} + + channels_to_test=(1 2 6 8) + + rec_opt="-f S16_LE -c 2 -r 48000" + + failures=0 +} + +# This test requires topology allowing channel downmixing. +# Checks and changes topology if needed. +check_tplg() +{ + dlogi "Checking topology..." + nocodec_tplg="sof-ptl-nocodec.tplg" + + if [[ "$tplg" == *"nocodec"* && "$tplg" != *"$nocodec_tplg"* ]]; then + dlogi "NO-CODEC topology change required" + new_tplg_filename=$nocodec_tplg + update_topology_filename + fi +} + +# Arguments: the number of channels soundfile should have +generate_soundfile() +{ + ch_nr="$1" + if python3 "$SCRIPT_HOME"/tools/test-sound-generator.py "$1"; then + dlogi "Testfile generated." + return 0 + else + dlogw "Error generating testfile" + return 1 + fi +} + +# Checks for soundfiles needed for test, generates missing ones +prepare_test_soundfiles() +{ + for ch_nr in "${channels_to_test[@]}" + do + filename="$HOME/Music/${ch_nr}_channels_test.wav" + if [ ! -f "$filename" ]; then + generate_soundfile "$ch_nr" + fi + done +} + +run_tests() +{ + set +e + for ch_nr in "${channels_to_test[@]}" + do + test_filename="$HOME/Music/${ch_nr}_channels_test.wav" + result_filename="$LOG_ROOT/rec_${ch_nr}ch.wav" + + play_and_record "-D$capture_dev $rec_opt -d 25 $result_filename" "-Dplug$playback_dev $test_filename" + + if ! analyze_mixed_sound "$result_filename" "$ch_nr"; then + failures=$((failures+1)) + fi + done + set -e + + if [ $failures -eq 0 ]; then + dlogi "All files correct" + else + die "Detected corrupted files!" + fi + +} + +main() +{ + init_globals + prepare_test_soundfiles + check_tplg + + start_test + + if [[ "$tplg" != *"nocodec"* ]]; then + skip_test "Test currently supported for NO-CODEC platforms only" + fi + + logger_disabled || func_lib_start_log_collect + + setup_kernel_check_point + func_lib_check_sudo + func_pipeline_export "$tplg" "type:any" + + run_tests +} + +{ + main "$@"; exit "$?" +} diff --git a/test-case/check-src-play.sh b/test-case/check-src-play.sh new file mode 100755 index 00000000..9b6148e8 --- /dev/null +++ b/test-case/check-src-play.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +## +## Case Name: check-src-play +## Preconditions: +## ffmpeg installed +## Description: +## Verify sample-rate conversion (SRC) behavior when playing audio at various sample rates and capturing at a fixed 48 kHz rate. +## The test generates chirp signals at multiple sample rates, plays them back through the DUT, and records the resulting audio as 48 kHz. This validates +## that the audio pipeline correctly resamples incoming streams and preserves signal integrity across sample-rate boundaries. +## Case steps: +## 1. For each sample rate under test, generate a stereo chirp waveform using ffmpeg at that sample rate. +## 2. Play the generated file through the specified ALSA playback device and record the output with arecord capturing at 48 kHz. +## 3. Save each recorded file into the test log directory for later inspection. +## 4. Analyze the recorded files to ensure the chirp content is present and resampling occurred without errors. +## Expected result: +## - Playback and capture operations complete without errors for all sample rates. +## - A recorded file exists for each input sample rate in the log directory. +## - Recorded files contain the expected chirp content (no severe distortion or silence) and no pipeline errors are observed. +## + +set -e + +# It is pointless to perf component in HDMI pipeline, so filter out HDMI pipelines +# shellcheck disable=SC2034 +NO_HDMI_MODE=true + +# shellcheck source=case-lib/lib.sh +source "$(dirname "${BASH_SOURCE[0]}")"/../case-lib/lib.sh + +OPT_NAME['t']='tplg' OPT_DESC['t']='tplg file, default value is env TPLG: $''TPLG' +OPT_HAS_ARG['t']=1 OPT_VAL['t']="$TPLG" + +OPT_NAME['p']='playback_device' OPT_DESC['p']='ALSA pcm playback device. Example: hw:0,1' +OPT_HAS_ARG['p']=1 OPT_VAL['p']='' + +OPT_NAME['c']='capture_device' OPT_DESC['c']='ALSA pcm capture device. Example: hw:0,1' +OPT_HAS_ARG['c']=1 OPT_VAL['c']='' + +OPT_NAME['s']='sof-logger' OPT_DESC['s']="Open sof-logger trace the data will store at $LOG_ROOT" +OPT_HAS_ARG['s']=0 OPT_VAL['s']=1 + +func_opt_parse_option "$@" + +init_globals() +{ + tplg=${OPT_VAL['t']} + playback_dev=${OPT_VAL['p']} + capture_dev=${OPT_VAL['c']} + + chirp_rates=("350" "700" "1000" "1500" "2000" "2000" "3000" "4000" "4500" "8000" "9000") + sample_rates=("8000" "16000" "22050" "32000" "44100" "48000" "64000" "88200" "96000" "176400" "192000") + rec_opt="-f S16_LE -c 2 -r 48000 -d 7" + + all_result_files=() +} + +run_tests() +{ + set +e + for i in "${!sample_rates[@]}" + do + sample_rate=${sample_rates[$i]} + chirp_rate=${chirp_rates[$i]} + + test_sound_filename=$LOG_ROOT/play.wav + result_filename=$LOG_ROOT/rec_play_$sample_rate.wav + all_result_files+=("$result_filename") + + dlogi "Play $sample_rate Hz chirp 0 - $chirp_rate Hz, capture as 48 kHz" + ffmpeg -y -f lavfi -i "aevalsrc='sin($chirp_rate*t*2*PI*t)':s=$sample_rate:d=5" -ac 2 "$test_sound_filename" #TODO: maybe separate dir for artifacts ?? + + play_and_record "-D$capture_dev $rec_opt $result_filename" "-D$playback_dev $test_sound_filename" + done + set -e + + if check_soundfile_for_glitches "${all_result_files[@]}"; then + dlogi "All files correct" + else + die "Detected corrupted files!" + fi +} + +main() +{ + init_globals + + start_test + logger_disabled || func_lib_start_log_collect + + setup_kernel_check_point + func_lib_check_sudo + func_pipeline_export "$tplg" "type:any" + + run_tests +} + +{ + main "$@"; exit "$?" +} diff --git a/test-case/check-src-rec.sh b/test-case/check-src-rec.sh new file mode 100755 index 00000000..d3e3e8a6 --- /dev/null +++ b/test-case/check-src-rec.sh @@ -0,0 +1,97 @@ +#!/bin/bash + +## +## Case Name: check-src-rec +## Preconditions: +## ffmpeg installed +## Description: +## Verify sample-rate conversion (SRC) for capture paths by playing a reference 48 kHz chirp and recording it at a variety of lower and higher sample rates. +## This ensures the capture pipeline correctly resamples incoming audio from a fixed playback sample rate to requested capture rates. +## Case steps: +## 1. Generate a 48 kHz stereo chirp signal using ffmpeg and store it in the test log directory. +## 2. For each sample rate under test, play the 48 kHz chirp and record using arecord at the target sample rate. +## 3. Save recorded files for each sample rate to the log directory for later inspection. +## 4. Analyze the recorded files to confirm the chirp content is present and resampling is performed without major artifacts. +## Expected result: +## - Playback and capture operations succeed for all tested sample rates. +## - A recorded file exists for each requested sample rate in the log directory. +## - Recorded files contain the expected chirp (no severe distortion or silence) and no pipeline errors are observed. +## + +set -e + +# It is pointless to perf component in HDMI pipeline, so filter out HDMI pipelines +# shellcheck disable=SC2034 +NO_HDMI_MODE=true + +# shellcheck source=case-lib/lib.sh +source "$(dirname "${BASH_SOURCE[0]}")"/../case-lib/lib.sh + +OPT_NAME['t']='tplg' OPT_DESC['t']='tplg file, default value is env TPLG: $''TPLG' +OPT_HAS_ARG['t']=1 OPT_VAL['t']="$TPLG" + +OPT_NAME['p']='playback_device' OPT_DESC['p']='ALSA pcm playback device. Example: hw:0,1' +OPT_HAS_ARG['p']=1 OPT_VAL['p']='' + +OPT_NAME['c']='capture_device' OPT_DESC['c']='ALSA pcm capture device. Example: hw:0,1' +OPT_HAS_ARG['c']=1 OPT_VAL['c']='' + +OPT_NAME['s']='sof-logger' OPT_DESC['s']="Open sof-logger trace the data will store at $LOG_ROOT" +OPT_HAS_ARG['s']=0 OPT_VAL['s']=1 + +func_opt_parse_option "$@" + +init_globals() +{ + tplg=${OPT_VAL['t']} + playback_dev=${OPT_VAL['p']} + capture_dev=${OPT_VAL['c']} + + sample_rates=("8000" "16000" "22050" "32000" "44100" "48000") + + rec_opt="-f S16_LE -c 2 -d 7" + + test_sound_filename=$LOG_ROOT/play.wav + all_result_files=() +} + +run_tests() +{ + dlogi "Generate 48 kHz chirp 0 - 20 kHz" + ffmpeg -y -f lavfi -i "aevalsrc='sin(2000*t*2*PI*t)':s=48000:d=5" -ac 2 "$test_sound_filename" + + set +e + for i in "${!sample_rates[@]}" + do + sample_rate=${sample_rates[$i]} + + result_filename=$LOG_ROOT/rec_$sample_rate.wav + all_result_files+=("$result_filename") + play_and_record "-D$capture_dev $rec_opt -r $sample_rate $result_filename" "-D$playback_dev $test_sound_filename" + done + set -e + + if check_soundfile_for_glitches "${all_result_files[@]}"; then + dlogi "All files correct" + else + die "Detected corrupted files!" + fi +} + +main() +{ + init_globals + + start_test + logger_disabled || func_lib_start_log_collect + + setup_kernel_check_point + func_lib_check_sudo + func_pipeline_export "$tplg" "type:any" + + run_tests +} + +{ + main "$@"; exit "$?" +} diff --git a/tools/analyze-sound-fragments.py b/tools/analyze-sound-fragments.py new file mode 100755 index 00000000..199b01f1 --- /dev/null +++ b/tools/analyze-sound-fragments.py @@ -0,0 +1,27 @@ +import sys +from pydub import AudioSegment, silence + + +def count_sound_fragments(file): + audio = AudioSegment.from_wav(file) + + audio_fragments = silence.split_on_silence( + audio, + min_silence_len=100, + silence_thresh=audio.dBFS, + keep_silence=100 + ) + return len(audio_fragments) + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Wrong nr of arguments! Usage: python3 analyze-sound-fragments.py ") + sys.exit(1) + + filename=sys.argv[1] + expected_chs_nr=int(sys.argv[2]) + + result = count_sound_fragments(filename) + print(f"Found sound from {result} channels") + sys.exit(0 if result==expected_chs_nr else 1) diff --git a/tools/analyze-wav.py b/tools/analyze-wav.py new file mode 100644 index 00000000..daf5a153 --- /dev/null +++ b/tools/analyze-wav.py @@ -0,0 +1,101 @@ +""" +Detect glitches (vertical lines) in a spectrogram of a WAV file. +Usage: python analyze-wav.py +The script reads the WAV file, computes its spectrogram, +and identifies time points where vertical lines (broadband events) occur. +The audio's first and last 0.5 seconds are trimmed to avoid edge artifacts. + +The vertical lines are computed the following way: +1. Trim silence from the start and end of the audio. +2. Trim 0.5 seconds from the start and end of the audio to avoid edge artifacts. +3. Compute the spectrogram of the audio signal. +4. Normalize the spectrogram per frequency bin to [0, 1]. +5. For each time slice, determine the fraction of frequency bins that are "active" + (i.e., have normalized values above a certain threshold). +6. If the fraction of active frequency bins exceeds a predefined threshold, + mark that time slice as containing a vertical line. + +We define active_freqs instead of just calculating the average normalised energy +value to avoid a situation, where few very loud frequencies dominate the average. + +Output: +- Prints detected glitch timestamps (in seconds) +- Returns 1 (exit code) if glitches are detected, 0 if sound is clean. +""" + +import sys +import numpy as np +from scipy.io import wavfile +from scipy.signal import spectrogram +import soundfile as sf + + +def get_dtype(subtype): + if 'pcm_16' in subtype: + return 'int16' + elif 'pcm_24' in subtype: + return 'int32' # soundfile uses int32 for 24-bit PCM + elif 'pcm_32' in subtype: + return 'int32' + elif 'ulaw' in subtype or 'mulaw' in subtype: + return 'int16' + elif 'alaw' in subtype: + return 'int16' + else: + return 'float32' # fallback for unknown or float encodings + + +def detect_vertical_lines(wav_file): + info = sf.info(wav_file) + dtype = get_dtype(info.subtype.lower()) + + data, sr = sf.read(wav_file, dtype=dtype) + if data.ndim > 1: + data = data[:, 0] + # Remove silence at the beginning and end + silence_threshold = 0.01 + abs_data = np.abs(data) + mean_amp = abs_data.mean() + threshold = silence_threshold * mean_amp + non_silent = np.where(abs_data > threshold)[0] + if non_silent.size == 0: + print("No non-silent audio detected.") + else: + print(f"Non-silent audio from {non_silent[0]/sr:.2f}s to {non_silent[-1]/sr:.2f}s") + start_idx = non_silent[0] + end_idx = non_silent[-1] + 1 + data = data[start_idx:end_idx] + # Trim beginning and end + trim_duration_sec = 0.2 + trim_samples = int(trim_duration_sec * sr) + if len(data) <= 2 * trim_samples: + print("Audio file too short after trimming.") + return [] + + data = data[trim_samples:-trim_samples] + + f, t, Sxx = spectrogram(data, sr) + Sxx_dB = 10 * np.log10(Sxx + 1e-10) + + # Normalize per frequency bin to [0, 1] + Sxx_norm = (Sxx_dB - np.min(Sxx_dB, axis=1, keepdims=True)) / ( + np.ptp(Sxx_dB, axis=1, keepdims=True) + 1e-10 + ) + freq_threshold = 0.6 + # If the normalised frequency is greater than the threshold, mark it as active. + active_freqs = Sxx_norm > freq_threshold + coverage = np.sum(active_freqs, axis=0) / active_freqs.shape[0] + coverage_threshold = 0.8 + # If coverage exceeds threshold, mark as vertical line. + vertical_lines = np.where(coverage > coverage_threshold)[0] + print(f"Detected possible glitches at time indices: {vertical_lines + trim_samples + non_silent[0]}") + print(f"Corresponding times (s): {t[vertical_lines] + trim_duration_sec + non_silent[0]/sr}") + return vertical_lines + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python analyze-wav.py ") + sys.exit(1) + + result = detect_vertical_lines(sys.argv[1]) + sys.exit(1 if result.any() else 0) diff --git a/tools/test-sound-generator.py b/tools/test-sound-generator.py new file mode 100755 index 00000000..cf50b45a --- /dev/null +++ b/tools/test-sound-generator.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +# Generates soundfile with given amount of channels. +# Every channel has a 2s sound, there is 1s pause between sounds. Every channel sound has a different frequency. +# Result file is saved as ~/Music/{num_channels}_channels_test.wav. +# Usage: python3 test-sound-generator.py + +import sys +import numpy as np +import soundfile as sf + + +SAMPLE_RATE = 48000 +SOUND_DURATION = 2.0 +SILENCE_DURATION = 1.0 +DIR_PATH = "/home/ubuntu/Music/" + + +# Generates sound with given number of channels. +# Returns path of the result soundfile. +def generate_sound(channels_nr): + frequency = 500 + + sound_samples = int(SOUND_DURATION * SAMPLE_RATE) + silence_samples = int(SILENCE_DURATION * SAMPLE_RATE) + + # Start sound quietly, gradually make it louder, and then make it quiet again. + fade = np.linspace(0, 1, sound_samples // 2) + fade_out = np.linspace(1, 0, sound_samples - len(fade)) + envelope = np.concatenate((fade, fade_out)) + + total_samples = (sound_samples + silence_samples) * channels_nr + multichannel = np.zeros((total_samples, channels_nr), dtype=np.float32) + + for ch in range(channels_nr): + start = ch * (sound_samples + silence_samples) + end = start + sound_samples + + t = np.linspace(0, SOUND_DURATION, sound_samples, endpoint=False) + signal = 0.5 * np.sin(2 * np.pi * frequency * t) * envelope + multichannel[start:end, ch] = signal + + frequency+=100 # Sound on every channel has a different frequency + + # Save result to file + filepath = f"{DIR_PATH}{channels_nr}_channels_test.wav" + sf.write(filepath, multichannel, SAMPLE_RATE, subtype="PCM_16") + return filepath + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Incorrect arguments! Usage: python3 test-sound-generator.py ") + sys.exit(1) + + ch_nr=int(sys.argv[1]) + + file_path = generate_sound(ch_nr) + print(f"Sound with {ch_nr} channels generated: {file_path}") + + sys.exit(0)