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