1% bf = bf_design(bf)
2%
3% This script calculates beamformer filters with superdirective design
4% criteria.
5
6% SPDX-License-Identifier: BSD-3-Clause
7%
8% Copyright (c) 2020, Intel Corporation. All rights reserved.
9%
10% Author: Seppo Ingalsuo <seppo.ingalsuo@linux.intel.com>
11
12function bf = bf_design(bf)
13
14addpath('../../test/audio/test_utils');
15addpath('../../test/audio/std_utils');
16mkdir_check('plots');
17mkdir_check('data');
18
19if length(bf.steer_az) ~= length(bf.steer_el)
20	error('The steer_az and steer_el lengths need to be equal.');
21end
22
23if length(find(bf.steer_az > 180)) || length(find(bf.steer_az < -180))
24	error('The steer_az angles need to be -180 to +180 degrees');
25end
26
27if length(find(bf.steer_el > 90)) || length(find(bf.steer_el < -90))
28	error('The steer_el angles need to be -90 to +90 degrees');
29end
30
31switch lower(bf.array)
32	case 'line'
33		bf = bf_array_line(bf);
34	case 'circular'
35		bf = bf_array_circ(bf);
36	case 'rectangle'
37		bf = bf_array_rect(bf);
38	case 'lshape'
39		bf = bf_array_lshape(bf);
40	case 'xyz'
41		bf = bf_array_xyz(bf);
42	otherwise
43		error('Invalid array type')
44end
45
46if isempty(bf.num_filters)
47	if isempty(bf.input_channel_select)
48		bf.num_filters = bf.mic_n;
49	else
50		bf.num_filters = length(bf.input_channel_select);
51	end
52end
53
54bf = bf_array_rot(bf);
55
56% The design function handles only single (az, el) value, so need to
57% loop every steer angle.
58bf.num_angles = length(bf.steer_az);
59all_az = bf.steer_az;
60all_el = bf.steer_el;
61all_array_id = bf.array_id;
62all_sinerot_fn = bf.sinerot_fn;
63all_diffuse_fn = bf.diffuse_fn;
64all_random_fn = bf.random_fn;
65all_mat_fn = bf.mat_fn;
66
67for n = 1:bf.num_angles
68	bf.steer_az = all_az(n);
69	bf.steer_el = all_el(n);
70	bf.array_id = all_array_id{n};
71	bf.sinerot_fn = all_sinerot_fn{n};
72	bf.diffuse_fn = all_diffuse_fn{n};
73	bf.random_fn = all_random_fn{n};
74	bf.mat_fn = all_mat_fn{n};
75	bf = bf_one_design(bf);
76	w_all(:,:,n) = bf.w;
77end
78
79bf.steer_az = all_az;
80bf.steer_el = all_el;
81bf.array_id = all_array_id;
82bf.sinerot_fn = all_sinerot_fn;
83bf.diffuse_fn = all_diffuse_fn;
84bf.random_fn = all_random_fn;
85bf.mat_fn = all_mat_fn;
86bf.w = w_all;
87
88end
89
90function bf = bf_one_design(bf)
91
92
93%% Defaults
94j = complex(0,-1);
95fs = bf.fs;
96N = 1024;
97N_half = N/2+1;
98f = (0:N/2)*fs/N';
99phi_rad = (-180:180)*pi/180;
100phi_rad = phi_rad(1:end-1);
101n_phi = length(phi_rad);
102steer_az = bf.steer_az*pi/180;
103steer_el = bf.steer_el*pi/180;
104mu = ones(1,N_half) * 10^(bf.mu_db/20);
105
106%% Source at distance r
107[src_x, src_y, src_z] = source_xyz(bf.steer_r, steer_az, steer_el);
108
109%% Default frequency domain weights
110W = zeros(N_half, bf.num_filters);
111
112%% Coherence matrix, diffuse field
113% Equation 2.11
114Gamma_vv = zeros(N_half, bf.num_filters, bf.num_filters);
115for n=1:bf.num_filters
116    for m=1:bf.num_filters
117        % Equation 2.17
118            lnm = sqrt( (bf.mic_x(n) - bf.mic_x(m))^2 ...
119			+(bf.mic_y(n) - bf.mic_y(m))^2 ...
120			+(bf.mic_z(n) - bf.mic_z(m))^2);
121            Gamma_vv(:,n,m) = sinc(2*pi*f*lnm/bf.c);
122    end
123end
124
125%% Delays from source to each mic
126dt = delay_from_source(bf, src_x, src_y, src_z);
127dt = dt-min(dt);
128
129%% Create array vector
130tau0 = zeros(n_phi, bf.num_filters);
131A = zeros(N_half, n_phi, bf.num_filters);
132d = zeros(N_half, bf.num_filters);
133for n=1:bf.num_filters
134	% Equation 2.27
135	d(:,n) = exp(-j*2*pi*f*dt(n)); % Delays to steer direction
136	for ip = 1:n_phi;
137		phi = phi_rad(ip);
138		x_phi = bf.steer_r*cos(phi)*cos(steer_el);
139		y_phi = bf.steer_r*sin(phi)*cos(steer_el);
140		z_phi = bf.steer_r*sin(steer_el);
141		tau0(ip, n) = sqrt((x_phi-bf.mic_x(n))^2 ...
142				   + (y_phi-bf.mic_y(n))^2 ...
143				   + (z_phi-bf.mic_z(n))^2)/bf.c;
144	end
145end
146tau = tau0-min(min(tau0));
147
148for n=1:bf.num_filters
149	for ip = 1:n_phi
150		% N_half x n_phi x Nm
151		A(:, ip, n) = exp(-j*2*pi*f*tau(ip, n));
152	end
153end
154
155switch lower(bf.type)
156	case 'sdb'
157		% Superdirective
158		for iw = 1:N_half
159			% Equation 2.33
160			I = eye(bf.num_filters, bf.num_filters);
161			d_w = d(iw,:).';
162			Gamma_vv_w = squeeze(Gamma_vv(iw, :, :));
163			Gamma_vv_w_diagload = Gamma_vv_w + mu(iw)*I;
164			Gamma_vv_w_inv = inv(Gamma_vv_w_diagload);
165			num = Gamma_vv_w_inv * d_w;
166			denom1 = d_w' * Gamma_vv_w_inv;
167			denom2 = denom1 * d_w;
168			W_w = num / denom2;
169			W(iw, :) = W_w.';
170		end
171	case 'dsb'
172		% Delay and sum
173		for iw = 1:N_half
174			% Equation 2.31
175			% W = 1/N*d
176			d_w = d(iw, :);
177			W_w = 1/bf.num_filters * d_w;
178			W(iw, :) = W_w.';
179		end
180	otherwise
181		error('Invalid type, use SDB or DSB');
182end
183
184%% Convert w to time domain
185W_full = zeros(N, bf.num_filters);
186W_full = W(1:N_half, :);
187for i=N_half+1:N
188	W_full(i,:) = conj(W(N_half-(i-N_half),:));
189end
190
191win = kaiser(bf.fir_length,bf.fir_beta);
192bf.w = zeros(bf.fir_length, bf.num_filters);
193w_tmp = zeros(N, bf.num_filters);
194idx_max = zeros(bf.num_filters, 1);
195for i=1:bf.num_filters
196	h = real(fftshift(ifft(W_full(:,i))));
197	w_tmp(:,i) = h;
198	idx_max(i) = find(h == max(h));
199end
200
201center_idx = round(mean(idx_max));
202start = center_idx - floor(bf.fir_length/2);
203win0 = kaiser(bf.fir_length,bf.fir_beta);
204for i=1:bf.num_filters
205	win = zeros(bf.fir_length, 1);
206	win_shift = center_idx - idx_max(i) - 1;
207	if (win_shift >= 0)
208		win(1:end-win_shift) = win0(win_shift+1:end);
209	else
210		win(-win_shift:end) = win0(1:end+win_shift+1);
211	end
212	bf.w(:,i) = w_tmp(start:start + bf.fir_length - 1, i) .* win;
213end
214
215%% Back to frequency domain to check spatial response
216W2_full = zeros(N, bf.num_filters);
217for i=1:bf.num_filters
218	% Zero pad
219	h2 = zeros(1,N);
220	h2(start:start + bf.fir_length - 1) = bf.w(:,i);
221	W2_full(:,i) = fft(h2);
222end
223W2 = W2_full(1:N_half, :);
224B2 = zeros(N_half, n_phi);
225for iw = 1:N_half
226    WT = (W2(iw,:)').';
227    AS = squeeze(A(iw,:,:)).';
228    WA = WT * AS;
229    B2(iw,:) = WA;
230end
231bf.resp_fa = B2';
232bf.resp_angle = phi_rad * 180/pi;
233
234%% Directivity in diffuse field
235% Equation 2.18
236% DI(exp(j Omega) = 10*log10( abs(W^H d)^2 / (W^H Gamma_vv W))
237bf.f = f;
238bf.di_db = zeros(1, N_half);
239for iw = 1:N_half
240    W_w = W2(iw,:).';
241    d_w = d(iw,:).';
242    Gamma_vv_w = squeeze(Gamma_vv(iw,:,:));
243    W_wh = W_w';
244    num = abs(W_wh * d_w)^2;
245    denom1 = W_wh * Gamma_vv_w;
246    denom2 = denom1 * W_w;
247    di = num / denom2;
248    bf.di_db(iw) = 10*log10(abs(di));
249end
250
251
252%% White noise gain
253for iw = 1:N_half
254    % WNG = abs(^w^H d)^2/(w^H w);
255    W_w = W2(iw,:).';
256    d_w = d(iw,:).';
257    W_wh = W_w';
258    num = abs(W_wh * d_w)^2;
259    denom = W_wh * W_w;
260    wng = num / denom2;
261    wng_db(iw) = 10*log10(abs(wng));
262end
263bf.wng_db = wng_db;
264
265if bf.do_plots
266	%% Array
267	fh(1) = figure(bf.fn);
268	plot3(bf.mic_x(1), bf.mic_y(1), bf.mic_z(1), 'ro');
269	hold on;
270	plot3(bf.mic_x(2:end), bf.mic_y(2:end), bf.mic_z(2:end), 'bo');
271	plot3(src_x, src_y, src_z, 'gx');
272	plot3([0 src_x],[0 src_y],[0 src_z],'c--')
273	for n=1:bf.num_filters
274		text(bf.mic_x(n),  bf.mic_y(n),  bf.mic_z(n) + 20e-3, ...
275		     num2str(n));
276	end
277	hold off
278	pb2 = bf.plot_box / 2;
279	axis([-pb2 pb2 -pb2 pb2 -pb2 pb2]);
280	axis('square');
281	grid on;
282	xlabel('x (m)'); ylabel('y (m)'); zlabel('z (m)');
283	view(-50, 30);
284	title(['Geometry ' bf.array_id], 'Interpreter','none');
285
286	%% Coef
287	fh(2) = figure(bf.fn + 1);
288	plot(bf.w)
289	grid on;
290	xlabel('FIR coefficient'); ylabel('Tap value');
291	title(['FIR filters ' bf.array_id], 'Interpreter','none');
292	ch_legend(bf.num_filters);
293
294	%% Frequency responses
295	fh(3) = figure(bf.fn + 2);
296	f = linspace(0, bf.fs/2, 256);
297	h = zeros(256, bf.num_filters);
298	for i = 1:bf.num_filters
299		h(:,i) = freqz(bf.w(:,i), 1, f, bf.fs);
300	end
301	plot(f, 20*log10(abs(h)));
302	grid on
303	ylabel('Magnitude (dB)');
304	xlabel('Frequency (Hz)');
305	title(['FIR magnitude responses ' bf.array_id], 'Interpreter','none');
306	ch_legend(bf.num_filters);
307
308	%% Group delays
309	fh(4) = figure(bf.fn + 3);
310	gd = zeros(256, bf.num_filters);
311	for i = 1:bf.num_filters
312		gd(:,i) = grpdelay(bf.w(:,i), 1, f, bf.fs);
313	end
314	plot(f, gd/bf.fs*1e6);
315	grid on
316	ylabel('Group delay (us)');
317	xlabel('Frequency (Hz)');
318	title(['FIR group delays ' bf.array_id], 'Interpreter','none');
319	ch_legend(bf.num_filters);
320
321	%% DI
322	bf.fh(5) = figure(bf.fn + 4);
323	semilogx(bf.f(2:end), bf.di_db(2:end))
324	xlabel('Frequency (Hz)'); ylabel('DI (dB)'); grid on;
325	legend('Suppression of diffuse field noise','Location','SouthEast');
326	title(['Directivity Index ' bf.array_id], 'Interpreter','none');
327
328	%% WNG
329	bf.fh(6) = figure(bf.fn + 5);
330	semilogx(bf.f(2:end), bf.wng_db(2:end))
331	xlabel('Frequency (Hz)'); ylabel('WNG (dB)'); grid on;
332	legend('Attenuation of uncorrelated noise','Location','SouthEast');
333	title(['White noise gain ' bf.array_id], 'Interpreter','none');
334	drawnow;
335
336	%% 2D
337	bf.fh(7) = figure(bf.fn + 6);
338	colormap(jet);
339	phi_deg = phi_rad*180/pi;
340	imagesc(bf.f, bf.resp_angle, 20*log10(abs(bf.resp_fa)), [-30 0]);
341	set(gca,'YDir','normal')
342	grid on;
343	colorbar;
344	xlabel('Frequency (Hz)'); ylabel('Angle (deg)');
345	title(['Spatial response ' bf.array_id], 'Interpreter','none');
346
347	%% Polar
348	bf.fh(8) = figure(bf.fn + 7);
349	flist = [1000 2000 3000 4000];
350	idx = [];
351	for i = 1:length(flist)
352		idx(i) = find(f > flist(i), 1, 'first');
353	end
354	bf.resp_polar = abs(B2(idx,:));
355	if exist('OCTAVE_VERSION', 'builtin')
356		polar(phi_rad, bf.resp_polar);
357	else
358		polarplot(phi_rad, bf.resp_polar);
359	end
360	legend('1 kHz','2 kHz','3 kHz','4 kHz');
361	title(['Polar response ' bf.array_id], 'Interpreter','none');
362end
363
364%% Create data for simulation 1s per angle
365
366if isempty(bf.sinerot_fn)
367	fprintf(1, 'No file for 360 degree sine source rotate specified\n');
368else
369	fprintf(1, 'Creating 360 degree sine source rotate...\n');
370	fsi = 384e3; % Target interpolated rate
371	p = round(fsi / bf.fs); % Interpolation factor
372	fsi = p * bf.fs; % Recalculate high rate
373	ti = 1/fsi;  % perid at higher rate
374	t_add = 10.0/bf.c; % Additional signal time for max 10m propagation
375	tt0 = bf.sinerot_t + t_add; % Total sine length per angle
376	nt = bf.fs * bf.sinerot_t; % Number samples output per angle
377	nti = p * nt; % Number samples output per angle at high rate
378	t_ramp = 20e-3; % 20 ms ramps to start and end of each angle tone
379	n_ramp = t_ramp * bf.fs;
380	win = ones(nt, 1);
381	win(1:n_ramp) = linspace(0, 1, n_ramp);
382	win(end-n_ramp+1:end) = linspace(1, 0, n_ramp);
383	si = multitone(fsi, bf.sinerot_f, bf.sinerot_a, tt0);
384	test_az = (bf.sinerot_az_start:bf.sinerot_az_step:bf.sinerot_az_stop) * pi/180;
385	test_n = length(test_az);
386	test_el = zeros(1, test_n);
387	[test_x, test_y, test_z] = source_xyz(bf.steer_r, test_az, test_el);
388	td = zeros(test_n * nt, bf.mic_n);
389	for i = 1:length(test_az)
390		dt = delay_from_source(bf, test_x(i), test_y(i), test_z(i));
391		dn = round(dt / ti);
392		mi = zeros(nti, bf.num_filters);
393		for j = 1:bf.mic_n
394			mi(:,j) = mi(:,j) + si(end-dn(j)-nti+1:end-dn(j));
395		end
396		i1 = (i - 1) * nt + 1;
397		i2 = i1 + nt -1;
398		for j = 1:bf.mic_n
399			m = mi(1:p:end, j) .* win;
400			td(i1:i2, j) = m;
401		end
402	end
403	myaudiowrite(bf.sinerot_fn, td, bf.fs);
404end
405
406if isempty(bf.diffuse_fn)
407	fprintf(1, 'No file for diffuse noise field specified\n');
408else
409	fprintf(1, 'Creating diffuse noise field...\n');
410	fsi = 384e3; % Target interpolated rate
411	p = round(fsi / bf.fs); % Interpolation factor
412	fsi = p * bf.fs; % Recalculate high rate
413	ti = 1/fsi;  % period at higher rate
414	t_add = 10.0/bf.c; % Additional signal time for max 20m propagation
415	t0 = bf.diffuse_t + t_add; % Total sine length per angle
416	n0 = floor(bf.fs * t0);
417	nt = floor(bf.fs * bf.diffuse_t); % Number samples output per angle
418	nti = p * nt; % Number samples output per angle at high rate
419	el = 0;
420	for az_deg = -160:20:180 % Azimuth plane only noise with sources
421		az = az_deg * pi/180;
422		[nx, ny, nz] = source_xyz(bf.steer_r, az, el);
423		dt = delay_from_source(bf, nx, ny, nz);
424		dn = round(dt / ti);
425		ns = rand(n0, 1) + rand(n0, 1) - 1;
426		nsi = interp(ns, p);
427		nmi = zeros(nti, bf.mic_n);
428		for j = 1:bf.mic_n
429			nmi(:,j) = nmi(:,j) + nsi(end-dn(j)-nti+1:end-dn(j));
430		end
431	end
432	nm = nmi(1:p:end, :);
433	nlev = level_dbfs(nm(:,1));
434	nm = nm * 10^((bf.diffuse_lev - nlev)/20);
435	myaudiowrite(bf.diffuse_fn, nm, bf.fs);
436end
437
438if isempty(bf.random_fn)
439	fprintf(1, 'No file for random noise specified\n');
440else
441	fprintf(1, 'Creating random noise ...\n');
442	nt = bf.fs * bf.random_t;
443	rn = rand(nt, bf.num_filters) + rand(nt, bf.num_filters) - 1;
444
445	nlev = level_dbfs(rn(:,1));
446	rn = rn * 10^((bf.random_lev - nlev)/20);
447	myaudiowrite(bf.random_fn, rn, bf.fs);
448end
449
450if isempty(bf.mat_fn)
451	fprintf(1, 'No file for beam pattern simulation data specified.\n');
452else
453	fprintf(1, 'Saving design to %s\n', bf.mat_fn);
454	bf_copy = bf;
455	bf.fh = []; % Don't save the large figures, this avoids a warning print too
456	mkdir_check(bf.data_path);
457	save(bf.mat_fn, 'bf');
458	bf = bf_copy;
459end
460
461fprintf(1, 'Done.\n');
462
463end
464
465%% Helper functions
466
467function [x, y, z] = source_xyz(r, az, el)
468
469x = r * cos(az) .* cos(el);
470y = r * sin(az) .* cos(el);
471z = r * sin(el);
472
473end
474
475function dt = delay_from_source(bf, src_x, src_y, src_z)
476
477dm = zeros(1,bf.num_filters);
478for n=1:bf.num_filters
479	dm(n) = sqrt((src_x - bf.mic_x(n))^2 ...
480		     + (src_y - bf.mic_y(n))^2 ...
481		     + (src_z - bf.mic_z(n))^2);
482end
483dt = dm/bf.c;
484
485end
486
487function ch_legend(n)
488switch n
489	case 2
490		legend('1', '2');
491	case 3
492		legend('1', '2', '3');
493	case 4
494		legend('1', '2', '3', '4');
495	case 5
496		legend('1', '2', '3', '4', '5');
497	case 6
498		legend('1', '2', '3', '4', '5', '6');
499	case 7
500		legend('1', '2', '3', '4', '5', '6', '7');
501	otherwise
502		legend('1', '2', '3', '4', '5', '6', '7', '8');
503end
504end
505
506function myaudiowrite(fn, x, fs)
507[~, ~, ext] = fileparts(fn);
508if strcmp(lower(ext), '.raw')
509	s = size(x);
510	xq = zeros(s(1) * s(2), 1, 'int16');
511	scale = 2^15;
512	xs = round(x * scale);
513	xs(xs > scale - 1) = scale -1;
514	xs(xs < -scale) = -scale;
515	for i = 1:s(2)
516		xq(i:s(2):end) = xs(:,i);
517	end
518	fh = fopen(fn, 'wb');
519	fwrite(fh, xq, 'int16');
520	fclose(fh);
521else
522	audiowrite(fn, x, fs);
523end
524end
525