From 575bd7baced902b1be44cf30a86ac44157a7cdc4 Mon Sep 17 00:00:00 2001 From: Seppo Ingalsuo Date: Tue, 6 Oct 2020 16:31:36 +0300 Subject: [PATCH] Test-case: Add check volume levels This patch adds a shell script to check that SOF volume component related system applies correctly channels gains and mute switches. The script determines the volume gains from measured sine wave levels and compares to used amixer control values. The test can be executed with a nocodec topology where SSP ports are connected to play -> capture loopback. The loopback avoids need for noise sensitive acoustical test or more complex electrical test. An error is issued if the gains deviate more than used +/- 0.5 dB tolerance. Signed-off-by: Seppo Ingalsuo --- test-case/check-volume-levels.sh | 337 +++++++++++++++++++++++++++++++ test-case/run-all-tests.sh | 7 + tools/check_volume_levels.m | 253 +++++++++++++++++++++++ 3 files changed, 597 insertions(+) create mode 100755 test-case/check-volume-levels.sh create mode 100644 tools/check_volume_levels.m diff --git a/test-case/check-volume-levels.sh b/test-case/check-volume-levels.sh new file mode 100755 index 00000000..fc2fc45c --- /dev/null +++ b/test-case/check-volume-levels.sh @@ -0,0 +1,337 @@ +#!/bin/bash + +# SPDX-License-Identifier: BSD-3-Clause +# Copyright(c) 2020 Intel Corporation. All rights reserved. + +set -e + +## +## Case Name: check-volume-levels +## Preconditions: +## topology is nocodec with loopack in PCM0P -> PCM0C +## aplay should work +## arecord should work +## PCM0C capture PGA supports -50 - +30 dB range +## PCM0C capture PGA supports mute switch +## Description: +## Set volume and mute switch to various values, measure +## volume gain from the actual levels and compare to set gains. +## Case step: +## 1. Start aplay +## 2a. Capture 1st wav file +## 2b. Capture 2nd wav file +## 2c. Capture 3rd wav file +## 3. Measure volume gains +## Expect result: +## command line check with $? without error +## + +TESTDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) + +# shellcheck source=case-lib/lib.sh +source "$TESTDIR/case-lib/lib.sh" + +OPT_OPT_lst['t']='tplg' OPT_DESC_lst['t']='tplg file, default value is env TPLG: $''TPLG' +OPT_PARM_lst['t']=1 OPT_VALUE_lst['t']="$TPLG" + +func_opt_parse_option "$@" + +tplg=${OPT_VALUE_lst['t']} + +TPLGREADER="$TESTDIR"/tools/sof-tplgreader.py +PCM_ID=0 +CAP_FORMAT="S16_LE" +CAP_RATE="48000" +VOL_P30DB=80 +VOL_P10DB=60 +VOL_0DB=50 +VOL_M10DB=40 +VOL_M20DB=30 +VOL_M30DB=20 +VOL_MAX=$VOL_P30DB +VOL_MIN=1 +VOL_MUTE=0 +VOL_NOM=$VOL_0DB +APLAY_WAV=$(mktemp --suffix=.wav) +ARECORD_WAV1=$(mktemp --suffix=.wav) +ARECORD_WAV2=$(mktemp --suffix=.wav) +ARECORD_WAV3=$(mktemp --suffix=.wav) + +# +# Main test procedure +# + +main () { + # Preparations + do_preparations + generate_sine + + # Start sine playback + test -f "$APLAY_WAV" || die "Error: File $APLAY_WAV does not exist" + dlogi "Playing file $APLAY_WAV" + timeout -k 5 120 aplay -D "$PLAY_HW" "$APLAY_WAV" & aplayPID=$! + nap + + # Do capture tests + run_test_1 + run_test_2 + run_test_3 + + # Stop sine playback and do cleanup + nap + dlogi "The test procedure is now complete. Killing the aplay process $aplayPID." + kill $aplayPID + + # Measure, delete unnecessary wav files if passed + if measure_levels; then + dlogi "Deleting temporary files" + rm -f "$APLAY_WAV" "$ARECORD_WAV1" "$ARECORD_WAV2" "$ARECORD_WAV3" + fi +} + +# +# Test #1 volume and mute switch controls +# + +# 0...1s channels 1-4 max gain +# 1...2s channels 1-4 different gains +# 2...3s channels 1-4 nominal gain +# 3...4s channels 1-4 muted +# 4...5s channels 1-4 nominal gain +# 5...7s channels 1-4 muted +# 7...8s channels 1-4 max gain +# 8...9s channels 1-4 muted gain +# 9..10s channels 1-4 min gain +# 10..11s channels 1-4 different gains +run_test_1 () { + cset_max; cset_unmute; nap + + arecord_helper "$ARECORD_WAV1" "$CAP_CHANNELS" 11 & arecordPID=$! + nap + cset_volume_diff1 "$CAP_CHANNELS"; nap + cset_nom; nap + cset_mute; nap + cset_unmute; nap + cset_mute; nap + cset_max; nap + cset_unmute; nap + cset_mutevol; nap + cset_min; nap + cset_volume_diff2 "$CAP_CHANNELS" + + # Wait for arecord process to complete + dlogi "Waiting $arecordPID" + wait $arecordPID + dlogi "Ready." +} + +# +# Test #2, check gains preservation from previous to next +# + +# 0..1s channels 1-4 previous gains +# 1..2s channels 1-4 different mute switches +# 2..3s channels 1-4 all muted +run_test_2 () { + arecord_helper "$ARECORD_WAV2" "$CAP_CHANNELS" 4 & arecordPID=$! + nap + cset_nom; cset_mute_diff1 "$CAP_CHANNELS"; nap + cset_mute; nap + + dlogi "Waiting $arecordPID" + wait $arecordPID + dlogi "Ready." +} + +# +# Test #3, test mute switch preservation from previous to next +# + +# 0..1s channels 1-4 muted +# 1..2s channels 1-4 nominal gain +# 2..3s different mute switches +# 3..4s channels 1-4 nominal gain +run_test_3 () { + arecord_helper "$ARECORD_WAV3" "$CAP_CHANNELS" 4 & arecordPID=$! + nap + cset_nom; nap + cset_mute_diff2 "$CAP_CHANNELS"; nap + cset_unmute + + dlogi "Waiting $arecordPID" + wait $arecordPID + dlogi "Ready." +} + +# +# Helper functions +# + +cset_nom () { + dlogi "Set nominal volume" + amixer cset name="$CAP_VOLUME" $VOL_NOM +} + +cset_max () { + dlogi "Set maximum volume" + amixer cset name="$CAP_VOLUME" $VOL_MAX +} + +cset_min () { + dlogi "Set minimum volume" + amixer cset name="$CAP_VOLUME" $VOL_MIN +} + +cset_mutevol () { + dlogi "Set mute via volume" + amixer cset name="$CAP_VOLUME" $VOL_MUTE +} +cset_mute () { + dlogi "Set mute via switch" + amixer cset name="$CAP_SWITCH" off +} + +cset_unmute () { + dlogi "Set unmute" + amixer cset name="$CAP_SWITCH" on +} + +nap () { + dlogi "Sleeping" + sleep 1 +} + +cset_mute_diff1 () { + dlogi "Set switch pattern 1" + case "$1" in + 4) + amixer cset name="$CAP_SWITCH" off,on,on,off ;; + 2) + amixer cset name="$CAP_SWITCH" off,on ;; + 1) + amixer cset name="$CAP_SWITCH" off ;; + esac +} + +cset_mute_diff2 () { + dlogi "Set switch pattern 2" + case "$1" in + 4) + amixer cset name="$CAP_SWITCH" on,off,off,on ;; + 2) + amixer cset name="$CAP_SWITCH" on,off ;; + 1) + amixer cset name="$CAP_SWITCH" on ;; + esac +} + +cset_volume_diff1 () { + dlogi "Set volume pattern 1" + case "$1" in + 4) + amixer cset name="$CAP_VOLUME" $VOL_P10DB,$VOL_0DB,$VOL_M10DB,$VOL_M30DB ;; + 2) + amixer cset name="$CAP_VOLUME" $VOL_P10DB,$VOL_0DB ;; + 1) + amixer cset name="$CAP_VOLUME" $VOL_P10DB ;; + esac +} + +cset_volume_diff2 () { + dlogi "Set volume pattern 2" + case "$1" in + 4) + amixer cset name="$CAP_VOLUME" $VOL_M10DB,$VOL_P10DB,$VOL_0DB,$VOL_M20DB ;; + 2) + amixer cset name="$CAP_VOLUME" $VOL_M10DB,$VOL_P10DB ;; + 1) + amixer cset name="$CAP_VOLUME" $VOL_M10DB ;; + esac +} + +arecord_helper () { + dlogi "Capturing file $1" + timeout -k 5 30 arecord -D "$CAP_HW" -f $CAP_FORMAT -r $CAP_RATE -c "$2" -d "$3" "$1" +} + +do_preparations () { + + if [[ "$tplg" != *"nocodec"* ]]; then + # Return special value 2 with exit + dlogi "This test is executed only with nocodec topologies. Returning Not Applicable." + exit 2 + fi + + test -n "$(command -v octave)" || { + dlogi "Octave not found (need octave and octave-signal packages). Returning Not Applicable." + exit 2 + } + + # Get max. channels count to use from capture device + # remove xargs after the trailing blank from tplgreader is fixed + CAP_CHANNELS=$($TPLGREADER "$tplg" -f "id:$PCM_ID & type:capture" -d ch_max -v) + CAP_HW=$($TPLGREADER "$tplg" -f "id:$PCM_ID & type:capture" -d dev -v) + PLAY_HW=$($TPLGREADER "$tplg" -f "id:$PCM_ID & type:playback" -d dev -v) + CAP_PGA=$($TPLGREADER "$tplg" -f "id:$PCM_ID & type:capture" -d pga -v) + PLAY_PGA=$($TPLGREADER "$tplg" -f "id:$PCM_ID & type:playback" -d pga -v) + export CAP_CHANNELS + export CAP_HW + export PLAY_HW + + dlogi "Test uses $CAP_CHANNELS channels" + dlogi "Playback device is $PLAY_HW" + dlogi "Playback PGA is $PLAY_PGA" + dlogi "Capture device is $CAP_HW" + dlogi "Capture PGA is $CAP_PGA" + + # Find amixer controls for capture, error if more than one PGA + numpga=${#CAP_PGA[@]} + test "$numpga" = 1 || die "Error: more than one capture PGA found." + + tmp=$(amixer controls | grep -e "$CAP_PGA.*Volume" || true ) + test -n "$tmp" || die "No control with name Volume found in $CAP_PGA" + search="name=" + CAP_VOLUME=${tmp#*$search} + export CAP_VOLUME + dlogi "Capture volume control name is $CAP_VOLUME" + + tmp=$(amixer controls | grep -e "$CAP_PGA.*Switch" || true ) + test -n "$tmp" || die "No control with name Switch found in $CAP_PGA" + CAP_SWITCH=${tmp#*$search} + export CAP_SWITCH + dlogi "Capture switch control name is $CAP_SWITCH" + + # Check needed controls + amixer cget name="$CAP_VOLUME" || die "Error: failed capture volume get command" + amixer cget name="$CAP_SWITCH" || die "Error: failed capture switch get command" + + for pga in $PLAY_PGA; do + tmp=$(amixer controls | grep "$pga" | grep Volume) + search="name=" + play_volume=${tmp#*$search} + dlogi "Set $play_volume to 100%" + amixer cset name="$play_volume" 100% || die "Error: failed play volume set command" + done +} + +generate_sine () { + dlogi "Creating sine wave file" + cd "$TESTDIR"/tools + octave --silent --no-gui --eval "check_volume_levels('generate', '$APLAY_WAV')" || + die "Error: failed sine wave generate." +} + +measure_levels () { + dlogi "Measuring volume gains" + cd "$TESTDIR"/tools + octave --silent --no-gui --eval "check_volume_levels('measure', '$ARECORD_WAV1', '$ARECORD_WAV2', '$ARECORD_WAV3')" || { + dloge "Error: Failed one or more tests in volume levels check." + die "Please inspect files: $ARECORD_WAV1, $ARECORD_WAV2, and $ARECORD_WAV3." + } +} + +# +# Start test +# + +main "$@" diff --git a/test-case/run-all-tests.sh b/test-case/run-all-tests.sh index 9112a74d..bfbd17e8 100755 --- a/test-case/run-all-tests.sh +++ b/test-case/run-all-tests.sh @@ -64,6 +64,9 @@ tplg-binary sof-logger ' +# Requires Octave +# testlist="$testlist volume_levels" + main() { # Default values overriden by the environment if any @@ -191,6 +194,10 @@ test_volume() { "$mydir"/volume-basic-test.sh -l "$large_loop" } +test_volume_levels() +{ + "$mydir"/check-volume-levels.sh +} test_signal-stop-start-playback() { "$mydir"/check-signal-stop-start.sh -m playback -c "$medium_count" diff --git a/tools/check_volume_levels.m b/tools/check_volume_levels.m new file mode 100644 index 00000000..8e2b1e5b --- /dev/null +++ b/tools/check_volume_levels.m @@ -0,0 +1,253 @@ +function check_volume_levels(cmd, fn1, fn2, fn3) + +% check_volume_levels(cmd, fn1, fn2, fn3) +% +% Inputs +% cmd - Use 'generate' or 'analyze' +% fn1 - File name for sine wave to generate or first record file name to analyze +% fn2 - File name to analyze 2nd +% fn3 - File name to analyze 3rd +% +% E.g. +% check_volume_levels('generate','sine.wav'); +% check_volume_levels('measure','rec1.wav','rec2.wav','rec3.wav'); + +% SPDX-License-Identifier: BSD-3-Clause +% Copyright(c) 2016 Intel Corporation. All rights reserved. +% Author: Seppo Ingalsuo + + addpath('../../sof/tools/test/audio/std_utils'); + addpath('../../sof/tools/test/audio/test_utils'); + + if exist('OCTAVE_VERSION', 'builtin') + pkg load signal + end + + switch lower(cmd) + case 'generate' + pass = generate(fn1); + if pass + fprintf(1, 'done\n'); + else + error('FAIL'); + end + case 'measure' + pass = measure(fn1, fn2, fn3); + if pass + fprintf(1, 'PASS\n'); + else + error('FAIL'); + end + otherwise + error('Invalid cmd') + endswitch + +end + +% Generate a 701 Hz and 1297 Hz -40 dBFS stereo sine wave +% to test volume gain and muting + +function pass = generate(fn) + fprintf('Create sine wave file %s\n', fn) + fs = 48e3; + f1 = 701; + f2 = 1297; + a = 10^(-40/20); + t = 60; + x1 = multitone(fs, f1, a, t); + x2 = multitone(fs, f2, a, t); + x = [x1'; x2']'; + sx = size(x); + d = (rand(sx(1), sx(2)) - 0.5)/2^15; + audiowrite(fn, x + d, fs); + pass = 1; +end + + +function pass = measure(fn1, fn2, fn3) + + % General test defaults + lm.tgrid = 5e-3; % Return level per every 5ms + lm.tlength = 10e-3; % Use 10 ms long measure window + lm.sine_freqs = [701 1297]; % The stimulus wav frequencies + lm.sine_dbfs = [-40 -40]; % The stimulus wav dBFS levels + + % Default gains for test 1 + v1 = [+10 0 -10 -30]; + v2 = [-10 +10 0 -20]; + vmax = +30; + vnom = 0; + vmut = -100; + vmin = -49; + vol_ch1 = [ vmax v1(1) vnom vmut vnom vmut vmut vmax vmut vmin v2(1) ]; + vol_ch2 = [ vmax v1(2) vnom vmut vnom vmut vmut vmax vmut vmin v2(2) ]; + t1.vctimes = [ 0 1 2 3 4 5 6 7 8 9 10 ]; + t1.volumes = [vol_ch1 ; vol_ch2 ]'; % Merge channels to matrix + t1.meas = [0.5 0.9]; % Measure levels 0.5s after transition until 0.9s + t1.vtol = 0.5; % Pass test with max +/- 0.5 dB mismatch + + % Check test 1 + pass1 = level_vs_time_checker(fn1, t1, lm, '1/3'); + + % Default gains for test 2 + m1 = [vmut vnom vnom vmut]; + m2 = [vnom vmut vmut vnom]; + vol_ch1 = [ v2(1) m1(1) vmut ]; + vol_ch2 = [ v2(2) m1(2) vmut ]; + t2.vctimes = [ 0 1 2 ]; + t2.volumes = [ vol_ch1 ; vol_ch2 ]'; % Merge channels to matrix + t2.meas = t1.meas; % Same as previous + t2.vtol = t1.vtol; % Same as previous + + % Check test 2 + pass2 = level_vs_time_checker(fn2, t2, lm, '2/3'); + + % Default gains for test 3 + vol_ch1 = [ vmut vmut m2(1) vnom ]; + vol_ch2 = [ vmut vmut m2(2) vnom ]; + t3.vctimes = [ 0 1 2 3 ]; + t3.volumes = [ vol_ch1 ; vol_ch2 ]'; + t3.meas = t1.meas; % Same as previous + t3.vtol = t1.vtol; % Same as previous + + % Check test 3 + pass3 = level_vs_time_checker(fn3, t3, lm, '3/3'); + + if pass1 == 1 && pass2 == 1 && pass3 == 1 + pass = 1; + else + pass = 0; + end + +end + +function pass = level_vs_time_checker(fn, tc, lm, id) + fprintf(1, 'File %s:\n', fn); + + lev = level_vs_time(fn, lm); + %plot_levels(lev, tc, lm); + pass = check_levels(lev, tc, lm, 1); + if pass + fprintf(1, 'pass (%s)\n', id); + else + fprintf(1, 'fail (%s)\n', id); + + % Swapped channels? + sine_freqs_orig = lm.sine_freqs; + lm.sine_freqs = sine_freqs_orig(end:-1:1); + lev = level_vs_time(fn, lm); + pass_test = check_levels(lev, tc, lm, 0); + if pass_test + fprintf(1,'Note: The test would pass with swapped channels.\n'); + return + end + + % Swapped controls? + lm.sine_freqs = sine_freqs_orig; + volumes_orig = tc.volumes; + tc.volumes = volumes_orig(:, end:-1:1); + lev = level_vs_time(fn, lm); + pass_test = check_levels(lev, tc, lm, 0); + if pass_test + fprintf(1,'Note: The test would pass with swapped controls.\n') + return + end + + % Swapped controls and swapped channels + lm.sine_freqs = sine_freqs_orig(end:-1:1); + lev = level_vs_time(fn, lm); + pass_test = check_levels(lev, tc, lm, 0); + if pass_test + fprintf(1,'Note: The test would pass with swapped controls and swapped channels.\n') + end + end +end + +function plot_levels(meas, tc, lm) + figure + plot(meas.t, meas.levels - lm.sine_dbfs); + grid on; + + sv = size(tc.volumes); + hold on; + for j = 1:sv(2) + for i = 1:sv(1) + plot([tc.vctimes(i)+tc.meas(1) tc.vctimes(i)+tc.meas(2)], ... + [tc.volumes(i,j)+tc.vtol tc.volumes(i,j)+tc.vtol], 'r--'); + if tc.volumes(i,j) > -100 + plot([tc.vctimes(i)+tc.meas(1) tc.vctimes(i)+tc.meas(2)], ... + [tc.volumes(i,j)-tc.vtol tc.volumes(i,j)-tc.vtol], 'r--'); + end + end + end + hold off; + xlabel('Time (s)'); + ylabel('Gain (dB)'); +end + +function pass = check_levels(meas, tc, lm, verbose) + pass = 1; + gains = meas.levels - lm.sine_dbfs; + sv = size(tc.volumes); + for j = 1:sv(2) + for i = 1:sv(1) + ts = tc.vctimes(i)+tc.meas(1); + te = tc.vctimes(i)+tc.meas(2); + idx0 = find(meas.t < te); + idx = find(meas.t(idx0) > ts); + avg_gain = mean(gains(idx, j)); + max_gain = tc.volumes(i,j) + tc.vtol; + min_gain = tc.volumes(i,j) - tc.vtol; + if avg_gain > max_gain + if verbose + fprintf(1, 'Channel %d Failed upper gain limit at ', j); + fprintf(1, '%4.1f - %4.1fs, gain %5.1f dB, max %5.1f dB\n', ... + ts, te, avg_gain, max_gain); + end + pass = 0; + end + if tc.volumes(i,j) > -100 + if avg_gain < min_gain + if verbose + fprintf(1, 'Channel %d failed lower gain limit at ', j); + fprintf(1, '%4.1f - %4.1fs, gain %5.1f dB, min %5.1f dB\n', ... + ts, te, avg_gain, min_gain); + end + pass = 0; + end + end + end + end +end + +function ret = level_vs_time(fn, lm) + [x, fs] = audioread(fn); + x = bandpass_filter(x, lm.sine_freqs, fs); + sx = size(x); + tclip = sx(1) / fs; + nch = sx(2); + + nlev = floor(tclip / lm.tgrid); + ngrid = lm.tgrid * fs; + nlength = lm.tlength * fs; + nmax = nlev - round(nlength / ngrid) + 1; + ret.t = (0:(nmax-1)) * lm.tgrid; + ret.levels = zeros(nmax, nch); + for i = 1:nmax + i1 = floor((i - 1) * ngrid + 1); + i2 = floor(i1 + nlength -1); + ret.levels(i, :) = level_dbfs(x(i1:i2, :)); + end + ret.levels_lin = 10.^(ret.levels/20); +end + +function y = bandpass_filter(x, f, fs) + sx = size(x); + y = zeros(sx(1), sx(2)); + c1 = 0.8; + c2 = 1/c1; + for j = 1:sx(2) + [b, a] = butter(4, 2*[c1*f(j) c2*f(j)]/fs); + y(:,j) = filter(b, a, x(:,j)); + end +end