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