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