From 37d7933948b06ce56fe0c985a54273e84f9fd65e Mon Sep 17 00:00:00 2001 From: Seppo Ingalsuo Date: Tue, 18 Oct 2022 21:07:16 +0300 Subject: [PATCH] [SKIP CI] Test-case: Test if acoustic capture of ambient is normal Capture sound clip from microphone endpoint and automatically analyze if recording has normal characteristic. Detected issues are only PCM zero code or DC constant DC values, large offset, duplicated channels, saturated PCM code values, or abnormal FFT spectrum. Signed-off-by: Seppo Ingalsuo --- test-case/noise_capture_check.m | 148 +++++++++++++++++++++++++++++++ test-case/noise_capture_check.sh | 23 +++++ 2 files changed, 171 insertions(+) create mode 100644 test-case/noise_capture_check.m create mode 100755 test-case/noise_capture_check.sh diff --git a/test-case/noise_capture_check.m b/test-case/noise_capture_check.m new file mode 100644 index 00000000..dd497e04 --- /dev/null +++ b/test-case/noise_capture_check.m @@ -0,0 +1,148 @@ +function noise_capture_check(fn) + +t_min = 2.0; % Min 2.0s long clip +t_skip = 1.0; % Trim out first 1.0s +n_fft = 1024; % 1024 bits FFT +clipped_db = -0.01; % Max. PCM code value in dB +max_offset_db = -80; % Max. DC offset in dB +mask.low_f = [ 0 1000 5000 7000 96000]; +mask.low_db = [-160 -180 -200 -250 -250]; +mask.high_f = [ 0 7000 24000 96000]; +mask.high_db = [ -20 -30 -50 -60]; + +%% Octave packages, skip for Matlab +if exist('OCTAVE_VERSION', 'builtin') + pkg load signal; +end + +%% Get audio recording +[x0, fs] = audioread(fn); +n_samples = size(x0, 1); +n_channels = size(x0, 2); +t_clip = n_samples / fs; +if t_clip < t_min + fprintf(1, 'Error: The clip needs to be at least %.1fs long\n', t_min); + fail_check(1); +end + +%% Trim sample, skip the beginning +x = x0(t_skip * fs + 1:end,:); +n_samples = size(x, 1); + +%% Check that all channels contain signal, fail for zero or DC value +failed = 0; +for i = 1:n_channels + abs_max_diff = max(abs(diff(x(:,i)))); + if abs_max_diff < eps + fprintf(1, 'Error: Channel %d does not contain signal\n', i); + failed = 1; + end +end +fail_check(failed); + +%% Check for excessive offset +failed = 0; +for i = 1:n_channels + offset = 20*log10(abs(mean(x(:,i)))); + if offset > max_offset_db + fprintf(1, 'Error: Channel %d offset %5.2f is too high\n', i, offset); + failed = 1; + end +end +fail_check(failed); + +%% Check that channels are unique +failed = 0; +for i = 1:n_channels + for j = 1:n_channels + if i == j + continue + end + delta = x(:,i) - x(:,j); + max_abs_delta = max(abs(delta)); + if max_abs_delta < eps + fprintf(1, 'Error: Channels %d and %d are identical\n', i, j); + failed = 1; + end + end +end +fail_check(failed); + +%% Check that PCM code values are not saturated +failed = 0; +for i = 1:n_channels + if max(abs(x(:,i))) > 10^(clipped_db/20) + fprintf(1, 'Error: Channel %d contains clipped samples\n', i); + failed = 1; + end +end +fail_check(failed); + +%% Check for normal spectra shape +failed = 0; +for i = 1:n_channels + if fft_check_channel(x, i, fs, mask) + failed = 1; + end +end +fail_check(failed); + + +end + +%% Helper functions + +function fail_check(fail) + +if fail + quit(1); +end + +end + +function failed = fft_check_channel(x, ch, fs, mask) + +failed = 0; +do_plot = 0; +[f, mdb] = get_fft(x(:,ch), fs); +mask_low = interp1(mask.low_f, mask.low_db, f)'; +mask_high = interp1(mask.high_f, mask.high_db, f)'; +if do_plot + figure + plot(f, mdb); + hold on + plot(f, mask_low, '--', f, mask_high, '--'); + hold off + grid on; +end + +idx = find(mdb < mask_low); +if ~isempty(idx) + f_min = f(idx(1)); + f_max = f(idx(end)); + fprintf(1, 'Error: Channel %d is below mask in %.0f - %0.f Hz\n', ... + ch, f_min, f_max); + failed = 1; +end +idx = find(mdb > mask_high); +if ~isempty(idx) + f_min = f(idx(1)); + f_max = f(idx(end)); + fprintf(1, 'Error: Channel %d is above mask in %.0f - %0.f Hz\n', ... + ch, f_min, f_max); + failed = 1; +end + +end + +function [f, mdb] = get_fft(x, fs) + +n_fft = 8192; +win = kaiser(n_fft, 20); +idx = length(x) - n_fft + 1; +s = fft(x(idx:end) .* win) / n_fft; +s = s(1:n_fft/2 + 1); +f = linspace(0, fs/2, length(s)); +mdb = 20*log10(abs(s)); + +end diff --git a/test-case/noise_capture_check.sh b/test-case/noise_capture_check.sh new file mode 100755 index 00000000..e13d149d --- /dev/null +++ b/test-case/noise_capture_check.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e + +check_device_format () { + DEV=$1 + FMT=$2 + WAV=$(mktemp).wav + OCTAVE_SCRIPT="noise_capture_check('${WAV}')" + echo Testing "$DEV" + rm -f "$WAV" + arecord -D${DEV} $FMT -d 2 "$WAV" + octave --silent --no-gui --eval "$OCTAVE_SCRIPT" + status=$? + [ $status -eq 0 ] && echo Passed. + rm -f "$WAV" +} + +check_device_format "hw:1,0" "-c 2 -r 48000 -f S16_LE" +check_device_format "hw:1,6" "-c 2 -r 48000 -f S16_LE" +check_device_format "hw:1,6" "-c 2 -r 48000 -f S32_LE" +check_device_format "hw:1,7" "-c 2 -r 16000 -f S16_LE" +check_device_format "hw:1,7" "-c 2 -r 16000 -f S32_LE"