1% process_test - test objective audio quality parameters 2% 3% process_test(comp, bits_in_list, bits_out_list, fs) 4 5% SPDX-License-Identifier: BSD-3-Clause 6% Copyright(c) 2017-2022 Intel Corporation. All rights reserved. 7% Author: Seppo Ingalsuo <seppo.ingalsuo@linux.intel.com> 8 9function [n_fail, n_pass, n_na] = process_test(comp, bits_in_list, bits_out_list, fs, fulltest) 10 %% Defaults for call parameters 11 if nargin < 1 12 comp = 'EQIIR'; 13 end 14 15 if nargin < 2 16 bits_in_list = 32; 17 end 18 19 if nargin < 3 20 bits_out_list = 32; 21 end 22 23 if nargin < 4 24 fs = 48e3; 25 end 26 27 if nargin < 5 28 fulltest = 1; 29 end 30 31 %% Paths 32 t.blobpath = '../../topology/topology1/m4'; 33 plots = 'plots'; 34 reports = 'reports'; 35 36 %% Defaults for test 37 t.comp = comp; % Pass component name from func arguments 38 t.fmt = 'raw'; % Can be 'raw' (fast binary) or 'txt' (debug) 39 t.iirblob = 'eq_iir_coef_loudness.m4'; % Use loudness type response 40 t.firblob = 'eq_fir_coef_loudness.m4'; % Use loudness type response 41 t.fs = fs; % Sample rate from func arguments 42 t.nch = 2; % Number of channels 43 t.ch = [1 2]; % Test channel 1 and 2 44 t.bits_in = bits_in_list; % Input word length from func arguments 45 t.bits_out = bits_out_list; % Output word length from func arguments 46 t.full_test = fulltest; % 0 is quick check only, 1 is full test 47 48 %% Show graphics or not. With visible plot windows Octave may freeze if too 49 % many windows are kept open. As workaround setting close windows to 50 % 1 shows only flashing windows while the test proceeds. With 51 % visibility set to 0 only console text is seen. The plots are 52 % exported into plots directory in png format and can be viewed from 53 % there. 54 t.plot_close_windows = 1; % Workaround for visible windows if Octave hangs 55 t.plot_visible = 'off'; % Use off for batch tests and on for interactive 56 t.files_delete = 1; % Set to 0 to inspect the audio data files 57 58 %% Prepare 59 addpath('std_utils'); 60 addpath('test_utils'); 61 addpath('../../tune/eq'); 62 mkdir_check(plots); 63 mkdir_check(reports); 64 n_meas = 5; 65 n_bits_in = length(bits_in_list); 66 n_bits_out = length(bits_out_list); 67 r.bits_in_list = bits_in_list; 68 r.bits_out_list = bits_out_list; 69 r.g = NaN(n_bits_in, n_bits_out); 70 r.dr = NaN(n_bits_in, n_bits_out); 71 r.thdnf = NaN(n_bits_in, n_bits_out); 72 r.pf = -ones(n_bits_in, n_bits_out, n_meas); 73 r.n_fail = 0; 74 r.n_pass = 0; 75 r.n_skipped = 0; 76 r.n_na = 0; 77 78 %% Loop all modes to test 79 for b = 1:n_bits_out 80 for a = 1:n_bits_in 81 v = -ones(n_meas,1); % Set pass/fail test verdict to not executed 82 tn = 1; 83 t.bits_in = bits_in_list(a); 84 t.bits_out = bits_out_list(b); 85 86 v(1) = chirp_test(t); 87 if v(1) ~= -1 && t.full_test 88 [v(2), g] = g_test(t); 89 [v(3), dr] = dr_test(t); 90 [v(4), thdnf] = thdnf_test(t); 91 v(5) = fr_test(t); 92 93 % TODO: Collect results for all channels, now get worst-case 94 r.g(a, b) = g(1); 95 r.dr(a, b) = min(dr); 96 r.thdnf(a, b) = max(thdnf); 97 r.pf(a, b, :) = v; 98 end 99 100 %% Done, store pass/fail 101 if v(1) ~= -1 102 idx = find(v > 0); 103 r.n_fail = r.n_fail + length(idx); 104 idx = find(v == 0); 105 r.n_pass = r.n_pass + length(idx); 106 idx = find(v == -1); 107 r.n_skipped = r.n_skipped + length(idx); 108 idx = find(v == -2); 109 r.n_na = r.n_na + length(idx); 110 end 111 end 112 end 113 114 %% FIXME: get unique string to keep all the incremental logs, like datetime string. 115 %% For now bitspersample is differentiator. 116 117 %% Print table with test summary: Gain 118 fn = sprintf('%s/g_%s_%d.txt', reports, t.comp, t.bits_in); 119 print_val(t.comp, 'Gain (dB)', fn, bits_in_list, bits_out_list, r.g, r.pf); 120 121 %% Print table with test summary: DR 122 fn = sprintf('%s/dr_%s_%d.txt', reports, t.comp, t.bits_in); 123 print_val(t.comp, 'Dynamic range (dB CCIR-RMS)', fn, bits_in_list, bits_out_list, r.dr, r.pf); 124 125 %% Print table with test summary: THD+N vs. frequency 126 fn = sprintf('%s/thdnf_%s_%d.txt', reports, t.comp, t.bits_in); 127 print_val(t.comp, 'Worst-case THD+N vs. frequency', fn, bits_in_list, bits_out_list, r.thdnf, r.pf); 128 129 130 %% Print table with test summary: pass/fail 131 fn = sprintf('%s/pf_%s_%d.txt', reports, t.comp, t.bits_in); 132 print_pf(t.comp', fn, bits_in_list, bits_out_list, r.pf, 'Fails chirp/gain/DR/THD+N/FR'); 133 134 fprintf('\n'); 135 fprintf('============================================================\n'); 136 fprintf('Number of passed tests = %d\n', r.n_pass); 137 fprintf('Number of failed tests = %d\n', r.n_fail); 138 fprintf('Number of non-applicable tests = %d\n', r.n_na); 139 fprintf('Number of skipped tests = %d\n', r.n_skipped); 140 fprintf('============================================================\n'); 141 142 n_fail = r.n_fail; 143 n_pass = r.n_pass; 144 n_na = r.n_na; 145 if r.n_fail < 1 && r.n_pass < 1 146 fprintf('\nERROR: No test results.\n'); 147 n_fail = 1; 148 elseif r.n_fail > 0 149 fprintf('\nERROR: TEST FAILED!!!\n'); 150 else 151 fprintf('\nTest passed.\n'); 152 end 153 154end 155 156%% ------------------------------------------------------------ 157%% Test execution with help of common functions 158%% 159 160function fail = chirp_test(t) 161 fprintf('Spectrogram test %d Hz ...\n', t.fs); 162 163 % Create input file 164 test = test_defaults(t); 165 test = chirp_test_input(test); 166 167 % Run test 168 test = test_run_process(test); 169 170 % Analyze 171 test = chirp_test_analyze(test); 172 test_result_print(t, 'Continuous frequency sweep', 'chirpf', test); 173 174 % Delete files unless e.g. debugging and need data to run 175 delete_check(t.files_delete, test.fn_in); 176 delete_check(t.files_delete, test.fn_out); 177 178 fail = test.fail; 179end 180 181 182%% Reference: AES17 6.2.2 Gain 183function [fail, g_db] = g_test(t) 184 test = test_defaults(t); 185 186 % Create input file 187 test = g_test_input(test); 188 189 % Run test 190 test = test_run_process(test); 191 192 % Measure 193 test = g_spec(test, t); 194 test = g_test_measure(test); 195 196 % Get output parameters 197 fail = test.fail; 198 g_db = test.g_db; 199 delete_check(t.files_delete, test.fn_in); 200 delete_check(t.files_delete, test.fn_out); 201end 202 203%% Reference: AES17 6.4.1 Dynamic range 204function [fail, dr_db] = dr_test(t) 205 test = test_defaults(t); 206 207 % Create input file 208 test = dr_test_input(test); 209 210 % Run test 211 test = test_run_process(test); 212 213 % Measure 214 test = dr_test_measure(test); 215 216 % Get output parameters 217 fail = test.fail; 218 dr_db = test.dr_db; 219 delete_check(t.files_delete, test.fn_in); 220 delete_check(t.files_delete, test.fn_out); 221end 222 223%% Reference: AES17 6.3.2 THD+N ratio vs. frequency 224function [fail, thdnf] = thdnf_test(t) 225 test = test_defaults(t); 226 227 % Create input file 228 test = thdnf_test_input(test); 229 230 % Run test 231 test = test_run_process(test); 232 233 % Measure 234 test = thdnf_mask(test); 235 test = thdnf_test_measure(test); 236 237 % For EQ use the -20dBFS result and ignore possible -1 dBFS fail 238 thdnf = max(test.thdnf_low); 239 fail = test.fail; 240 delete_check(t.files_delete, test.fn_in); 241 delete_check(t.files_delete, test.fn_out); 242 243 % Print 244 test_result_print(t, 'THD+N ratio vs. frequency', 'THDNF', test); 245end 246 247%% Reference: AES17 6.2.3 Frequency response 248function fail = fr_test(t) 249 test = test_defaults(t); 250 251 % Create input file 252 test = fr_test_input(test); 253 254 % Run test 255 test = test_run_process(test); 256 257 % Measure 258 test = fr_mask(test, t); 259 test = fr_test_measure(test); 260 fail = test.fail; 261 delete_check(t.files_delete, test.fn_in); 262 delete_check(t.files_delete, test.fn_out); 263 264 % Print 265 test_result_print(t, 'Frequency response', 'FR', test); 266end 267 268%% ------------------------------------------------------------ 269%% Helper functions 270 271function test = thdnf_mask(test) 272 min_bits = min(test.bits_in, test.bits_out); 273 test.thdnf_mask_f = [50 400 test.f_max]; 274 test.thdnf_mask_hi = [-40 -50 -50]; 275end 276 277function test = g_spec(test, prm) 278 switch lower(test.comp) 279 case 'eq-iir' 280 blob = fullfile(prm.blobpath, prm.iirblob); 281 h = eq_blob_plot(blob, 'iir', test.fs, test.f, 0); 282 case 'eq-fir' 283 blob = fullfile(prm.blobpath, prm.firblob); 284 h = eq_blob_plot(blob, 'fir', test.fs, test.f, 0); 285 otherwise 286 test.g_db_expect = zeros(1, test.nch); 287 return 288 end 289 290 test.g_db_expect = h.m(:, test.ch); 291end 292 293function test = fr_mask(test, prm) 294 switch lower(test.comp) 295 case 'eq-iir' 296 blob = fullfile(prm.blobpath, prm.iirblob); 297 h = eq_blob_plot(blob, 'iir', test.fs, test.f, 0); 298 case 'eq-fir' 299 blob = fullfile(prm.blobpath, prm.firblob); 300 h = eq_blob_plot(blob, 'fir', test.fs, test.f, 0); 301 otherwise 302 % Define a generic mask for frequency response, generally 303 % all processing at 8 kHz or above should pass, if not 304 % or need for tighter criteria define per component other 305 % target. 306 test.fr_mask_fhi = [20 test.f_max]; 307 test.fr_mask_flo = [200 400 3500 3600 ]; 308 for i = 1:test.nch 309 test.fr_mask_mhi(:,i) = [ 1 1 ]; 310 test.fr_mask_mlo(:,i) = [-10 -1 -1 -10]; 311 end 312 return 313 end 314 315 % Create mask from theoretical frequency response calculated from decoded 316 % response in h and align mask to be relative to 997 Hz response 317 i997 = find(test.f > 997, 1, 'first')-1; 318 j = 1; 319 for channel = test.ch 320 m = h.m(:, channel) - h.m(i997, channel); 321 test.fr_mask_flo = test.f; 322 test.fr_mask_fhi = test.f; 323 test.fr_mask_mlo(:,j) = m - test.fr_rp_max_db; 324 test.fr_mask_mhi(:,j) = m + test.fr_rp_max_db; 325 j = j + 1; 326 end 327end 328 329function test = test_defaults(t) 330 test.comp = t.comp; 331 test.fmt = t.fmt; 332 test.bits_in = t.bits_in; 333 test.bits_out = t.bits_out; 334 test.nch = t.nch; 335 test.ch = t.ch; 336 test.fs = t.fs; 337 test.plot_visible = t.plot_visible; 338 339 % Misc 340 test.quick = 0; 341 test.att_rec_db = 0; 342 343 % Plotting 344 test.plot_channels_combine = 1; 345 test.plot_thdn_axis = []; 346 test.plot_fr_axis = []; 347 test.plot_passband_zoom = 0; 348 349 % Test constraints 350 test.f_start = 20; 351 test.f_end = test.fs * 0.41667; % 20 kHz @ 48 kHz 352 test.fu = test.fs * 0.41667; % 20 kHz @ 48 kHz 353 test.f_max = 0.999*t.fs/2; % Measure up to min. Nyquist frequency 354 test.fs1 = test.fs; 355 test.fs2 = test.fs; 356 357 % Pass criteria 358 test.g_db_tol = 0.1; % Allow 0.1 dB gain variation 359 test.thdnf_max = []; % Set per component 360 test.dr_db_min = 80; % Min. DR 361 test.fr_rp_max_db = 0.5; % Allow 0.5 dB frequency response ripple 362end 363 364function test = test_run_process(test) 365 delete_check(1, test.fn_out); 366 test = test_run(test); 367end 368 369function test_result_print(t, testverbose, testacronym, test) 370 tstr = sprintf('%s %s %d-%d %d Hz', ... 371 testverbose, t.comp, t.bits_in, t.bits_out, t.fs); 372 373 %% FIXME: get unique string to keep all the incremental logs 374 375 for i = 1:length(test.ph) 376 title(test.ph(i), tstr); 377 end 378 379 for i = 1:length(test.fh) 380 figure(test.fh(i)); 381 set(test.fh(i), 'visible', test.plot_visible); 382 pfn = sprintf('plots/%s_%s_%d_%d_%d_%d.png', ... 383 testacronym, t.comp, ... 384 t.bits_in, t.bits_out, t.fs, i); 385 print(pfn, '-dpng'); 386 end 387end 388