1function [f, m_db] = mls_freq_resp(id)
2%% Measure frequency response with MLS test signal
3%
4%  [f, m] = mls_freq_resp(id)
5%
6%  Input parameters
7%  id - A string identifier for test case. An id 'selftest' is for special
8%       usage. It calculates response of filtered MLS signal and computes
9%       the measurement vs. known. Deviation is reported as error. It can
10%       be useful if the internal MLS measurement parameters are adjusted.
11%
12%  Output parameters
13%  f - Frequency vector in Hz
14%  m - Measured magnitude responses in dB
15%
16%  Configuration (edit these):
17%  mls_play_config.txt
18%  mls_rec_config.txt
19%
20%  The script will return also a text CSV format file with name mls-<id>.txt.
21%
22
23% SPDX-License-Identifier: BSD-3-Clause
24%
25% Copyright (c) 2018-2020, Intel Corporation. All rights reserved.
26%
27% Author: Seppo Ingalsuo <seppo.ingalsuo@linux.intel.com>
28
29%% Settings
30np = 1024;                  % Number of frequency points to use
31f_lo = 100;                 % Lower frequency limit for analysis
32f_hi = 20e3;                % Upper frequency limit for analysis
33t_tot = 10e-3;              % MLS analysis window length in s
34t_mls_s = 1.0;              % MLS test signal length in s
35a_mls_db = -10;             % MLS test signal amplitude in dB
36fs = 48e3;                  % Sample rate in Hz
37bits = 16;                  % Audio format to use (bits)
38fmt = 'S16_LE';             % Audio format to use (ALSA)
39dir = '/tmp';               % Directory for temporary files
40capture_level_max_db = -1;  % Expected max. level
41capture_level_min_db = -30; % Expacted min. level
42
43%% Get device identifier to use
44if nargin < 1
45	id = 'unknown';
46end
47
48if strcmp(id, 'selftest')
49	selftest = 1;
50	% Just some simulated speaker response to use as self test case
51	stb = [  0.341762453, -0.915611126,  0.482465118, ...
52	         1.017612317, -1.722527013,  0.711608745, ...
53	         0.630608859, -0.813609935,  0.267690582, ];
54	sta = [  1.000000000, -3.931695128,  6.630812276, ...
55	        -6.339248735,  3.800407709, -1.559376698, ...
56	         0.619626250, -0.317702349,  0.097453451, ];
57else
58	selftest = 0;
59end
60measfn = sprintf('mls-%s.wav', id);
61csvfn = sprintf('mls-%s.txt', id);
62
63%% Paths
64addpath('../../test/audio/test_utils');
65
66%% MLS
67n_mls = round(fs*t_mls_s);
68mls = 10^(a_mls_db/20) * (2 * mlsp12(1, n_mls) - 1);
69mlsfn = 'mls-ref.wav';
70audiowrite(mlsfn, mls, fs);
71
72%% Chip markers and parameters for find sync
73[x1, m1] = sync_chirp(fs, 'up');
74[x2, m2] = sync_chirp(fs, 'down');
75fnd.fs = fs; % Sample rate
76fnd.sm = 5; % Max seek from start
77fnd.em = 3; % Max seek from end
78fnd.idle_t = 2; % max idle in start or end
79fnd.mark_t = m1.t; % Marker length
80fnd.nf = 1; % One signal (amplitude)
81fnd.na = 1; % One signal (frequency)
82fnd.tl = t_mls_s; % Length of signal
83fnd.mt = 0.1; % Threshold length to issue error
84fnd.is = 0; % Ignore from start
85fnd.ie = 0; % Ignore from end
86
87%% Merge markers and MLS
88z = zeros(n_mls + m1.n + m2.n, 1);
89i1 = m1.n + 1;
90i2 = m1.n + n_mls;
91z(1:i1 - 1) = x1;
92z(i1:i2) = mls;
93z(i2 + 1:end) = x2;
94
95%% Get config
96rec_cfg = meas_remote_rec_config(fs, fmt);
97play_cfg = meas_remote_play_config;
98
99%% Capture MLS from all playback channel at time
100mixfn = 'mlsmix.wav';
101recfn = 'recch.wav';
102y = [];
103for i=1:play_cfg.nch
104	fprintf('\n');
105	fprintf('Measure playback channel %d\n', i);
106	if selftest
107		tz =zeros(2*fs+length(z), 1); % Pad 2s
108		tz(fs:fs+length(z)-1) = z;
109	        t = filter(stb, sta, tz); % Filter with test response
110		r = t(:) * ones(1, rec_cfg.nch); % Copy to all channels
111	else
112		x = zeros(length(z), play_cfg.nch);
113		x(:,i) = z;
114		mixdfn = sprintf('%s/%s', dir, mixfn);
115		audiowrite(mixdfn, x, fs, 'BitsPerSample', bits);
116		copy_playback(mixdfn, play_cfg);
117		tcap = floor(6 + t_mls_s); % Capture for MLS +6s
118		remote_capture(recfn, rec_cfg, tcap);
119		pause(1);
120		remote_play(mixfn, play_cfg);
121		pause(3);
122		r = get_recording(recfn, rec_cfg);
123	end
124	[d, nt] = find_test_signal(r(:,1), fnd);
125	if isempty(d)
126		figure;
127		sr = size(r);
128		ts = (0:sr(1)-1)/fs;
129		plot(ts, r(:,1));
130		grid on;
131		xlabel('Time (s)');
132		ylabel('Sample value');
133		title('Captured audio test waveform');
134		fprintf(1, 'Error: check the plot for skew in capture/playback.\n');
135		f = [];
136		m_db = [];
137		return
138	end
139	for j = 1:rec_cfg.nch
140		y(:, rec_cfg.nch*(i-1) + j) = r(d:d + nt -1, j);
141	end
142	m = 20*log10(max(abs(r)));
143	fprintf('Peak levels for capture channels (dB):');
144	for j = 1:rec_cfg.nch
145		fprintf(' %5.1f', m(j));
146	end
147	fprintf('\n');
148	if max(m) > capture_level_max_db
149		fprintf('Warning: The recording level is too loud.\n');
150	end
151	if min(m) < capture_level_min_db
152		fprintf('Warning: The recording level is too silent.\n');
153	end
154
155end
156audiowrite(measfn, y, fs, 'BitsPerSample', bits);
157fprintf('\n');
158fprintf('Done.\n');
159
160[f, m_db,  b] = mls_calc_resp(csvfn, mlsfn, measfn, t_tot, np, f_lo, f_hi);
161
162[f, m_db] = apply_mic_calibration(f, m_db, rec_cfg);
163
164figure
165idx = find(f>1e3, 1, 'first') - 1;
166m_db_align = m_db - m_db(idx);
167semilogx(f, m_db_align);
168ax=axis(); axis([f_lo f_hi ax(3:4)]);
169grid on;
170xlabel('Frequency (Hz)');
171ylabel('Magnitude (dB)');
172
173if selftest
174	title('Measured vs. reference response');
175	h = freqz(stb, sta, f, fs);
176        ref_db = 20*log10(abs(h));
177        ref_db_align = ref_db - ref_db(idx);
178	hold on;
179	plot(f, ref_db_align, 'r--');
180	hold off;
181        idx = find(f < f_hi);
182        idx = find(f(idx) > f_lo);
183        m_lin = 10.^(m_db_align(idx)/20);
184        ref_lin = 10.^(ref_db_align(idx)/20);
185        err_lin = m_lin - ref_lin;
186        e_rms = sqrt(mean(err_lin.^2));
187        e_db = 20*log10(e_rms);
188	figure;
189	semilogx(f(idx), 20*log10(abs(err_lin)))
190	grid on;
191	xlabel('Frequency (Hz)');
192	ylabel('Magnitude (dB)');
193	title('Observed Error in self test');
194        if e_db < -30
195                fprintf('Passed self test. ');
196        else
197                fprintf('Failed self test. ');
198        end
199        fprintf('Response RMS error is %4.1f dB.\n', e_db);
200end
201
202end
203
204function copy_playback(fn, cfg)
205	if cfg.ssh
206		cmd = sprintf('scp %s %s:%s/', fn, cfg.user, cfg.dir);
207		fprintf('Remote copy: %s\n', cmd);
208		system(cmd);
209	else
210		%cmd = sprintf('cp %s %s/', fn, cfg.dir);
211		%fprintf('Local copy: %s\n', cmd);
212	end
213end
214
215function y = get_recording(fn, cfg)
216	if cfg.ssh
217		cmd = sprintf('scp %s:%s/%s %s', cfg.user, cfg.dir, fn, fn);
218		fprintf('Remote copy: %s\n', cmd);
219	else
220		cmd = sprintf('cp %s/%s %s', cfg.dir, fn, fn);
221		fprintf('Local copy: %s\n', cmd);
222	end
223	system(cmd);
224	y = audioread(fn);
225	delete(fn);
226end
227
228function remote_play(fn, cfg)
229	if cfg.ssh
230		cmd = sprintf('ssh %s aplay -D%s %s/%s', cfg.user, cfg.dev, cfg.dir, fn);
231		fprintf('Remote play: %s\n', cmd);
232	else
233		cmd = sprintf('aplay -D%s %s/%s', cfg.dev, cfg.dir, fn);
234		fprintf('Local play: %s\n', cmd);
235	end
236	system(cmd);
237end
238
239function remote_capture(fn, cfg, t)
240	if cfg.ssh
241		cmd = sprintf('ssh %s arecord -q -D%s %s -d %d %s/%s &', ...
242			      cfg.user, cfg.dev, cfg.fmt, t, cfg.dir, fn);
243		fprintf('Remote capture: %s\n', cmd);
244	else
245		cmd = sprintf('arecord -q -D%s %s -d %d %s/%s &', ...
246			      cfg.dev, cfg.fmt, t, cfg.dir, fn);
247		fprintf('Local capture: %s\n', cmd);
248	end
249	system(cmd);
250end
251
252function play = meas_remote_play_config()
253	source mls_play_config.txt;
254	fprintf('\nThe setttings for remote playback are\n');
255	fprintf('Use ssh   : %d\n', play.ssh);
256	fprintf('User      : %s\n', play.user);
257	fprintf('Directory : %s\n', play.dir);
258	fprintf('Device    : %s\n', play.dev);
259	fprintf('Channels  : %d\n', play.nch);
260end
261
262function rec = meas_remote_rec_config(fs, fmt)
263	source mls_rec_config.txt;
264	rec.fmt = sprintf('-t wav -c %d -f %s -r %d', ...
265			     rec.nch, fmt, fs);
266
267	fprintf('\nThe setttings for remote capture are\n');
268	fprintf('Use ssh          : %d\n', rec.ssh);
269	fprintf('User             : %s\n', rec.user);
270	fprintf('Directory        : %s\n', rec.dir);
271	fprintf('Device           : %s\n', rec.dev);
272	fprintf('format           : %s\n', rec.fmt);
273	fprintf('Channels         : %d\n', rec.nch);
274	fprintf('Calibration Data : %s\n', rec.cal);
275
276	if ~isempty(rec.cal)
277		if exist(rec.cal, 'file')
278			[rec.cf, rec.cm, rec.sens, rec.cs] = get_calibration(rec.cal);
279		else
280			error('The calibration file does not exist');
281		end
282	else
283		rec.cf = [];
284		rec.cm = [];
285		rec.sens = [];
286		rec.cs = '';
287	end
288end
289
290% The syntax of ASCII text calibration data is such that first line is
291% text string sometimes within "" or text line about Sensitivity. Such
292% lines are read and stored to description text. The sensitivty is extracted
293% and printed if success but currently it is not utilized by the code.
294%
295% The next lines are <frequency Hz> <magnitude decibels> for
296% measurement data for microphone. If there are more than two numbers
297% per line the other than two first columns are ignored. This code
298% applies inverse of calibration data for measured response calibration.
299
300function [f, m, sens, desc] = get_calibration(fn)
301	fh = fopen(fn, 'r');
302	if fh < 0
303		error('Cannot open calibration data file');
304	end
305	n = 1;
306	f = [];
307	m = [];
308	sens =[];
309	desc = '';
310	str = fgets(fh);
311	idx = findstr(str, '"');
312	while length(idx) > 0
313		line = str(idx(1)+1:idx(2)-1);
314		desc = sprintf('%s%s ', desc, line);
315		str = fgets(fh);
316		idx = findstr(str, '"');
317	end
318	if length(strfind(str, 'Sens'))
319		desc = str;
320		str = fgets(fh);
321	end
322	while str ~= -1
323		d=sscanf(str,'%f');
324		f(n) = d(1);
325		m(n) = d(2);
326		n = n + 1;
327		str = fgets(fh);
328	end
329
330	% Strip possible linefeed from description end
331	if double(desc(end)) == 10
332		desc = desc(1:end-1);
333	end
334	fprintf('Calibration Info : %s\n', desc);
335	i1 = strfind(desc, 'Sens Factor =');
336	i2 = strfind(desc, 'dB');
337	if length(i1) == 1 && length(i2) == 1
338		sens = sscanf(desc(i1+13:i2-1), '%f');
339		fprintf('Calibration Sens : %6.2f dB\n', sens);
340	end
341
342	fprintf('Calibration range: %.2f .. %.2f Hz\n', min(f), max(f));
343	fprintf('Calibration range: %.2f .. %.2f dB\n', min(m), max(m));
344
345	figure;
346	semilogx(f, m);
347	grid on;
348	title(desc);
349	xlabel('Frequency (Hz)');
350	ylabel('Magnitude (dB)');
351end
352
353function [x, seed] = mlsp12(seed, n)
354
355% Based on book Numerical Recipes in C, chapter 7.4 Generation of
356% random bits method II p. 298-299 example and 12 bit primitive
357% polynomial (12, 6, 4, 1, 0)
358
359	x = zeros(1,n);
360	ib1 = 2^(1-1);
361	ib4 = 2^(4-1);
362	ib6 = 2^(6-1);
363	ib12 = 2^(12-1);
364	mask = ib1 + ib4 + ib6 + ib12;
365
366	for i = 1:n
367		if bitand(seed, ib12)
368			seed = bitor(bitxor(seed, mask) * 2 , ib1);
369			x(i) = 1;
370		else
371			seed = seed * 2;
372			x(i) = 0;
373		end
374	end
375end
376
377%% Calibration apply function
378%  Resample microphone calibration data into used grid and
379%  then subtract calibration response from measured
380%  response.
381
382function [cal_f, cal_m_db] = apply_mic_calibration(f, m_db, rec)
383
384	if length(rec.cm) > 0
385		if ~isvector(rec.cm)
386			error('Calibration can be for one channel only');
387		end
388		mic_m_db = interp1(rec.cf, rec.cm, f, 'linear');
389		nans = isnan(mic_m_db);
390		idx = find(nans == 0);
391		cal_f = f(idx);
392		cal_db = mic_m_db(idx);
393		s = size(m_db);
394		for i = 1:s(2)
395			cal_m_db(:,i) = m_db(idx,i) - cal_db(:);
396		end
397	else
398		cal_m_db = m_db;
399		cal_f = f;
400	end
401
402end
403