├── .gitignore ├── n_designs_1_124.mat ├── docs └── mccormack2023spatial.pdf ├── utils ├── findClosestGridPoints.m ├── findGridWeights.m ├── loadSofaFile.m └── PLOT_REPAIR.m ├── README.md ├── TEST_REPAIR.m └── REPAIR.m /.gitignore: -------------------------------------------------------------------------------- 1 | plots/ 2 | output/ 3 | *.asv 4 | .DS_Store -------------------------------------------------------------------------------- /n_designs_1_124.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leomccormack/REPAIR/HEAD/n_designs_1_124.mat -------------------------------------------------------------------------------- /docs/mccormack2023spatial.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leomccormack/REPAIR/HEAD/docs/mccormack2023spatial.pdf -------------------------------------------------------------------------------- /utils/findClosestGridPoints.m: -------------------------------------------------------------------------------- 1 | function [idx_closest, dirs_closest, angle_diff] = findClosestGridPoints(grid_dirs_rad, target_dirs_rad) 2 | 3 | nGrid = size(grid_dirs_rad,1); 4 | nDirs = size(target_dirs_rad,1); 5 | xyz_grid = unitSph2cart(grid_dirs_rad); 6 | xyz_target = unitSph2cart(target_dirs_rad); 7 | idx_closest = zeros(nDirs,1); 8 | for nd=1:nDirs 9 | [~, idx_closest(nd)] = max(dot(xyz_grid, repmat(xyz_target(nd,:),nGrid,1), 2)); 10 | end 11 | dirs_closest = grid_dirs_rad(idx_closest,:); 12 | angle_diff = acos( dot(xyz_grid(idx_closest,:), xyz_target, 2) ); 13 | 14 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REPAIR 2 | 3 | Reproduction and Parameterisation of Array Impulse Responses (REPAIR) [1]. 4 | 5 | ## Getting Started 6 | 7 | The [TEST_REPAIR.m](TEST_REPAIR.m) script is reliant on the following Matlab libraries: 8 | * [Spherical-Harmonic-Transform](https://github.com/polarch/Spherical-Harmonic-Transform) 9 | * [Higher-Order-Ambisonics](https://github.com/polarch/Higher-Order-Ambisonics) 10 | * [Vector-Base-Amplitude-Panning](https://github.com/polarch/Vector-Base-Amplitude-Panning) 11 | * [Array-Response-Simulator](https://github.com/polarch/Array-Response-Simulator) 12 | * [HO-SIRR Toolbox](https://github.com/leomccormack/HO-SIRR) 13 | * [SDM Toolbox](https://se.mathworks.com/matlabcentral/fileexchange/56663-sdm-toolbox) 14 | 15 | ## Developers 16 | 17 | * **Leo McCormack** - Matlab and algorithm design (contact: leo.mccormack@aalto.fi) 18 | * **Nils Meyer-Kahlen** - Matlab and algorithm design 19 | * **Archontis Politis** - Matlab and algorithm design 20 | 21 | ## License 22 | 23 | This code is provided under the [GNU GPLv2 license](https://choosealicense.com/licenses/gpl-2.0/). 24 | 25 | ## References 26 | 27 | [1] McCormack, L., Meyer-Kahlen, N., and Politis, A. (2023). [**"Spatial Reconstruction-Based Rendering of Microphone Array Room Impulse Responses"**](docs/mccormack2023spatial.pdf). 28 | Journal of the Audio Engineering Society, 71(5), pp.267-280. 29 | -------------------------------------------------------------------------------- /utils/findGridWeights.m: -------------------------------------------------------------------------------- 1 | function [weights, order] = findGridWeights(azi, zen, order) 2 | %findGridWeights Find / approximate quadrature weights by pseudo-inverse. 3 | % Grid azi and zen in rad, order optional (searches max if not given). 4 | % Chris Hold 2020 5 | 6 | assert(all(size(azi) == size(zen))) 7 | assert(size(azi, 2) == 1) 8 | 9 | if nargin < 3 10 | % find max grid order by condition number, this might take a while 11 | cnbefore = 1; 12 | for itn = 1:100 13 | Y_it = getSHreal(itn, [azi, zen]); 14 | cn = cond(Y_it' * Y_it); 15 | % check change of condition number 16 | if cn / cnbefore > 1.5 17 | order = itn - 1; 18 | break 19 | else 20 | cnbefore = cn; 21 | end 22 | end 23 | end 24 | 25 | Y_N = getSHreal(order, [azi, zen]); 26 | %W = pinv(Y_N') * pinv(Y_N); 27 | %W = ((Y_N.' * Y_N) \ Y_N.').' * ((Y_N.' * Y_N) \ Y_N.'); % faster 28 | %weights = sum(W, 2); 29 | 30 | % Fornberg, B., & Martel, J. M. (2014). 31 | % On spherical harmonics based numerical quadrature over the surface of a 32 | % sphere. 33 | % Advances in Computational Mathematics, 40(5–6) 34 | %P_leftinv = pinv(Y_N); 35 | P_leftinv = (Y_N.' * Y_N) \ Y_N.'; 36 | weights = sqrt(4*pi) * P_leftinv(1, :).'; 37 | 38 | if (sum(weights) - 4*pi) > 0.01 || any(weights < 0 ) 39 | warning('Could not calculate weights') 40 | end 41 | 42 | end 43 | 44 | 45 | 46 | function Y_N = getSHreal(N, dirs) 47 | % getSHreal adapted from 48 | % https://github.com/polarch/Spherical-Harmonic-Transform/blob/master/getSH.m 49 | Ndirs = size(dirs, 1); 50 | Nharm = (N+1)^2; 51 | 52 | Y_N = zeros(Nharm, Ndirs); 53 | idx_Y = 0; 54 | for n=0:N 55 | 56 | m = (0:n)'; 57 | Lnm_real = legendre(n, cos(dirs(:,2)')); 58 | if n~=0 59 | condon = (-1).^[m(end:-1:2);m] * ones(1,Ndirs); 60 | Lnm_real = condon .* [Lnm_real(end:-1:2, :); Lnm_real]; 61 | end 62 | norm_real = sqrt( (2*n+1)*factorial(n-m) ./ (4*pi*factorial(n+m)) ); 63 | 64 | Nnm_real = norm_real * ones(1,Ndirs); 65 | if n~=0 66 | Nnm_real = [Nnm_real(end:-1:2, :); Nnm_real]; 67 | end 68 | 69 | CosSin = zeros(2*n+1,Ndirs); 70 | CosSin(n+1,:) = ones(1,size(dirs,1)); 71 | if n~=0 72 | CosSin(m(2:end)+n+1,:) = sqrt(2)*cos(m(2:end)*dirs(:,1)'); 73 | CosSin(-m(end:-1:2)+n+1,:) = sqrt(2)*sin(m(end:-1:2)*dirs(:,1)'); 74 | end 75 | Ynm = Nnm_real .* Lnm_real .* CosSin; 76 | 77 | 78 | Y_N(idx_Y+1:idx_Y+(2*n+1), :) = Ynm; 79 | idx_Y = idx_Y + 2*n+1; 80 | end 81 | Y_N = Y_N.'; 82 | end 83 | -------------------------------------------------------------------------------- /utils/loadSofaFile.m: -------------------------------------------------------------------------------- 1 | function sofaStripped = loadSofaFile(hrtf_sofa_path) 2 | % LOADSOFAFILE Extract HRIRs and source positions from an HRTF sofa file 3 | % 4 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 5 | % 6 | % This file is part of the COMPASS reference implementation, as described 7 | % in the publication 8 | % 9 | % Archontis Politis, Sakari Tervo, and Ville Pulkki. 2018. 10 | % "COMPASS: Coding and multidirectional parameterization of ambisonic 11 | % sound scenes." 12 | % IEEE Int. Conf. on Acoustics, Speech and Signal Processing (ICASSP). 13 | % 14 | % Author: Leo McCormack (leomccormack@aalto.fi) 15 | % Copyright (C) 2021 - Leo McCormack 16 | % 17 | % The COMPASS reference code is free software; you can redistribute it 18 | % and/or modify it under the terms of the GNU General Public License as 19 | % published by the Free Software Foundation; either version 2 of the 20 | % License, or (at your option) any later version. 21 | % 22 | % The COMPASS reference code is distributed in the hope that it will be 23 | % useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 24 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 25 | % Public License for more details. 26 | % 27 | % You should have received a copy of the GNU General Public License along 28 | % with this program; if not, see . 29 | % 30 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 31 | % 32 | % INPUT ARGUMENTS 33 | % 34 | % hrtf_sofa_path % full path/filename to SOFA file 35 | % 36 | % OUTPUT ARGUMENTS 37 | % 38 | % sofaStripped % MATLAB structure holding HRIRs, HRIR samplerate, and 39 | % measurement directions 40 | % 41 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 42 | 43 | %% Info on sofa file 44 | %ncdisp(hrtf_sofa_path) 45 | 46 | %% Open sofa file and determine dimension IDs and lengths 47 | ncid = netcdf.open(hrtf_sofa_path, 'NOWRITE'); % read-only access 48 | for i=1:6% there are 6 dimensions in the sofa standard 49 | [dimname(i,1), dimlen(i,1)] = netcdf.inqDim(ncid,i-1); %#ok 50 | dimid(i,1) = netcdf.inqDimID(ncid,dimname(i,1)); %#ok 51 | end 52 | varname = 'DataType'; 53 | DataType = netcdf.getAtt(ncid,netcdf.getConstant('NC_GLOBAL'),varname); 54 | 55 | %% Extract IR data 56 | varname = 'Data.IR'; 57 | varid = netcdf.inqVarID(ncid,varname); 58 | %[~, ~, dimids, ~] = netcdf.inqVar(ncid,varid); 59 | %IR_dim1 = dimlen(dimid(dimids(1,1)+1)+1); %+1s only for MatLab 60 | %IR_dim2 = dimlen(dimid(dimids(1,2)+1)+1); 61 | %IR_dim3 = dimlen(dimid(dimids(1,3)+1)+1); 62 | IR = netcdf.getVar(ncid,varid); 63 | varname = 'Data.SamplingRate'; 64 | varid = netcdf.inqVarID(ncid,varname); 65 | IR_fs = netcdf.getVar(ncid,varid); 66 | 67 | %% Extract positional data 68 | varname = 'SourcePosition'; 69 | varid = netcdf.inqVarID(ncid,varname); 70 | %[~, ~, dimids, ~] = netcdf.inqVar(ncid,varid); 71 | %SourcePosition_dim1 = dimlen(dimid(dimids(1,1)+1)+1); %+1s only for MatLab 72 | %SourcePosition_dim2 = dimlen(dimid(dimids(1,2)+1)+1); 73 | SourcePosition = netcdf.getVar(ncid,varid); 74 | 75 | %% Stripped down sofa info 76 | sofaStripped.DataType = DataType; 77 | sofaStripped.IR = IR; 78 | sofaStripped.IR_fs = IR_fs; 79 | sofaStripped.SourcePosition = SourcePosition; 80 | %sofaStripped 81 | 82 | %% Close sofa file, once info extracted 83 | netcdf.close(ncid); 84 | 85 | end 86 | -------------------------------------------------------------------------------- /utils/PLOT_REPAIR.m: -------------------------------------------------------------------------------- 1 | function PLOT_REPAIR(name, h_lspkr, h_lspkr_dir, h_lspkr_diff, pars, analysis, applyPROC, path_for_plots) 2 | 3 | if nargin>7 4 | if ~isempty(path_for_plots) 5 | if ~exist(path_for_plots, 'dir'), mkdir(path_for_plots); end 6 | end 7 | else 8 | path_for_plots = []; 9 | end 10 | 11 | f = 0:pars.fs/pars.winsize:pars.fs/2; 12 | kkk = {'b', 'r', 'm', 'c', 'g', 'k', 'y', 'b', 'r', 'm', 'c', 'g', 'k', 'y', 'b', 'r', 'm', 'c', 'g', 'k', 'y', 'b', 'r', 'm', 'c', 'g', 'k', 'y'}; 13 | display_lpf_cutoff_freq_hz = 100; 14 | 15 | %% SPATIAL ANALYSIS ESTIMATES 16 | if ~isempty(analysis) 17 | figure, 18 | h=subplot(4,1,1); 19 | s=mesh(1:size(analysis.K,1), f, analysis.K.'); 20 | s.FaceColor = 'flat'; 21 | set(h,'YScale','log'), 22 | ylabel('frequency') 23 | colormap('jet'), title('number of sources detected'), xlabel('time, hops'), xlim([1 size(analysis.K,1)]), ylim([f(1) f(end)]) 24 | zlabel('K') 25 | 26 | h=subplot(4,1,2); 27 | s=mesh(1:size(analysis.diffuseness,1), f, analysis.diffuseness.'); 28 | s.FaceColor = 'flat'; 29 | set(h,'YScale','log'), 30 | ylabel('frequency') 31 | colormap('jet'), title('diffuseness'), xlabel('time, hops'), zlim([0 1]), xlim([1 size(analysis.K,1)]), ylim([f(1) f(end)]) 32 | zlabel('diffuseness') 33 | 34 | h=subplot(4,1,3); 35 | for ii=1:size(analysis.azim,3) 36 | for jj=1:size(analysis.azim,2) 37 | plot3(1:size(analysis.azim,1), ... 38 | f(ii)*ones(size(analysis.azim,1),1), 180/pi.*analysis.azim(:,jj,ii), '.', 'Color', kkk{jj} ); hold on, 39 | end 40 | end 41 | set(h,'YScale','log'), 42 | ylabel('frequency'), title('azimuth'), xlabel('time, hops'), xlim([1 size(analysis.K,1)]), ylim([f(1) f(end)]) 43 | zlabel('degrees'), zlim([-180 180]) 44 | grid on 45 | 46 | h=subplot(4,1,4); 47 | for ii=1:size(analysis.elev,3) 48 | for jj=1:size(analysis.elev,2) 49 | plot3(1:size(analysis.elev,1), ... 50 | f(ii)*ones(size(analysis.elev,1),1), 180/pi.*analysis.elev(:,jj,ii), '.', 'Color', kkk{jj} ); hold on, 51 | end 52 | end 53 | set(h,'YScale','log'), 54 | ylabel('frequency'), title('elevation'), xlabel('time, hops'), xlim([1 size(analysis.K,1)]), ylim([f(1) f(end)]) 55 | zlabel('degrees'), zlim([-90 90]) 56 | grid on 57 | 58 | sgtitle(name) 59 | 60 | if ~isempty(path_for_plots) 61 | print([path_for_plots filesep ['SpatialAnalysis ' name]], '-dpng', '-r300'); 62 | %savefig([path_for_plots filesep name]); 63 | end 64 | 65 | 66 | figure, 67 | h=subplot(4,1,1); 68 | energy_dB_ref = 10*log10(abs(analysis.energy_in.')); 69 | energy_min = max(min(energy_dB_ref(:))-6, -60); 70 | energy_max = max(energy_dB_ref(:))+6; 71 | s=mesh(1:size(analysis.energy_in,1), f, max(energy_dB_ref,-59.8)); 72 | s.FaceColor = 'flat'; 73 | set(h,'YScale','log'), 74 | ylabel('Energy') 75 | colormap('jet'), title('Input energy'), xlabel('time, hops'), xlim([1 size(analysis.K,1)]), ylim([f(1) f(end)]), zlim([energy_min energy_max]) 76 | zlabel('Energy') 77 | 78 | h=subplot(4,1,2); 79 | s=mesh(1:size(analysis.energy_ndiff,1), f, max(10*log10(abs(analysis.energy_ndiff.')),-59.8)); 80 | s.FaceColor = 'flat'; 81 | set(h,'YScale','log'), 82 | ylabel('frequency') 83 | colormap('jet'), title('Non-diffuse energy'), xlabel('time, hops'), xlim([1 size(analysis.K,1)]), ylim([f(1) f(end)]), zlim([energy_min energy_max]) 84 | zlabel('Energy') 85 | 86 | h=subplot(4,1,3); 87 | s=mesh(1:size(analysis.energy_diff,1), f, max(10*log10(abs(analysis.energy_diff.')),-59.8)); 88 | s.FaceColor = 'flat'; 89 | set(h,'YScale','log'), 90 | ylabel('frequency') 91 | colormap('jet'), title('Diffuse energy'), xlabel('time, hops'), xlim([1 size(analysis.K,1)]), ylim([f(1) f(end)]), zlim([energy_min energy_max]) 92 | zlabel('Energy') 93 | 94 | h=subplot(4,1,4); 95 | s=mesh(1:size(analysis.energy_diff,1), f, max(10*log10(abs(analysis.energy_total.')),-59.8)); 96 | s.FaceColor = 'flat'; 97 | set(h,'YScale','log'), 98 | ylabel('frequency') 99 | colormap('jet'), title('Total output energy'), xlabel('time, hops'), xlim([1 size(analysis.K,1)]), ylim([f(1) f(end)]), zlim([energy_min energy_max]) 100 | zlabel('Energy') 101 | end 102 | 103 | %% OUTPUT LOUSPEAKER RIR PLOTS 104 | figure, 105 | 106 | h_plot = h_lspkr; 107 | if applyPROC 108 | [blp,alp]=butter(1, display_lpf_cutoff_freq_hz/(pars.fs/2)); % low-pass filter 109 | e_ref=filter(blp,alp,h_plot.^2); e_ref=10*log10(e_ref+eps); 110 | maxx=max(max(e_ref)); e_ref=e_ref-maxx; 111 | h_plot=max(-59.8,e_ref); 112 | else 113 | h_plot = max(20*log10(abs(h_plot)), -59.8); 114 | end 115 | subplot(1,3,3), s=mesh(h_plot); 116 | s.FaceColor = 'flat'; 117 | colormap('jet'), xlim([1, size(pars.ls_dirs_deg,1)]), zlim([-60, 10 ]) 118 | title('combined'), xlabel('loudspeaker #'), zlabel('energy, dB'), ylabel('time, samples') 119 | 120 | h_plot = h_lspkr_dir; 121 | if applyPROC 122 | [blp,alp]=butter(1, display_lpf_cutoff_freq_hz/(pars.fs/2)); % low-pass filter 123 | e_ref=filter(blp,alp,h_plot.^2); e_ref=10*log10(e_ref+eps); 124 | e_ref=e_ref-maxx; 125 | h_plot=max(-59.8,e_ref); 126 | else 127 | h_plot = max(20*log10(abs(h_plot)), -59.8); 128 | end 129 | subplot(1,3,1), s=mesh(h_plot); 130 | s.FaceColor = 'flat'; 131 | colormap('jet'), xlim([1, size(pars.ls_dirs_deg,1)]), zlim([-60,10 ]) 132 | title('direct stream'), xlabel('loudspeaker #'), zlabel('energy, dB'), ylabel('time, samples') 133 | 134 | h_plot = h_lspkr_diff; 135 | if applyPROC 136 | [blp,alp]=butter(1, display_lpf_cutoff_freq_hz/(pars.fs/2)); % low-pass filter 137 | e_ref=filter(blp,alp,h_plot.^2); e_ref=10*log10(e_ref+eps); 138 | e_ref=e_ref-maxx; 139 | h_plot=max(-59.8,e_ref); 140 | else 141 | h_plot = max(20*log10(abs(h_plot)), -59.8); 142 | end 143 | subplot(1,3,2), s=mesh(h_plot); 144 | s.FaceColor = 'flat'; 145 | colormap('jet'), xlim([1, size(pars.ls_dirs_deg,1)]), zlim([-60, 10 ]) 146 | title('ambient stream'), xlabel('loudspeaker #'), zlabel('energy, dB'), ylabel('time, samples') 147 | 148 | 149 | sgtitle(name) 150 | 151 | if ~isempty(path_for_plots) 152 | print([path_for_plots filesep ['LRIR ' name]], '-dpng', '-r300'); 153 | %savefig([path_for_plots filesep name]); 154 | end 155 | 156 | end 157 | 158 | -------------------------------------------------------------------------------- /TEST_REPAIR.m: -------------------------------------------------------------------------------- 1 | % ------------------------------------------------------------------------- 2 | % This file is part of REPAIR 3 | % Copyright (c) 2021 - Leo McCormack, Archontis Politis & Nils Meyer-Kahlen 4 | % 5 | % REPAIR is free software; you can redistribute it and/or modify it under 6 | % the terms of the GNU General Public License as published by the Free 7 | % Software Foundation; either version 2 of the License, or (at your option) 8 | % any later version. 9 | % 10 | % REPAIR is distributed in the hope that it will be useful, but WITHOUT 11 | % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | % FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 13 | % more details. 14 | % 15 | % See for a copy of the GNU General Public 16 | % License. 17 | % ------------------------------------------------------------------------- 18 | % 19 | % Unit tests for Reproduction and Parameterisation of Array Impulse 20 | % Responses (REPAIR) [1] 21 | % 22 | % DEPENDENCES 23 | % Spherical-Harmonic-Transform Matlab library 24 | % https://github.com/polarch/Spherical-Harmonic-Transform 25 | % Higher-Order-Ambisonics Matlab library 26 | % https://github.com/polarch/Higher-Order-Ambisonics 27 | % Vector-Base-Amplitude-Panning 28 | % https://github.com/polarch/Vector-Base-Amplitude-Panning 29 | % Spherical-Array-Processing 30 | % https://github.com/polarch/Spherical-Array-Processing 31 | % shoebox-roomsim 32 | % https://github.com/polarch/shoebox-roomsim 33 | % HO-SIRR 34 | % https://github.com/leomccormack/HO-SIRR 35 | % (Optional) SDM Toolbox 36 | % https://se.mathworks.com/matlabcentral/fileexchange/56663-sdm-toolbox 37 | % 38 | % REFERENCES 39 | % [1] McCormack, L., Meyer-Kahlen, N., and Politis, A. (2023). "Spatial 40 | % Reconstruction-Based Rendering of Microphone Array Room Impulse 41 | % Responses". Journal of the Audio Engineering Society, 71(5), pp.267-280. 42 | % 43 | % ------------------------------------------------------------------------- 44 | % 45 | % Leo McCormack, 01/11/2021 46 | % leo.mccormack@aalto.fi 47 | % 48 | % ------------------------------------------------------------------------- 49 | clear all, close all, dbstop if error %#ok 50 | if ~exist('render_array_rirs', 'file'), error('shoebox-roomsim not in path'); end 51 | addpath('utils') 52 | 53 | fs = 48e3; 54 | ENABLE_PLOTTING = 1; 55 | path_for_plots = []; % set to '[]' to not save plots, or, e.g. './output/plots' 56 | path_for_renders = './output/renders'; % set to '[]' to not save renders, 57 | path_for_renders_lt = './output/renders_lt'; % set to '[]' to not save listening test renders, 58 | ideal_SH_order = 4; 59 | mic_arrays = {'eigenmike32','tetra','intensity-probe'}; % Options: {'eigenmike32', 'tetra', 'intensity-probe'} 60 | t_designs = load('n_designs_1_124'); % Courtesy of Chris Hold (https://github.com/chris-hld/spaudiopy) 61 | signalLength = fs/16; 62 | sofa_file = '/Users/mccorml1/Documents/HRIRs_SOFA/D1_48K_24bit_256tap_FIR_SOFA_KU100.sofa'; % Can be obtained from the SADIE HRIR database, or any other SOFA file can be used 63 | 64 | % Default REPAIR configuration 65 | pars.grid_svecs = []; % Defined for each test (can be e.g. SH weights, space-domain steering vectors, etc.) 66 | pars.grid_dirs_xyz = t_designs.N060; 67 | pars.grid_dirs_rad = unitCart2sph(pars.grid_dirs_xyz); 68 | pars.grid_weights = findGridWeights(pars.grid_dirs_rad(:,1), pi/2-pars.grid_dirs_rad(:,2))./(4*pi); %(1/size(pars.grid_dirs_rad,1)).*ones(size(pars.grid_dirs_rad,1),1); 69 | pars.fs = fs; 70 | pars.SCMavgOption = 'recur'; % Options: {'block', 'recur', 'alltime'} 71 | pars.SCMavg_coeff = 0.5; % Temporal averaging coefficient, [0..1], if SCMavgOption is set to "recur" 72 | pars.SCMavg_Nframes = 1; % Number of frames in each averaging block, if SCMavgOption is set to "block" 73 | pars.Kestimator = 'RECON'; % Options: {'SORTE', 'SORTED', 'RECON', 'ORACLE'} 74 | pars.DoAestimator = 'MUSIC'; % Options: {'MUSIC', 'SRP', 'ORACLE'} 75 | pars.winsize = 256; % Window size, in time-domain samples 76 | pars.freqGrouping = 'octave'; % Options: {'broadband', 'octave', 'erb', 'fullres'} 77 | pars.streamBalance = 1; % 0: only diffuse stream, 1: both streams are balanced, 2: only direct stream 78 | pars.ENABLE_DIFF_WHITENING = 1; % Applies an operation that diagonalises the SCMs when under diffuse conditions 79 | pars.ENABLE_COHERENT_FOCUSING = 1; % Only used if the steering vectors are frequency dependent, and if there is some band grouping 80 | pars.ENABLE_AMBIENT_ENERGY_PRESERVATION = 1; % Forces the beamformers used for the ambient stream to be energy-preserving over the sphere 81 | pars.decorrelation = 'covMatch'; % Options: {'off', 'convNoise', 'shapedNoise', 'phaseRand', 'covMatch'} 82 | pars.beamformerOption = 'SD'; % Options: {'pinv', 'MF', 'SD'} 83 | pars.ENABLE_QUANTISE_TO_NEAREST_LS = 0; % Quantise to nearest loudspeaker instead of using VBAP 84 | pars.maxAnaFreq_Hz = fs/2; % above this frequency, everything is treated as one band 85 | pars.ls_dirs_deg = 180/pi.*unitCart2sph(t_designs.N008); 86 | pars.vbapNorm = 1; % 0:reverberant room, ~0.5: dry listening room, 1: anechoic 87 | 88 | % Create output folder for the renders 89 | if ~isempty(path_for_renders), if ~exist(path_for_renders, 'dir'), mkdir(path_for_renders); end, end 90 | if ~isempty(path_for_renders_lt), if ~exist(path_for_renders_lt, 'dir'), mkdir(path_for_renders_lt); end, end 91 | path_for_renders_lt_bin = [path_for_renders_lt filesep 'bin']; 92 | if ~isempty(path_for_renders_lt), if ~exist(path_for_renders_lt_bin, 'dir'), mkdir(path_for_renders_lt_bin); end, end 93 | 94 | % Array specifications and steering vectors for the enabled microphone arrays 95 | for mi = 1:length(mic_arrays) 96 | mic_array = mic_arrays{mi}; 97 | 98 | % Simulate array responses for the scanning/beamforming grid 99 | switch mic_array 100 | case 'eigenmike32' 101 | mic_spec{mi}.name = 'eigenmike32'; %#ok 102 | mic_spec{mi}.radius = 0.042; mic_spec{mi}.type = 'rigid'; mic_spec{mi}.dir_coeff = []; mic_spec{mi}.sh_order = 4; %#ok 103 | mic_spec{mi}.dirs_rad = pi/180.*[0 21; 32 0; 0 -21; 328 0; 0 58; 45 35; 69 0; 45 -35; 0 -58; 315 -35; 291 0; 315 35; 91 69; 90 32; 90 -31; 89 -69; 180 21; 212 0; 180 -21; 148 0; 180 58; 225 35; 249 0; 225 -35; 180 -58; 135 -35; 111 0; 135 35; 269 69; 270 32; 270 -32; 271 -69;]; %#ok 104 | case 'tetra' 105 | mic_spec{mi}.name = 'tetra'; %#ok 106 | mic_spec{mi}.radius = 0.02; mic_spec{mi}.type = 'directional'; mic_spec{mi}.dir_coeff = 0.5; mic_spec{mi}.sh_order = 1; %#ok 107 | mic_spec{mi}.dirs_rad = [0.785398163397448, 0.615472907423280; -0.785398163397448, -0.615472907423280; 2.35619449019235, -0.615472907423280; -2.35619449019235, 0.615472907423280;]; %#ok 108 | case 'intensity-probe' 109 | mic_spec{mi}.name = 'intensity-probe'; %#ok 110 | mic_spec{mi}.radius = 0.025; mic_spec{mi}.type = 'open'; mic_spec{mi}.dir_coeff = 1; mic_spec{mi}.sh_order = 1; %#ok 111 | mic_spec{mi}.dirs_rad = unitCart2sph([0.025 0 0; -0.025 0 0; 0 0.025 0; 0 -0.025 0; 0 0 0.025; 0 0 -0.025]); %#ok 112 | end 113 | [mic_spec{mi}.h_grid mic_spec{mi}.H_grid] = simulateSphArray(pars.winsize, mic_spec{mi}.dirs_rad, pars.grid_dirs_rad, mic_spec{mi}.type, mic_spec{mi}.radius, 50, fs, mic_spec{mi}.dir_coeff); %#ok 114 | 115 | % Encoding filters/matrix 116 | [mic_spec{mi}.E_sh, mic_spec{mi}.e_sh] = arraySHTfiltersMeas_regLSHD(mic_spec{mi}.H_grid, mic_spec{mi}.sh_order, pars.grid_dirs_rad, [], pars.winsize, 15); %#ok 117 | 118 | % Array SH domain steering vectors 119 | for nb=1:pars.winsize/2+1 120 | mic_spec{mi}.H_grid_sh(nb,:,:) = mic_spec{mi}.E_sh(:,:,nb) * squeeze(mic_spec{mi}.H_grid(nb,:,:)); 121 | end 122 | if ENABLE_PLOTTING==1 123 | figure, 124 | for ii=1:size(mic_spec{mi}.e_sh,1), for jj=1:size(mic_spec{mi}.e_sh,2), plot(squeeze(mic_spec{mi}.e_sh(ii,jj,:))), hold on, end, end 125 | title([ mic_spec{mi}.name ' encoding filters']), grid on 126 | evaluateSHTfilters(mic_spec{mi}.E_sh, mic_spec{mi}.H_grid, fs, sqrt(4*pi) * getSH(mic_spec{mi}.sh_order, [pars.grid_dirs_rad(:,1) pi/2-pars.grid_dirs_rad(:,2)], 'real'),[]); 127 | end 128 | 129 | % Permute into the format that REPAIR requires 130 | mic_spec{mi}.H_grid = permute(mic_spec{mi}.H_grid, [2 3 1]); %#ok 131 | mic_spec{mi}.H_grid_sh = permute(mic_spec{mi}.H_grid_sh, [2 3 1]); %#ok 132 | end 133 | 134 | % For binauralising the test rendered outputs 135 | if ~isempty(path_for_renders) 136 | sofa = loadSofaFile(sofa_file); 137 | hrirs = sofa.IR; 138 | hrirs = 0.25.*hrirs./max(abs(hrirs(:))); 139 | hrir_dirs_rad = pi/180.*sofa.SourcePosition([1 2], :).'; 140 | assert(sofa.IR_fs==fs) 141 | h_ls2bin = permute(hrirs(:,:,findClosestGridPoints(hrir_dirs_rad, pars.ls_dirs_deg*pi/180)), [1 3 2]); 142 | clear sofa 143 | end 144 | 145 | %% Plane-waves (pw) in a free-field 146 | pw_tests = {'one pw', 'two inc pw', 'three inc pw'}; % Options: {'one pw', 'two pw', 'two coh pw', 'three coh pw'} 147 | NFFT = 256; 148 | 149 | for ti = 1:length(pw_tests) 150 | pw_test = pw_tests{ti}; 151 | 152 | % pw signals and directions: 153 | switch pw_test 154 | case 'one pw' 155 | src_dirs_rad = pi/180.*[58.8941020926610,53.6513674205284]; 156 | src_sigs = (1/3).*randn(signalLength,size(src_dirs_rad,1)); 157 | case 'two inc pw' 158 | src_dirs_rad = pi/180.*[58.8941020926610,53.6513674205284; -130.226160991636,-14.0948465161701]; 159 | src_sigs = (1/3).*randn(signalLength,size(src_dirs_rad,1)); 160 | case 'two coh pw' 161 | src_dirs_rad = pi/180.*[58.8941020926610,53.6513674205284; -130.226160991636,-14.0948465161701]; 162 | src_sigs = (1/3).*repmat(randn(signalLength,1), [1 size(src_dirs_rad,1)]); 163 | case 'three inc pw' 164 | src_dirs_rad = pi/180.*[58.8941020926610,53.6513674205284; -130.226160991636,-14.0948465161701; 0,-14.0948465161701]; 165 | src_sigs = (1/3).*randn(signalLength,size(src_dirs_rad,1)); 166 | case 'three coh pw' 167 | src_dirs_rad = pi/180.*[58.8941020926610,53.6513674205284; -130.226160991636,-14.0948465161701; 0,-14.0948465161701]; 168 | src_sigs = (1/3).*repmat(randn(signalLength,1), [1 size(src_dirs_rad,1)]); 169 | end 170 | if ~isempty(path_for_renders) 171 | h_src2bin = permute(hrirs(:,:,findClosestGridPoints(hrir_dirs_rad, src_dirs_rad)), [1 3 2]); 172 | y_bin = matrixConvolver(src_sigs, h_src2bin, size(hrirs,1)); 173 | audiowrite( [path_for_renders filesep 'TEST ' pw_test ' reference' '.wav'], y_bin, fs); 174 | if ENABLE_PLOTTING==1 175 | y_bin_fft = []; for ch = 1:2, y_bin_fft(:,:,ch) = fft(buffer(y_bin(:,ch),NFFT)); end %#ok 176 | figure, semilogx(0:fs/NFFT:fs/2, 20*log10(abs(squeeze(mean(abs(y_bin_fft(1:end/2+1,:,:)), 2))))) 177 | title(['TEST ' pw_test ' reference']), grid on, xlim([100 20e3]) 178 | end 179 | end 180 | 181 | % Ideal SH receiver 182 | test_name = ['TEST ' pw_test ' (ideal SH receiver, order ' num2str(ideal_SH_order) ')']; disp(test_name); 183 | pars.grid_svecs = getSH(ideal_SH_order, [pars.grid_dirs_rad(:,1) pi/2-pars.grid_dirs_rad(:,2)], 'real').'; 184 | srir_sh = src_sigs * (getSH(ideal_SH_order, [src_dirs_rad(:,1) pi/2-src_dirs_rad(:,2)], 'real')); 185 | [h_ls, h_ls_dir, h_ls_diff, analysis] = REPAIR(srir_sh, pars); 186 | if ENABLE_PLOTTING, PLOT_REPAIR(test_name, h_ls, h_ls_dir, h_ls_diff, pars, analysis, 0, path_for_plots); end 187 | if ~isempty(path_for_renders) 188 | y_bin = matrixConvolver(h_ls, h_ls2bin, size(hrirs,1)); 189 | audiowrite( [path_for_renders filesep test_name '.wav'], y_bin, fs); 190 | if ENABLE_PLOTTING==1 191 | y_bin_fft = []; for ch = 1:2, y_bin_fft(:,:,ch) = fft(buffer(y_bin(:,ch),NFFT)); end %#ok 192 | figure, semilogx(0:fs/NFFT:fs/2, 20*log10(abs(squeeze(mean(abs(y_bin_fft(1:end/2+1,:,:)), 2))))) 193 | title(test_name), grid on, xlim([100 20e3]) 194 | end 195 | end 196 | 197 | % Now loop over the microphone arrays 198 | for mi = 1:length(mic_arrays) 199 | % Space domain: 200 | test_name = ['TEST ' pw_test ' (array ' mic_spec{mi}.name ', array steering vectors [space domain])']; disp(test_name); 201 | pars.grid_svecs = mic_spec{mi}.H_grid; 202 | h_src = simulateSphArray(pars.winsize, mic_spec{mi}.dirs_rad, src_dirs_rad, mic_spec{mi}.type, mic_spec{mi}.radius, 25, fs, mic_spec{mi}.dir_coeff); 203 | srir = matrixConvolver(src_sigs, permute(h_src, [1 3 2]), pars.winsize); 204 | [h_ls, h_ls_dir, h_ls_diff, analysis] = REPAIR(srir, pars); 205 | if ENABLE_PLOTTING, PLOT_REPAIR(test_name, h_ls, h_ls_dir, h_ls_diff, pars, analysis, 0, path_for_plots); end 206 | if ~isempty(path_for_renders) 207 | y_bin = matrixConvolver(h_ls, h_ls2bin, size(hrirs,1)); 208 | audiowrite( [path_for_renders filesep test_name '.wav'], y_bin, fs); 209 | if ENABLE_PLOTTING==1 210 | y_bin_fft = []; for ch = 1:2, y_bin_fft(:,:,ch) = fft(buffer(y_bin(:,ch),NFFT)); end %#ok 211 | figure, semilogx(0:fs/NFFT:fs/2, 20*log10(abs(squeeze(mean(abs(y_bin_fft(1:end/2+1,:,:)), 2))))) 212 | title(test_name), grid on, xlim([100 20e3]) 213 | end 214 | end 215 | 216 | if ~strcmp(mic_spec{mi}.type, 'open') 217 | % SH domain (using broad-band SHs as steering vectors): 218 | test_name = ['TEST ' pw_test ' (array ' mic_spec{mi}.name ', broad-band SH steering vectors)']; disp(test_name); 219 | pars.grid_svecs = getSH(mic_spec{mi}.sh_order, [pars.grid_dirs_rad(:,1) pi/2-pars.grid_dirs_rad(:,2)], 'real').'; 220 | srir_sh = matrixConvolver(srir, permute(mic_spec{mi}.e_sh, [3 2 1]), pars.winsize); 221 | [h_ls, h_ls_dir, h_ls_diff, analysis] = REPAIR(srir_sh, pars); 222 | if ENABLE_PLOTTING, PLOT_REPAIR(test_name, h_ls, h_ls_dir, h_ls_diff, pars, analysis, 0, path_for_plots); end 223 | if ~isempty(path_for_renders) 224 | audiowrite( [path_for_renders filesep test_name '.wav'], matrixConvolver(h_ls, h_ls2bin, size(hrirs,1)), fs); 225 | if ENABLE_PLOTTING==1 226 | y_bin_fft = []; for ch = 1:2, y_bin_fft(:,:,ch) = fft(buffer(y_bin(:,ch),NFFT)); end %#ok 227 | figure, semilogx(0:fs/NFFT:fs/2, 20*log10(abs(squeeze(mean(abs(y_bin_fft(1:end/2+1,:,:)), 2))))) 228 | title(test_name), grid on, xlim([100 20e3]) 229 | end 230 | end 231 | 232 | % SH domain (using the encoded SMA steering vectors): 233 | test_name = ['TEST ' pw_test ' (array ' mic_spec{mi}.name ', SH encoded array steering vectors)']; disp(test_name); 234 | pars.grid_svecs = mic_spec{mi}.H_grid_sh; 235 | [h_ls, h_ls_dir, h_ls_diff, analysis] = REPAIR(srir_sh, pars); 236 | if ENABLE_PLOTTING, PLOT_REPAIR(test_name, h_ls, h_ls_dir, h_ls_diff, pars, analysis, 0, path_for_plots); end 237 | if ~isempty(path_for_renders) 238 | audiowrite( [path_for_renders filesep test_name '.wav'], matrixConvolver(h_ls, h_ls2bin, size(hrirs,1)), fs); 239 | if ENABLE_PLOTTING==1 240 | y_bin_fft = []; for ch = 1:2, y_bin_fft(:,:,ch) = fft(buffer(y_bin(:,ch),NFFT)); end %#ok 241 | figure, semilogx(0:fs/NFFT:fs/2, 20*log10(abs(squeeze(mean(abs(y_bin_fft(1:end/2+1,:,:)), 2))))) 242 | title(test_name), grid on, xlim([100 20e3]) 243 | end 244 | end 245 | end 246 | end 247 | end 248 | 249 | 250 | %% Diffuse-field input 251 | NFFT = 4096; 252 | diff_dirs_xyz = t_designs.N040; 253 | diff_dirs_rad = unitCart2sph(diff_dirs_xyz); 254 | 255 | % Reference 256 | diff_sigs = (1/3).*randn(signalLength,size(diff_dirs_rad,1)); 257 | diff_sigs = sqrt(1/size(diff_dirs_rad,1)) .* diff_sigs; 258 | if ~isempty(path_for_renders) 259 | h_diff2bin = permute(hrirs(:,:,findClosestGridPoints(hrir_dirs_rad, diff_dirs_rad)), [1 3 2]); 260 | y_bin=matrixConvolver(diff_sigs, h_diff2bin, size(hrirs,1)); 261 | if ENABLE_PLOTTING==1 262 | y_bin_fft = []; for ch = 1:2, y_bin_fft(:,:,ch) = fft(buffer(y_bin(:,ch),NFFT)); end %#ok 263 | figure, semilogx(0:fs/NFFT:fs/2, 20*log10(abs(squeeze(mean(abs(y_bin_fft(1:end/2+1,:,:)), 2))))) 264 | title('TEST diffusefield reference'), grid on, xlim([100 20e3]) 265 | end 266 | audiowrite( [path_for_renders filesep 'TEST diffusefield reference' '.wav'], y_bin, fs); 267 | end 268 | 269 | % Ideal SH receiver 270 | test_name = ['TEST diffusefield (ideal SH receiver, order ' num2str(ideal_SH_order) ')']; disp(test_name); 271 | pars.grid_svecs = getSH(ideal_SH_order, [pars.grid_dirs_rad(:,1) pi/2-pars.grid_dirs_rad(:,2)], 'real').'; 272 | srir_sh = diff_sigs * (getSH(ideal_SH_order, [diff_dirs_rad(:,1) pi/2-diff_dirs_rad(:,2)], 'real')); 273 | [h_ls, h_ls_dir, h_ls_diff, analysis] = REPAIR(srir_sh, pars); 274 | if ENABLE_PLOTTING==1 275 | PLOT_REPAIR(test_name, h_ls, h_ls_dir, h_ls_diff, pars, analysis, 0, path_for_plots); 276 | y_bin=matrixConvolver(h_ls, h_ls2bin, size(hrirs,1)); 277 | y_bin_fft = []; for ch = 1:2, y_bin_fft(:,:,ch) = fft(buffer(y_bin(:,ch),NFFT)); end %#ok 278 | figure, semilogx(0:fs/NFFT:fs/2, 20*log10(abs(squeeze(mean(abs(y_bin_fft(1:end/2+1,:,:)), 2))))) 279 | title(test_name), grid on, xlim([100 20e3]) 280 | figure, imagesc(h_ls_diff'*h_ls_diff), title(pars.decorrelation) 281 | end 282 | if ~isempty(path_for_renders) 283 | audiowrite( [path_for_renders filesep test_name '.wav'], matrixConvolver(h_ls, h_ls2bin, size(hrirs,1)), fs); 284 | end 285 | 286 | % Now loop over the microphone arrays 287 | for mi = 1:length(mic_arrays) 288 | % Space domain: 289 | test_name = ['TEST diffusefield (array ' mic_spec{mi}.name ', array steering vectors [space domain])']; disp(test_name); 290 | pars.grid_svecs = mic_spec{mi}.H_grid; 291 | h_diff = simulateSphArray(pars.winsize, mic_spec{mi}.dirs_rad, diff_dirs_rad, mic_spec{mi}.type, mic_spec{mi}.radius, 25, fs, mic_spec{mi}.dir_coeff); 292 | srir = matrixConvolver(diff_sigs, permute(h_diff, [1 3 2]), pars.winsize); 293 | [h_ls, h_ls_dir, h_ls_diff, analysis] = REPAIR(srir, pars); 294 | if ENABLE_PLOTTING==1 295 | PLOT_REPAIR(test_name, h_ls, h_ls_dir, h_ls_diff, pars, analysis, 0, path_for_plots); 296 | y_bin=matrixConvolver(h_ls, h_ls2bin, size(hrirs,1)); 297 | y_bin_fft = []; for ch = 1:2, y_bin_fft(:,:,ch) = fft(buffer(y_bin(:,ch),NFFT)); end %#ok 298 | figure, semilogx(0:fs/NFFT:fs/2, 20*log10(abs(squeeze(mean(abs(y_bin_fft(1:end/2+1,:,:)), 2))))) 299 | title(test_name), grid on, xlim([100 20e3]) 300 | end 301 | if ~isempty(path_for_renders) 302 | audiowrite( [path_for_renders filesep test_name '.wav'], matrixConvolver(h_ls, h_ls2bin, size(hrirs,1)), fs); 303 | end 304 | 305 | if ~strcmp(mic_spec{mi}.type, 'open') 306 | % SH domain (using broad-band SHs as steering vectors): 307 | test_name = ['TEST diffusefield (array ' mic_spec{mi}.name ', broad-band SH steering vectors)']; disp(test_name); 308 | pars.grid_svecs = getSH(mic_spec{mi}.sh_order, [pars.grid_dirs_rad(:,1) pi/2-pars.grid_dirs_rad(:,2)], 'real').'; 309 | srir_sh = matrixConvolver(srir, permute(mic_spec{mi}.e_sh, [3 2 1]), pars.winsize); 310 | [h_ls, h_ls_dir, h_ls_diff, analysis] = REPAIR(srir_sh, pars); 311 | if ENABLE_PLOTTING==1 312 | PLOT_REPAIR(test_name, h_ls, h_ls_dir, h_ls_diff, pars, analysis, 0, path_for_plots); 313 | y_bin=matrixConvolver(h_ls, h_ls2bin, size(hrirs,1)); 314 | y_bin_fft = []; for ch = 1:2, y_bin_fft(:,:,ch) = fft(buffer(y_bin(:,ch),NFFT)); end %#ok 315 | figure, semilogx(0:fs/NFFT:fs/2, 20*log10(abs(squeeze(mean(abs(y_bin_fft(1:end/2+1,:,:)), 2))))) 316 | title(test_name), grid on, xlim([100 20e3]) 317 | end 318 | if ~isempty(path_for_renders) 319 | audiowrite( [path_for_renders filesep test_name '.wav'], matrixConvolver(h_ls, h_ls2bin, size(hrirs,1)), fs); 320 | end 321 | 322 | % SH domain (using the encoded SMA steering vectors): 323 | test_name = ['TEST diffusefield (array ' mic_spec{mi}.name ', SH encoded array steering vectors)']; disp(test_name); 324 | pars.grid_svecs = mic_spec{mi}.H_grid_sh; 325 | [h_ls, h_ls_dir, h_ls_diff, analysis] = REPAIR(srir_sh, pars); 326 | if ENABLE_PLOTTING==1 327 | PLOT_REPAIR(test_name, h_ls, h_ls_dir, h_ls_diff, pars, analysis, 0, path_for_plots); 328 | y_bin=matrixConvolver(h_ls, h_ls2bin, size(hrirs,1)); 329 | y_bin_fft = []; for ch = 1:2, y_bin_fft(:,:,ch) = fft(buffer(y_bin(:,ch),NFFT)); end %#ok 330 | figure, semilogx(0:fs/NFFT:fs/2, 20*log10(abs(squeeze(mean(abs(y_bin_fft(1:end/2+1,:,:)), 2))))) 331 | title(test_name), grid on, xlim([100 20e3]) 332 | end 333 | if ~isempty(path_for_renders) 334 | audiowrite( [path_for_renders filesep test_name '.wav'], matrixConvolver(h_ls, h_ls2bin, size(hrirs,1)), fs); 335 | end 336 | end 337 | end 338 | 339 | 340 | %% Image-source based RIRs 341 | rooms = {'small', 'medium','large'}; % Options: {'small', 'medium', 'large'} 342 | 343 | % Optionally, output listening test files 344 | if ~isempty(path_for_renders_lt) 345 | % Options: 346 | ENABLE_HOSIRR_RENDERS = 1; 347 | ENABLE_SDM_RENDERS = 1; 348 | 349 | % Load anechoic stimuli 350 | addpath('../HO-SIRR') 351 | addpath('../HO-SIRR/_Stimuli_') 352 | if ~exist('HOSIRR', 'file'), error('HOSIRR not in path'); end 353 | addpath('_Stimuli_') 354 | stimuli = {'music__KickDrumClicky' 355 | 'music__Trombone' 356 | 'music__Castanets' 357 | 'speech__HarvardMale' 358 | 'music__snares'}; 359 | for si=1:length(stimuli) 360 | [srcsig_tmp, srcsig_fs] = audioread([stimuli{si} '.wav']); assert(srcsig_fs==fs) 361 | srcsig_tmp = [srcsig_tmp; zeros(fs/2,1)]; 362 | srcsig{si} = 0.7.*srcsig_tmp./max(abs(srcsig_tmp(:))); %#ok 363 | 364 | hpf_cutoff = 60; 365 | w_hh = hpf_cutoff/(fs/2); 366 | h_filt = fir1(10000, w_hh, 'high').'; 367 | srcsig{si} = fftfilt(repmat(h_filt, [1 size(srcsig{si},2)]), [srcsig{si}; zeros(5001,size(srcsig{si},2))]); %#ok 368 | srcsig{si} = srcsig{si}(5000:end,:); %#ok 369 | end 370 | 371 | % For binaural rendering 372 | h_ls2bin = 3.*permute(hrirs(:,:,findClosestGridPoints(hrir_dirs_rad, pars.ls_dirs_deg*pi/180)), [1 3 2]); 373 | 374 | % Configurations for other methods: 375 | if ENABLE_HOSIRR_RENDERS 376 | addpath('../HO-SIRR') 377 | if ~exist('HOSIRR', 'file'), error('HOSIRR not in path'); end 378 | % Default HOSIRR toolbox configuration 379 | hosirr_pars.chOrdering = 'ACN'; 380 | hosirr_pars.normScheme = 'N3D'; 381 | hosirr_pars.fs = fs; 382 | hosirr_pars.multires_winsize = 128; 383 | hosirr_pars.multires_xovers = [ ]; 384 | hosirr_pars.RENDER_DIFFUSE = 1; 385 | hosirr_pars.decorrelationType = 'noise'; 386 | hosirr_pars.BROADBAND_FIRST_PEAK = 1; 387 | hosirr_pars.BROADBAND_DIFFUSENESS = 1; 388 | hosirr_pars.maxDiffFreq_Hz = 3000; 389 | hosirr_pars.alpha_diff = 0.5; 390 | hosirr_pars.ls_dirs_deg = pars.ls_dirs_deg; 391 | end 392 | if ENABLE_SDM_RENDERS 393 | if ~exist('createSDMStruct', 'file'), error('SDM not in path'); end 394 | % Default SDM toolbox space-domain configuration 395 | sdm_pars = createSDMStruct('micLocs', [0.025 0 0; -0.025 0 0; 0 0.025 0; 0 -0.025 0; 0 0 0.025; 0 0 -0.025], 'fs',fs); 396 | ls_dirs_deg_r = [pars.ls_dirs_deg ones(size(pars.ls_dirs_deg,1),1)]; % add radius 397 | end 398 | end 399 | 400 | ls_dirs_rad = pars.ls_dirs_deg*pi/180; 401 | 402 | for ri = 1:length(rooms) 403 | room = rooms{ri}; 404 | 405 | % Configure room 406 | switch room 407 | case 'small' 408 | room_dims = [6 5 3.1]; 409 | rt60 = [0.25 0.3 0.2 0.15 0.05 0.03].*1.3; 410 | case 'medium' 411 | room_dims = [10 6 3.5]; 412 | rt60 = [0.4 0.45 0.3 0.15 0.12 0.1].*1.3; 413 | case 'large' 414 | room_dims = [12 7.4 3.5]; 415 | rt60 = [0.4 0.47 0.35 0.18 0.13 0.12].*1.4; 416 | end 417 | abs_wall_ratios = [0.75 0.86 0.56 0.95 0.88 1]; 418 | nBands = length(rt60); 419 | band_centerfreqs = zeros(nBands,1); 420 | band_centerfreqs(1) = 125; 421 | for nb=2:nBands, band_centerfreqs(nb) = 2*band_centerfreqs(nb-1); end 422 | abs_wall = findAbsCoeffsFromRT(room_dims, rt60,abs_wall_ratios); 423 | [RT60_sabine, d_critical] = room_stats(room_dims, abs_wall); 424 | rec_pos_xyz = room_dims/2 + [-0.42 -0.44 0.18].*1.3; 425 | src_pos_xyz = rec_pos_xyz + [2 0.24 -0.15].*1.1; 426 | 427 | % Generate reference loudspeaker RIRs 428 | type = 'maxTime'; % 'maxTime' 'maxOrder' 429 | maxlim = max(rt60).*ones(size(rt60))*2; % just cut if it's longer than that ( or set to max(rt60) ) 430 | src_pos_xyz2 = [src_pos_xyz(:,1) room_dims(2)-src_pos_xyz(:,2) src_pos_xyz(:,3)]; % change y coord for src/rec due to convention inside the IMS function 431 | rec_pos_xyz2 = [rec_pos_xyz(:,1) room_dims(2)-rec_pos_xyz(:,2) rec_pos_xyz(:,3)]; % change y coord for src/rec due to convention inside the IMS function 432 | abs_echograms = compute_echograms_arrays(room_dims, src_pos_xyz2, rec_pos_xyz2, abs_wall, maxlim); 433 | grid_cell{1} = ls_dirs_rad; 434 | array_irs{1} = [permute(eye(size(ls_dirs_rad,1)), [3 1 2]); zeros(7,size(ls_dirs_rad,1),size(ls_dirs_rad,1));]; % zero pad 435 | x_rir = render_array_rirs(abs_echograms, band_centerfreqs, fs, grid_cell, array_irs); x_rir = x_rir{1}; % remove rec cell 436 | x_rir = x_rir((500+8):end,:); % remove FIR filter incurred delays 437 | for ii=1:size(x_rir,2), x_rir_air(:,ii) = applyAirAbsorption(x_rir(:,ii), fs); end %#ok 438 | x_rir = x_rir_air;clear x_rir_air 439 | if ENABLE_PLOTTING==1 440 | figure, mesh(max(20*log10(abs(x_rir)), -49.8)) 441 | colormap('jet'), xlim([1, size(pars.ls_dirs_deg,1)]), zlim([-50,10 ]) 442 | title(['reference ' room ' room loudspeaker RIR']), xlabel('loudspeaker #'), zlabel('energy, dB'), ylabel('time, samples') 443 | end 444 | if ~isempty(path_for_renders) 445 | audiowrite( [path_for_renders filesep 'TEST ' room ' room reference' '.wav'], matrixConvolver(x_rir, h_ls2bin, size(hrirs,1)), fs, 'BitsPerSample', 32); 446 | end 447 | if ~isempty(path_for_renders_lt) 448 | for si=1:length(stimuli) 449 | yi = fftfilt(x_rir, repmat(srcsig{si}, [1 size(pars.ls_dirs_deg,1)])); 450 | %audiowrite( [path_for_renders_lt filesep room ' ' stimuli{si} ' ref' '.wav'], yi, fs, 'BitsPerSample', 32); 451 | audiowrite( [path_for_renders_lt_bin filesep room ' ' stimuli{si} ' ref' '.wav'], matrixConvolver(yi, h_ls2bin, size(hrirs,1)), fs, 'BitsPerSample', 32); 452 | end 453 | end 454 | 455 | % Obtain Oracle parameters 456 | maxK_oracle = 8; 457 | time_s = abs_echograms(1,1,1).time; 458 | doas_s = abs_echograms(1,1,1).coords; 459 | values = abs_echograms(1,1,1).value; 460 | time_hop_index = floor((time_s*fs)/(pars.winsize/2)); 461 | for ii=1:time_hop_index(end) 462 | idxs = find(ii==time_hop_index); 463 | if ~isempty(idxs) 464 | [~,sortdec] = sort(values(idxs),'descend'); 465 | time_s_tmp = time_s(idxs); 466 | doas_s_tmp = doas_s(idxs,:); 467 | values_tmp = values(idxs); 468 | time_s(idxs) = time_s_tmp(sortdec); 469 | doas_s(idxs,:) = doas_s_tmp(sortdec,:); 470 | values(idxs) = values_tmp(sortdec); 471 | end 472 | end 473 | nFrames = time_hop_index(end)+40; 474 | analysis_oracle.K = zeros(1,nFrames); 475 | analysis_oracle.est_idx = nan(maxK_oracle,nFrames); 476 | for i=1:length(time_hop_index) 477 | index = time_hop_index(i); 478 | src_dir_rad = unitCart2sph(doas_s(i,:)); 479 | est_idx = findClosestGridPoints(pars.grid_dirs_rad, src_dir_rad); 480 | if analysis_oracle.K(1,index)1, ybin = ybin./max(abs(ybin(:))); end 500 | audiowrite( [path_for_renders_lt_bin filesep room ' ' stimuli{si} ' idealSH o' num2str(ideal_SH_order) ' ' pars.Kestimator ' REPAIR SHD.wav'], ybin, fs, 'BitsPerSample', 32); 501 | end 502 | end 503 | 504 | % HOSIRR Ideal SH receiver 505 | if ENABLE_HOSIRR_RENDERS 506 | case_name = ['HOSIRR ' room ' room (ideal SH receiver, order ' num2str(ideal_SH_order) ')']; disp(case_name); 507 | [h_ls, h_ls_dir, h_ls_diff, analysis] = HOSIRR(x_rir_sh, hosirr_pars); 508 | if ENABLE_PLOTTING==1, PLOT_REPAIR(case_name, h_ls, h_ls_dir, h_ls_diff, pars, [], 1, path_for_plots); end 509 | for si=1:length(stimuli) 510 | yi = fftfilt(h_ls, repmat(srcsig{si},[1 size(hosirr_pars.ls_dirs_deg,1)])) * 2; 511 | %audiowrite( [path_for_renders_lt filesep room ' ' stimuli{si} ' idealSH o' num2str(ideal_SH_order) ' HOSIRR SHD.wav'], yi, fs, 'BitsPerSample', 32); 512 | audiowrite( [path_for_renders_lt_bin filesep room ' ' stimuli{si} ' idealSH o' num2str(ideal_SH_order) ' HOSIRR SHD.wav'], matrixConvolver(yi, h_ls2bin, size(hrirs,1)), fs, 'BitsPerSample', 32); 513 | end 514 | 515 | case_name = ['HOSIRR ' room ' room (ideal SH receiver, order ' num2str(1) ')']; disp(case_name); 516 | [h_ls, h_ls_dir, h_ls_diff, analysis] = HOSIRR(x_rir_sh(:,1:4), hosirr_pars); 517 | if ENABLE_PLOTTING==1, PLOT_REPAIR(case_name, h_ls, h_ls_dir, h_ls_diff, pars, [], 1, path_for_plots); end 518 | for si=1:length(stimuli) 519 | yi = fftfilt(h_ls, repmat(srcsig{si},[1 size(hosirr_pars.ls_dirs_deg,1)])) * 2; 520 | %audiowrite( [path_for_renders_lt filesep room ' ' stimuli{si} ' idealSH o' num2str(ideal_SH_order) ' HOSIRR SHD.wav'], yi, fs, 'BitsPerSample', 32); 521 | audiowrite( [path_for_renders_lt_bin filesep room ' ' stimuli{si} ' idealSH o' num2str(1) ' HOSIRR SHD.wav'], matrixConvolver(yi, h_ls2bin, size(hrirs,1)), fs, 'BitsPerSample', 32); 522 | end 523 | end 524 | 525 | % Now loop over the microphone arrays 526 | for mi = 1:length(mic_arrays) 527 | % Space domain: 528 | test_name = ['TEST ' room ' room (array ' mic_spec{mi}.name ' ' pars.Kestimator ', array steering vectors [space domain])']; disp(test_name); 529 | pars.grid_svecs = mic_spec{mi}.H_grid; 530 | h_ls2mic = simulateSphArray(1024, mic_spec{mi}.dirs_rad, ls_dirs_rad, mic_spec{mi}.type, mic_spec{mi}.radius, 50, fs, mic_spec{mi}.dir_coeff); 531 | srir = matrixConvolver(x_rir, permute(h_ls2mic, [1 3 2]), 1024); 532 | srir = srir(512:512+size(x_rir,1),:); 533 | if ENABLE_PLOTTING, figure, plot(srir), end 534 | [h_ls, h_ls_dir, h_ls_diff, analysis] = REPAIR(srir, pars); 535 | if ENABLE_PLOTTING==1, PLOT_REPAIR(test_name, h_ls, h_ls_dir, h_ls_diff, pars, analysis, 0, path_for_plots); end 536 | if ~isempty(path_for_renders_lt) 537 | for si=1:length(stimuli) 538 | yi = fftfilt(h_ls, repmat(srcsig{si},[1 size(pars.ls_dirs_deg,1)])); 539 | %audiowrite( [path_for_renders_lt filesep room ' ' stimuli{si} ' ' mic_spec{mi}.name ' ' pars.Kestimator ' REPAIR SD' '.wav'], yi, fs, 'BitsPerSample', 32); 540 | ybin = matrixConvolver(yi, h_ls2bin, size(hrirs,1)); 541 | if max(abs(ybin(:)))>1, ybin = ybin./max(abs(ybin(:))); end 542 | audiowrite( [path_for_renders_lt_bin filesep room ' ' stimuli{si} ' ' mic_spec{mi}.name ' ' pars.Kestimator ' REPAIR SD' '.wav'], ybin, fs, 'BitsPerSample', 32); 543 | end 544 | end 545 | 546 | if ~strcmp(mic_spec{mi}.type, 'open') 547 | % SH domain (using broad-band SHs as steering vectors): 548 | test_name = ['TEST ' room ' room (array ' mic_spec{mi}.name ', broad-band SH steering vectors)']; disp(test_name); 549 | pars.grid_svecs = getSH(mic_spec{mi}.sh_order, [pars.grid_dirs_rad(:,1) pi/2-pars.grid_dirs_rad(:,2)], 'real').'; 550 | srir_sh = matrixConvolver(srir, permute(mic_spec{mi}.e_sh, [3 2 1]), pars.winsize); 551 | srir_sh = srir_sh((pars.winsize/2):(pars.winsize/2)+size(x_rir,1),:); 552 | if ENABLE_PLOTTING, figure, plot(srir_sh), end 553 | [h_ls, h_ls_dir, h_ls_diff, analysis] = REPAIR(srir_sh, pars); 554 | if ENABLE_PLOTTING==1, PLOT_REPAIR(test_name, h_ls, h_ls_dir, h_ls_diff, pars, analysis, 0, path_for_plots); end 555 | if ~isempty(path_for_renders_lt) 556 | for si=1:length(stimuli) 557 | yi = fftfilt(h_ls, repmat(srcsig{si},[1 size(pars.ls_dirs_deg,1)])); 558 | %audiowrite( [path_for_renders_lt filesep room ' ' stimuli{si} ' ' mic_spec{mi}.name ' REPAIR SHD BB' '.wav'], yi, fs, 'BitsPerSample', 32); 559 | ybin = matrixConvolver(yi, h_ls2bin, size(hrirs,1)); 560 | if max(abs(ybin(:)))>1, ybin = ybin./max(abs(ybin(:))); end 561 | audiowrite( [path_for_renders_lt_bin filesep room ' ' stimuli{si} ' ' mic_spec{mi}.name ' REPAIR SHD BB' '.wav'], ybin, fs, 'BitsPerSample', 32); 562 | end 563 | end 564 | 565 | % SH domain (using the encoded SMA steering vectors): 566 | test_name = ['TEST ' room ' room (array ' mic_spec{mi}.name ', SH encoded array steering vectors)']; disp(test_name); 567 | pars.grid_svecs = mic_spec{mi}.H_grid_sh; 568 | [h_ls, h_ls_dir, h_ls_diff, analysis] = REPAIR(srir_sh, pars); 569 | if ENABLE_PLOTTING==1, PLOT_REPAIR(test_name, h_ls, h_ls_dir, h_ls_diff, pars, analysis, 0, path_for_plots); end 570 | if ~isempty(path_for_renders_lt) 571 | for si=1:length(stimuli) 572 | yi = fftfilt(h_ls, repmat(srcsig{si},[1 size(pars.ls_dirs_deg,1)])); 573 | %audiowrite( [path_for_renders_lt filesep room ' ' stimuli{si} ' ' mic_spec{mi}.name ' REPAIR SHD E' '.wav'], yi, fs, 'BitsPerSample', 32); 574 | ybin = matrixConvolver(yi, h_ls2bin, size(hrirs,1)); 575 | if max(abs(ybin(:)))>1, ybin = ybin./max(abs(ybin(:))); end 576 | audiowrite( [path_for_renders_lt_bin filesep room ' ' stimuli{si} ' ' mic_spec{mi}.name ' REPAIR SHD E' '.wav'], ybin, fs, 'BitsPerSample', 32); 577 | end 578 | end 579 | 580 | % HOSIRR SH domain 581 | if ENABLE_HOSIRR_RENDERS 582 | case_name = ['HOSIRR ' room ' room (' mic_spec{mi}.name ' broad-band SH steering vectors)']; disp(case_name); 583 | [h_ls, h_ls_dir, h_ls_diff, analysis] = HOSIRR(srir_sh, hosirr_pars); 584 | if ENABLE_PLOTTING==1, PLOT_REPAIR(case_name, h_ls, h_ls_dir, h_ls_diff, pars, [], 1, path_for_plots); end 585 | for si=1:length(stimuli) 586 | yi = fftfilt(h_ls, repmat(srcsig{si},[1 size(hosirr_pars.ls_dirs_deg,1)])); 587 | %audiowrite( [path_for_renders_lt filesep room ' ' stimuli{si} ' ' mic_spec{mi}.name ' HOSIRR SHD' '.wav'], yi, fs, 'BitsPerSample', 32); 588 | audiowrite( [path_for_renders_lt_bin filesep room ' ' stimuli{si} ' ' mic_spec{mi}.name ' HOSIRR SHD' '.wav'], matrixConvolver(yi, h_ls2bin, size(hrirs,1)), fs, 'BitsPerSample', 32); 589 | end 590 | end 591 | end 592 | 593 | if strcmp(mic_spec{mi}.name, 'intensity-probe') && ENABLE_SDM_RENDERS 594 | case_name = ['SDM ' room ' room (' mic_spec{mi}.name ' Space-domain']; disp(case_name); 595 | DOA = SDMPar(srir, sdm_pars); 596 | P = srir(:, 5); 597 | s = createSynthesisStruct('lspLocs',ls_dirs_deg_r,'snfft',length(P), 'ShowArray',false,'fs',fs,'c',343, 'LFEchannel',[]); 598 | h_ls = synthesizeSDMCoeffs(P, DOA, s); 599 | if ENABLE_PLOTTING==1, PLOT_REPAIR(case_name, h_ls, h_ls, zeros(size(h_ls)), pars, [], 1, path_for_plots); end 600 | for si=1:length(stimuli) 601 | yi = fftfilt(h_ls, repmat(srcsig{si},[1 size(hosirr_pars.ls_dirs_deg,1)])); 602 | %audiowrite( [path_for_renders_lt filesep room ' ' stimuli{si} ' ' mic_spec{mi}.name ' SDM' '.wav'], yi, fs, 'BitsPerSample', 32); 603 | audiowrite( [path_for_renders_lt_bin filesep room ' ' stimuli{si} ' ' mic_spec{mi}.name ' SDM' '.wav'], matrixConvolver(yi, h_ls2bin, size(hrirs,1)), fs, 'BitsPerSample', 32); 604 | end 605 | end 606 | end 607 | end 608 | -------------------------------------------------------------------------------- /REPAIR.m: -------------------------------------------------------------------------------- 1 | % ------------------------------------------------------------------------- 2 | % This file is part of REPAIR 3 | % Copyright (c) 2021 - Leo McCormack, Archontis Politis & Nils Meyer-Kahlen 4 | % 5 | % REPAIR is free software; you can redistribute it and/or modify it under 6 | % the terms of the GNU General Public License as published by the Free 7 | % Software Foundation; either version 2 of the License, or (at your option) 8 | % any later version. 9 | % 10 | % REPAIR is distributed in the hope that it will be useful, but WITHOUT 11 | % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12 | % FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 13 | % more details. 14 | % 15 | % See for a copy of the GNU General Public 16 | % License. 17 | % ------------------------------------------------------------------------- 18 | 19 | function [lsir, lsir_ndiff, lsir_diff, analysis] = REPAIR(srir, pars, analysis_oracle) 20 | % Reproduction and Parameterisation of Array Impulse Responses (REPAIR) [1] 21 | % ------------------------------------------------------------------------- 22 | % 23 | % DEPENDENCES 24 | % Spherical-Harmonic-Transform Matlab library 25 | % https://github.com/polarch/Spherical-Harmonic-Transform 26 | % Higher-Order-Ambisonics Matlab library 27 | % https://github.com/polarch/Higher-Order-Ambisonics 28 | % Vector-Base-Amplitude-Panning 29 | % https://github.com/polarch/Vector-Base-Amplitude-Panning 30 | % 31 | % INPUT ARGUMENTS 32 | % srir : input microphone array/spatial RIR; signalLength x nCH 33 | % pars.grid_svecs : array steering vectors; nCH x nDirs 34 | % pars.grid_dirs_xyz : measurement grid for the array steering vectors (unit-length Cartesian vectors); nDirs x 3 35 | % pars.grid_dirs_rad : measurement grid for the array steering vectors (in Spherical coordinates, radians); nDirs x 2 36 | % pars.grid_weights : integration weights; nDirs x 1 37 | % pars.fs : sampling rate in Hz 38 | % pars.SCMavgOption : options: {'block', 'recur', 'alltime'} 39 | % pars.SCMavg_coeff : temporal averaging coefficient, [0..1], if SCMavgOption is set to "recur" 40 | % pars.SCMavg_Nframes : number of frames in each averaging block, if SCMavgOption is set to "block" 41 | % pars.Kestimator : options: {'SORTE', 'SORTED', 'RECON', 'ORACLE'} 42 | % pars.DoAestimator : options: {'MUSIC', 'SRP', 'ORACLE'} 43 | % pars.winsize : window size, in time-domain samples 44 | % pars.freqGrouping : options: {'broadband', 'octave', 'erb', 'fullres'} 45 | % pars.streamBalance : 0: only diffuse stream, 1: both streams are balanced, 2: only direct stream 46 | % pars.ENABLE_DIFF_WHITENING : flag (0 or 1), applies an operation that diagonalises the SCMs when under diffuse conditions 47 | % pars.ENABLE_COHERENT_FOCUSING : flag (0 or 1), only used if the steering vectors are frequency dependent, and if there is some band grouping 48 | % pars.ENABLE_AMBIENT_ENERGY_PRESERVATION : flag (0 or 1), forces the beamformers used for the ambient stream to be energy-preserving over the sphere 49 | % pars.decorrelation : options: {'off', 'convNoise', 'shapedNoise', 'phaseRand', 'covMatch'} 50 | % pars.beamformerOption : options: {'pinv', 'MF', 'SD'} 51 | % pars.ENABLE_QUANTISE_TO_NEAREST_LS : flag (0 or 1), quantise to nearest loudspeaker instead of using VBAP 52 | % pars.maxAnaFreq_Hz : above this frequency, everything is treated as one band 53 | % pars.ls_dirs_deg : loudspeaker directions, in degrees; nLS x 2 54 | % pars.vbapNorm : 0:reverberant room, ~0.5: dry listening room, 1: anechoic 55 | % analysis_oracle.K : (optional) known number of reflections (otherwise this is estimated) 56 | % analysis_oracle.est_idx : (optional) known DoA indices (otherwise they are estimated) 57 | % 58 | % OUTPUT ARGUMENTS 59 | % lsir : loudspeaker impulse responses; signalLength x nLS 60 | % lsir_ndiff : non-diffuse stream only; signalLength x nLS 61 | % lsir_diff : diffuse stream only; signalLength x nLS 62 | % analysis.K : (optional) estimated number of reflections 63 | % analysis.azim : (optional) estimated reflection azimuths 64 | % analysis.elev : (optional) estimated reflection elevations 65 | % 66 | % REFERENCES 67 | % [1] McCormack, L., Meyer-Kahlen, N., and Politis, A. (2023). "Spatial 68 | % Reconstruction-Based Rendering of Microphone Array Room Impulse 69 | % Responses". Journal of the Audio Engineering Society, 71(5), pp.267-280. 70 | % 71 | % ------------------------------------------------------------------------- 72 | % 73 | % Leo McCormack, 28/10/2021, leo.mccormack@aalto.fi, with contributions 74 | % from Archontis Politis and Nils Meyer-Kahlen 75 | % 76 | % ------------------------------------------------------------------------- 77 | 78 | maxNumReflections = 8; % An arbitrary limit on the maximum number of simultaneous reflections 79 | nCH = size(srir,2); 80 | nLS = size(pars.ls_dirs_deg,1); 81 | nGrid = size(pars.grid_dirs_rad,1); 82 | maxK = min(floor(nCH/2), maxNumReflections); 83 | 84 | %%% Defaults/Warnings/Errors etc. 85 | if strcmp(pars.DoAestimator, 'ORACLE') || strcmp(pars.Kestimator, 'ORACLE') 86 | if nargin<3, error('DoAestimator and/or Kestimator is set to ORACLE, but no Oracle analysis struct is given to REPAIR.'), end 87 | end 88 | if strcmp(pars.DoAestimator, 'ORACLE') && ~strcmp(pars.Kestimator, 'ORACLE') 89 | error('How would that work?') 90 | end 91 | if (abs(1-sum(pars.grid_weights))>0.001), error('grid_weights must sum to 1'), end 92 | if (~isfield(pars, 'fs')), error('Please specify "fs"'); end 93 | if (~isfield(pars, 'ls_dirs_deg')) 94 | error('Please specify "ls_dirs_deg", in degrees') 95 | end 96 | 97 | disp('REPAIR Configuration:'), pars %#ok 98 | 99 | if length(size(pars.grid_svecs))==2, BROAD_BAND_SVECS=1; else, BROAD_BAND_SVECS=0; end 100 | 101 | %%% Intialisations 102 | fprintf('REPAIR: \n - Initialising...\n'); 103 | 104 | lSig = size(srir,1); 105 | winsize = pars.winsize; 106 | fftsize = 2*winsize; % double the window size for FD convolution 107 | hopsize = winsize/2; % half the window size time-resolution 108 | nBins_anl = winsize/2 + 1; % nBins used for analysis 109 | nBins_syn = fftsize/2 + 1; % nBins used for analysis 110 | centrefreqs_anl = (0:winsize/2)'*pars.fs/winsize; 111 | 112 | % Frequency grouping 113 | switch pars.freqGrouping 114 | case 'broadband' 115 | [~,ind_1kHz] = min(abs(centrefreqs_anl-1000)); 116 | freqGrpInd = [1 nBins_anl]; 117 | case 'octave' 118 | [freqGrpInd, analysis.grpCentreFreq] = findOctavePartitions(centrefreqs_anl, 20e3); 119 | case 'erb' 120 | [freqGrpInd] = findERBpartitions(centrefreqs_anl, 20e3); 121 | case 'fullres' 122 | freqGrpInd = [1:nBins_anl nBins_anl]; 123 | end 124 | nFreqGrps = length(freqGrpInd)-1; 125 | 126 | % Replicate grid_svecs per bin for coding convenience 127 | if BROAD_BAND_SVECS, grid_svecs = repmat(pars.grid_svecs, [1 1 nBins_anl]); 128 | else, grid_svecs = pars.grid_svecs; end 129 | 130 | % Diffuse coherence matrix (DCM) 131 | DFCmtx = zeros(nCH, nCH, nBins_anl); 132 | for nb = 1:nBins_anl 133 | tmp = grid_svecs(:,:,nb); 134 | DFCmtx(:,:,nb) = tmp*diag(pars.grid_weights)*tmp'; 135 | end 136 | 137 | % Steering vector alignment over frequency 138 | for grp=1:nFreqGrps 139 | if grpnCH, A_p = V(:,1:nCH)*U'; 241 | else, A_p = V*U(:,1:nLS)'; end 242 | M_diff(:,:,nb) = sqrt(N_norm)*A_p; 243 | % Note: If in the SH domain, this reverts to Energy-Preserving Ambisonic Decoding (EPAD) 244 | else 245 | M_diff(:,:,nb) = sqrt(nLS./nCH).*(nCH/nLS).*(N_norm*Ad'); 246 | % Note: If in the SH domain, this reverts to Sampling Ambisonic Decoding (SAD) 247 | end 248 | 249 | % Diffuse-field equalise 250 | diff_eq(nb,1) = sqrt(1./real(trace(DFCmtx(:,:,nb))./nCH+eps)); 251 | if nb==2 && (diff_eq(nb,1)>4 || diff_eq(nb,1)<0.25), error('badly scaled steering vectors'), end 252 | M_diff(:,:,nb) = max(min(diff_eq(nb,1), 4), 0.25).*M_diff(:,:,nb); % Max +/-12dB boost 253 | end 254 | 255 | % Covariance matching based decorrelation uses M_diff as the protoype 256 | if strcmp(pars.decorrelation, 'covMatch') 257 | M_proto = interpolateFilters(M_diff, fftsize); 258 | M = zeros(nLS, nLS, nBins_anl); 259 | Mr = zeros(nLS, nLS, nBins_anl); 260 | end 261 | 262 | %%% Pre-processing of SRIR 263 | % transform window (hanning) 264 | x = 0:(winsize-1); 265 | win = sin(x.*(pi/winsize))'.^2; 266 | 267 | % zero pad the signal's start and end for the 50% overlap STFT, and account for the temporal averaging: 268 | srir = [zeros(pars.winsize*2, nCH); srir; zeros(fftsize*2, nCH)]; 269 | lSig_pad = size(srir,1); 270 | nFrames = ceil((lSig_pad + pars.winsize)/hopsize)+1; 271 | 272 | % Pre-compute input spectra 273 | idx = 1; 274 | framecount = 1; 275 | inspec_frame = zeros(nBins_syn, nCH, nFrames); 276 | while idx + fftsize <= lSig_pad 277 | insig_win = win*ones(1,nCH) .* srir(idx+(0:winsize-1),:); 278 | inspec_tmp = fft(insig_win, fftsize); 279 | inspec_frame(:,:,framecount) = inspec_tmp(1:nBins_syn,:); % keep up to nyquist 280 | idx = idx + hopsize; 281 | framecount = framecount + 1; 282 | end 283 | 284 | % Pre-compute time-averaged SCMs (any frequency averaging is done in main loop) 285 | Cxx_frame = zeros(nCH, nCH, nBins_anl, nFrames); 286 | switch pars.SCMavgOption 287 | case 'block' 288 | SCMavg_Nframes = pars.SCMavg_Nframes; 289 | if mod(SCMavg_Nframes,2)==0, SCMavg_Nframes = SCMavg_Nframes+1; end 290 | cvxm_frameBuffer = zeros(nBins_anl, nCH, SCMavg_Nframes); 291 | for framecount=1:nFrames 292 | inspec_anl = inspec_frame(1:fftsize/winsize:end,:,framecount); 293 | cvxm_frameBuffer(:,:,2:end) = cvxm_frameBuffer(:,:,1:end-1); 294 | cvxm_frameBuffer(:,:,1) = inspec_anl; 295 | cvxm_nFramesHalf = floor(SCMavg_Nframes/2); 296 | if framecount>cvxm_nFramesHalf 297 | for nb = 1:nBins_anl 298 | in = permute(cvxm_frameBuffer(nb,:,:), [ 2 3 1]); 299 | Cxx_frame(:,:,nb,framecount) = in*in'; 300 | end 301 | end 302 | end 303 | 304 | case 'recur' 305 | for framecount=1:nFrames 306 | inspec_anl = inspec_frame(1:fftsize/winsize:end,:,framecount); 307 | for nb = 1:nBins_anl 308 | in = permute(inspec_anl(nb,:,:), [2 3 1]); 309 | new_Cxx = in*in'; 310 | if framecount==1 311 | Cxx_frame(:,:,nb,1) = (1-pars.SCMavg_coeff).*new_Cxx; 312 | else 313 | Cxx_frame(:,:,nb,framecount) = pars.SCMavg_coeff.*Cxx_frame(:,:,nb,framecount-1) + (1-pars.SCMavg_coeff).*new_Cxx; 314 | end 315 | end 316 | end 317 | 318 | case 'alltime' 319 | Cxx_avg = zeros(nCH, nCH, nBins_anl); 320 | inspec_anl = inspec_frame(1:fftsize/winsize:end,:,:); 321 | for nb = 1:nBins_anl 322 | in = permute(inspec_anl(nb,:,:), [2 3 1]); 323 | Cxx_avg(:,:,nb) = in*in'; 324 | end 325 | Cxx_frame = repmat(Cxx_avg, [1 1 1 nFrames]); 326 | 327 | otherwise 328 | error('unsuported pars.SCMavgOption') 329 | end 330 | 331 | % "Zero-pad" Oracle parameters 332 | if strcmp(pars.DoAestimator, 'ORACLE') || strcmp(pars.Kestimator, 'ORACLE') 333 | delay_analysis = 2*winsize/hopsize; % Since srir is zero padded at the start by 2*winsize, we'll do the same here 334 | analysis_oracle.K = [zeros(1,delay_analysis) analysis_oracle.K ]; 335 | analysis_oracle.est_idx = [nan(size(analysis_oracle.est_idx,1),delay_analysis) analysis_oracle.est_idx]; 336 | assert(size(analysis_oracle.K,2) >= nFrames, "Oracle data does not cover at least the input length!") 337 | assert(size(analysis_oracle.est_idx,2) >= nFrames, "Oracle data does not cover at least the input length!") 338 | end 339 | 340 | %%% Main processing loop 341 | idx = 1; 342 | framecount = 1; 343 | progress = 1; 344 | 345 | % storage for estimated parameters 346 | if nargout==4 347 | analysis.K = zeros(ceil(lSig_pad/hopsize),nBins_anl); 348 | analysis.diffuseness = zeros(ceil(lSig_pad/hopsize),nBins_anl); 349 | analysis.azim = nan(ceil(lSig_pad/hopsize),maxK,nBins_anl); 350 | analysis.elev = nan(ceil(lSig_pad/hopsize),maxK,nBins_anl); 351 | analysis.energy_in = zeros(ceil(lSig_pad/hopsize),nBins_anl); 352 | analysis.energy_ndiff = zeros(ceil(lSig_pad/hopsize),nBins_anl); 353 | analysis.energy_diff = zeros(ceil(lSig_pad/hopsize),nBins_anl); 354 | analysis.energy_total = zeros(ceil(lSig_pad/hopsize),nBins_anl); 355 | end 356 | 357 | % Signal buffers, mixing matrices etc. 358 | Ms = zeros(nLS, nCH, nBins_anl); 359 | Md = zeros(nLS, nCH, nBins_anl); 360 | lsir_ndiff = zeros(lSig_pad, nLS); 361 | lsir_diff = zeros(lSig_pad, nLS); 362 | outspec_ndiff = zeros(nBins_syn, nLS); 363 | outspec_diff = zeros(nBins_syn, nLS); 364 | 365 | fprintf(' - Rendering: ') 366 | while idx + fftsize <= lSig_pad 367 | % Load spectra and Cxx for this frame 368 | inspec_syn = inspec_frame(:,:,framecount); 369 | Cxx = Cxx_frame(:,:,:,framecount); 370 | 371 | for grp = 1:nFreqGrps 372 | if grp4, KK = min([SORTE(lambda) maxK]); 389 | else, KK = 1; end 390 | case 'SORTED' 391 | if nCH>4, KK = min([SORTE(lambda) K_comedie maxK]); 392 | else, KK = min([maxK K_comedie]); end 393 | case 'RECON' 394 | KK=0:maxK; 395 | case 'ORACLE' 396 | KK = analysis_oracle.K(1,framecount); 397 | end 398 | 399 | % Source DOA estimation 400 | grid_svec_v0 = grid_svecs(:,:,v0_ind(grp)); 401 | j=1; est_idx = {}; 402 | switch pars.DoAestimator 403 | case 'MUSIC' 404 | [V,~] = sorted_eig(Cxx_WINGS); 405 | for K=KK 406 | if K>0 407 | Vn = V(:,K+1:end); 408 | pmap = sphMUSIC(grid_svec_v0, Vn); 409 | est_idx{j} = peakFind2d(pmap, pars.grid_dirs_rad, pars.grid_dirs_xyz, K); 410 | else 411 | est_idx{j} = []; 412 | end 413 | j=j+1; 414 | end 415 | case 'SRP' 416 | pmap = real(diag(grid_svec_v0'*Cxx_WINGS*grid_svec_v0)).*pmap_diffEQ(:,grp); 417 | for K=KK 418 | if K>0 419 | est_idx{j} = peakFind2d(pmap, pars.grid_dirs_rad, pars.grid_dirs_xyz, K); 420 | else 421 | est_idx{j} = []; 422 | end 423 | j=j+1; 424 | end 425 | case 'ORACLE' 426 | est_idx{1} = analysis_oracle.est_idx(1:KK,framecount); 427 | end 428 | 429 | % If using the "reconnaissance" approach (i.e. the brute-force reconstruction error based determination of K) 430 | switch pars.Kestimator 431 | case {'SORTE', 'SORTED', 'ORACLE'} 432 | K = KK; 433 | est_idx = est_idx{1}; 434 | case 'RECON' 435 | j=1; 436 | for K=KK 437 | % Obtain mixing matrices 438 | if K==0 439 | recon_Ms = zeros(nCH); 440 | recon_Md = eye(nCH); 441 | else 442 | Gs = grid_svecs(:,est_idx{j},v0_ind(grp)); 443 | Gd = eye(nCH); 444 | [recon_Ms, recon_Md] = constructMixingMatrices(pars.beamformerOption, grid_svecs(:,est_idx{j},v0_ind(grp)), DFCmtx_grp(:,:,grp), Gs, Gd, pars.streamBalance); 445 | end 446 | 447 | % Use to reconstruct the input 448 | Cxx_norm = Cxx_WINGS./(trace(Cxx_WINGS)+eps); 449 | Cxx_recon_s = recon_Ms*Cxx_norm*recon_Ms'; 450 | Cxx_recon_d = T_unwhiten(:,:,grp)*(recon_Md*Cxx_norm*recon_Md' .* eye(nCH))*T_unwhiten(:,:,grp)'; % Assuming perfect decorrelation, so more important that channel energies are OK 451 | Cxx_recon = Cxx_recon_s + Cxx_recon_d; 452 | recon_err(j, 1) = trace((Cxx_norm-Cxx_recon)*(Cxx_norm-Cxx_recon)'); 453 | 454 | j=j+1; 455 | end 456 | 457 | % Determine optimal K 458 | [~,min_ind] = min(recon_err); 459 | est_idx = est_idx{min_ind}; 460 | K = KK(min_ind); 461 | end 462 | 463 | % CONSTRUCT REPAIR SYNTHESIS MATRICES 464 | if K==0 % Bypass direct stream if there are no reflections 465 | for nb = grp_bins 466 | Ms(:,:,nb) = zeros(nLS,nCH); 467 | Md(:,:,nb) = M_diff(:,:,nb); 468 | end 469 | else 470 | for nb = grp_bins 471 | % Construct mixing matrices using VBAP gains for target setup 472 | Gs = spat_gains(est_idx,:).'; 473 | if ~pars.ENABLE_QUANTISE_TO_NEAREST_LS && pars.vbapNorm>0 474 | Gs = Gs./(ones(nLS,1) * sum(Gs.^vbap_pValue(nb)).^(1/vbap_pValue(nb))); 475 | end 476 | [Ms(:,:,nb), Md(:,:,nb)] = constructMixingMatrices(pars.beamformerOption, grid_svecs(:,est_idx,nb), DFCmtx(:,:,nb), Gs, M_diff(:,:,nb), pars.streamBalance); 477 | end 478 | end 479 | 480 | % for optional plotting 481 | if nargout==4 482 | for nb = grp_bins 483 | analysis.K(framecount,nb) = K; 484 | analysis.KperGroup(framecount,grp) = K; 485 | analysis.diffuseness(framecount,nb) = diff_comedie; 486 | if K>0 487 | analysis.azim(framecount,1:K,nb) = pars.grid_dirs_rad(est_idx,1); 488 | analysis.elev(framecount,1:K,nb) = pars.grid_dirs_rad(est_idx,2); 489 | analysis.azimPerGroup(framecount,1:K,grp) = pars.grid_dirs_rad(est_idx,1); 490 | analysis.elevPerGroup(framecount,1:K,grp) = pars.grid_dirs_rad(est_idx,2); 491 | 492 | end 493 | end 494 | end 495 | end 496 | 497 | % for optional plotting 498 | if nargout==4 499 | for nb=1:nBins_anl 500 | analysis.energy_in(framecount,nb) = real(trace(Cxx(:,:,nb))); 501 | analysis.energy_ndiff(framecount,nb) = real(trace(Ms(:,:,nb)*Cxx(:,:,nb)*Ms(:,:,nb)')); 502 | analysis.energy_diff(framecount,nb) = real(trace( Md(:,:,nb)*Cxx(:,:,nb)*Md(:,:,nb)' .* eye(nLS) )); 503 | analysis.energy_total(framecount,nb) = real(trace( Ms(:,:,nb)*Cxx(:,:,nb)*Ms(:,:,nb)' + (Md(:,:,nb)*Cxx(:,:,nb)*Md(:,:,nb)' .* eye(nLS)) )); 504 | end 505 | end 506 | 507 | % REPAIR SYNTHESIS 508 | Ms_int = interpolateFilters(Ms, fftsize); 509 | Md_int = interpolateFilters(Md, fftsize); 510 | for nb=1:nBins_syn 511 | in = permute(inspec_syn(nb,:,:), [2 3 1]); 512 | outspec_ndiff(nb,:,:) = Ms_int(:,:,nb)*in; 513 | outspec_diff(nb,:,:) = Md_int(:,:,nb)*in; 514 | end 515 | 516 | % Decorrelation options 517 | switch pars.decorrelation 518 | case 'convNoise' % Applied outside of main loop 519 | case 'shapedNoise' % Applied outside of main loop 520 | case 'phaseRand' 521 | randomPhi = rand(size(outspec_diff))*2*pi-pi; 522 | outspec_diff = abs(outspec_diff) .* exp(1i*randomPhi); 523 | case 'covMatch' 524 | % Compute mixing matrices 525 | for nb=1:nBins_anl 526 | Cproto = M_diff(:,:,nb)*Cxx(:,:,nb)*M_diff(:,:,nb)'; 527 | Ctarget = diag(diag(Md(:,:,nb)*Cxx(:,:,nb)*Md(:,:,nb)')); 528 | [M(:,:,nb), Cr] = formulate_M_and_Cr(Cproto, Ctarget, eye(nLS), 0, 0.2); 529 | [Mr(:,:,nb), ~] = formulate_M_and_Cr(diag(real(diag(Cproto))), Cr, eye(nLS), 0, 0.2); 530 | end 531 | M_int = interpolateFilters(M, fftsize); 532 | Mr_int = interpolateFilters(Mr, fftsize); 533 | 534 | % Apply mixing matrices and override outspec_diff 535 | for nb=1:nBins_syn 536 | in = permute(inspec_syn(nb,:,:), [2 3 1]); 537 | protospec = M_proto(:,:,nb) * in; 538 | randomPhi = rand(size(protospec))*2*pi-pi; 539 | protospec_decor = abs(protospec) .* exp(1i*randomPhi); 540 | outspec_diff(nb,:,:) = M_int(:,:,nb)*protospec + Mr_int(:,:,nb)*protospec_decor; 541 | end 542 | end 543 | 544 | % overlap-add 545 | lsir_win_ndiff = real(ifft([outspec_ndiff; conj(outspec_ndiff(end-1:-1:2,:))])); 546 | lsir_ndiff(idx+(0:fftsize-1),:) = lsir_ndiff(idx+(0:fftsize-1),:) + lsir_win_ndiff; 547 | lsir_win_diff = real(ifft([outspec_diff; conj(outspec_diff(end-1:-1:2,:))])); 548 | lsir_diff(idx+(0:fftsize-1),:) = lsir_diff(idx+(0:fftsize-1),:) + lsir_win_diff; 549 | 550 | % advance sample pointer 551 | idx = idx + hopsize; 552 | framecount = framecount + 1; 553 | if framecount >= floor(nFrames/10*progress) 554 | fprintf('*'); 555 | progress=progress+1; 556 | end 557 | end 558 | fprintf(' done!\n') 559 | 560 | %%% Post-Processing of LSIR 561 | fprintf(' - Post-Processing...\n'); 562 | % Remove delay caused by the filter interpolation of gains and circular shift 563 | delay_sig = winsize*2+hopsize+1; 564 | lsir_ndiff = lsir_ndiff(delay_sig:delay_sig+lSig-1,:); 565 | lsir_diff = lsir_diff (delay_sig:delay_sig+lSig-1,:); 566 | 567 | % Remove the analysis frame results that occured before the first input sample actually encounted any analysis 568 | if nargout==4 569 | delay_analysis = 2*winsize/hopsize; 570 | analysis_len = floor(lSig/(hopsize)); 571 | analysis.K = analysis.K(delay_analysis:delay_analysis+analysis_len-1,:); 572 | analysis.diffuseness = analysis.diffuseness(delay_analysis:delay_analysis+analysis_len-1,:); 573 | analysis.azim = analysis.azim(delay_analysis:delay_analysis+analysis_len-1,:,:); 574 | analysis.elev = analysis.elev(delay_analysis:delay_analysis+analysis_len-1,:,:); 575 | analysis.energy_in = analysis.energy_in(delay_analysis:delay_analysis+analysis_len-1,:); 576 | analysis.energy_ndiff = analysis.energy_ndiff(delay_analysis:delay_analysis+analysis_len-1,:); 577 | analysis.energy_diff = analysis.energy_diff(delay_analysis:delay_analysis+analysis_len-1,:); 578 | analysis.energy_total = analysis.energy_total(delay_analysis:delay_analysis+analysis_len-1,:); 579 | end 580 | 581 | % Apply decorrelation 582 | switch pars.decorrelation 583 | case 'convNoise' 584 | % Apply convolution decorrelation to diffuse stream 585 | % we want to apply just enough noise-based reverberation as 586 | % to suitably decorrelate the signals, but not change the captured room 587 | % characteristics too much. T60s of a very, very dry room should suffice for 588 | % this task: 589 | t60 = [0.07 0.07 0.06 0.04 0.02 0.01]; 590 | fcentre = [250 500 1e3 2e3 4e3]; 591 | fcutoffs = fcentre/sqrt(2); 592 | randmat = synthesizeNoiseReverb(nLS, pars.fs, t60, fcutoffs, 1); 593 | lsir_diff = fftfilt(randmat, lsir_diff); 594 | 595 | case 'shapedNoise' 596 | fcentre = 10.^(0.1.*[19:43]); 597 | fcutoffs = fcentre * 10^0.05; 598 | lsir_diff = synthesizeShapedNoise(lsir_diff, fcutoffs, pars.fs); 599 | 600 | case 'phaseRand' % Applied within the main loop 601 | case 'covMatch' % Applied within the main loop 602 | end 603 | 604 | lsir = lsir_ndiff+lsir_diff; 605 | 606 | end 607 | 608 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 609 | function [Ms, Md] = constructMixingMatrices(beamformerOption, As, DFCmtx, Gs, Gd, streamBalance) 610 | nCH = size(As,1); 611 | K = size(As,2); 612 | 613 | % Source stream beamforming weights, Ds 614 | switch beamformerOption 615 | case 'pinv' 616 | Ds = pinv(As); 617 | % Trivia: if K==1, then this reverts to 'MF' 618 | case 'MF' 619 | As_n = As*diag(1./(diag(As'*As)+eps)); 620 | Ds = As_n'; 621 | % Trivia: if SHD, then this reverts to hyper-cardioid beamformers 622 | case 'SD' 623 | beta = 0.01; 624 | DFCmtx_dl = DFCmtx + (trace(DFCmtx)+0.000001).*eye(nCH).*beta; 625 | for k=1:K 626 | Ds(k,:) = (As(:,k)'/DFCmtx_dl*As(:,k))\(As(:,k)'/DFCmtx_dl); 627 | end 628 | % Trivia: if SHD, then this reverts to 'MF' (i.e. hyper-cardioid beamformers) 629 | end 630 | 631 | % Ambient stream beamforming weights, Dd 632 | Dd = eye(nCH) - As*Ds; 633 | 634 | % Optional stream balance manipulations 635 | if streamBalance<1 636 | a = streamBalance; 637 | b = 1; 638 | elseif (streamBalance>=1) && (streamBalance<=2) 639 | a = 1; 640 | b = streamBalance; 641 | end 642 | 643 | % Mixing matrices for direct and ambient streams (Ms & Md) 644 | Ms = a*Gs*Ds; 645 | Md = b*Gd*Dd; 646 | end 647 | 648 | 649 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 650 | function rir_filt = synthesizeNoiseReverb(nCH, fs, t60, fcutoffs, FLATTEN) 651 | %NOISEVERB Simulates a quick and dirty exponential decay reverb tail 652 | % 653 | % order: HOA order 654 | % fs: sample rate 655 | % t60: reverberation times in different bands 656 | % fc: cutoff frequencies of reverberation time bands (1 more than t60) 657 | % 658 | % Archontis Politis, 12/06/2018 659 | % archontis.politis@aalto.fi 660 | 661 | if nargin<5, FLATTEN = 0; end 662 | 663 | % number of HOA channels 664 | nSH = nCH; 665 | % number of frequency bands 666 | nBands = length(t60); 667 | % decay constants 668 | alpha = 3*log(10)./t60; 669 | % length of RIR 670 | %lFilt = ceil(max(t60)*fs); 671 | t = (0:1/fs:max(t60)-1/fs)'; 672 | lFilt = length(t); 673 | % generate envelopes 674 | env = exp(-t*alpha); 675 | % generate RIRs 676 | rir = randn(lFilt, nSH, nBands); 677 | for k = 1:nBands 678 | rir(:, :, k) = rir(:,:,k).*(env(:,k)*ones(1,nSH)); 679 | end 680 | % get filterbank IRs for each band 681 | filterOrder = 10000; 682 | h_filt = filterbank(fcutoffs, filterOrder, fs); 683 | % filter rirs 684 | rir_filt = zeros(lFilt+ceil(filterOrder/2), nSH); 685 | for n = 1:nSH 686 | h_temp = [squeeze(rir(:,n,:)); zeros(ceil(filterOrder/2), nBands)]; 687 | rir_filt(:, n) = sum(fftfilt(h_filt, h_temp), 2); 688 | end 689 | 690 | if FLATTEN, rir_filt = equalizeMinphase(rir_filt); end 691 | 692 | rir_filt = rir_filt(filterOrder/2+1:end,:); % remove delay 693 | end 694 | 695 | function ls_diff_shaped_noise = synthesizeShapedNoise(ls_diff, fcutoffs, fs) 696 | 697 | nLS = size(ls_diff, 2); 698 | nBands = length(fcutoffs)+1; 699 | 700 | winSize_samp = 8000; 701 | %win = hann(winSize_samp); 702 | %win = win(winSize_samp/2:end); 703 | 704 | % Exponential window 705 | tau = winSize_samp / 6; 706 | win = exp(-(1:winSize_samp)/tau); 707 | win = win / sum(win); 708 | 709 | % get filterbank IRs for each band 710 | filterOrder = 70000; 711 | h_filt = filterbank(fcutoffs, filterOrder, fs); 712 | irLen = size(ls_diff, 1); 713 | 714 | % zero padding 715 | ls_diff = [ls_diff; zeros(filterOrder, nLS)]; 716 | 717 | % init 718 | ls_diff_shaped_noise = zeros(irLen + filterOrder, nLS); 719 | rir_filt = zeros(size(ls_diff, 1), nBands, nLS); 720 | 721 | for n = 1:nLS 722 | 723 | % analyse in freq bands 724 | rir_filt(:, :, n) = fftfilt(h_filt, ls_diff(:, n)); 725 | 726 | % get the envelop in each band 727 | %env(:, :, n) = real(fftfilt(win, abs(rir_filt(:, :, n)))); 728 | env(:, :, n) = real(fftfilt(win, (rir_filt(:, :, n)))); 729 | 730 | % synthesize noise 731 | noise = randn(irLen+filterOrder, 1); 732 | 733 | % weight noise with envelope in each band 734 | shaped_noise = noise .* env(:, :, n); 735 | 736 | % filter 737 | shaped_noise_filt = fftfilt(h_filt, shaped_noise); 738 | 739 | ls_diff_shaped_noise(:, n) = sum(shaped_noise_filt, 2); 740 | end 741 | 742 | % shift back because filters are zero phase 743 | ls_diff_shaped_noise = ls_diff_shaped_noise(filterOrder+1:end, :); 744 | 745 | % normalize 746 | ls_diff_shaped_noise = ls_diff_shaped_noise .* rms(ls_diff) ./ rms(ls_diff_shaped_noise); 747 | 748 | % figure 749 | % subplot(3, 1, 1) 750 | % plot(ls_diff) 751 | % subplot(3, 1, 2) 752 | % plot(ls_diff_shaped_noise) 753 | % subplot(3, 1, 3) 754 | % plot(squeeze(sum(env, 2))) 755 | % 756 | end 757 | 758 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 759 | function h_filt = filterbank(fcutoffs, filterOrder, fs) 760 | % fc: the cutoff frequencies of the bands 761 | % Nord: order of hte FIR filter 762 | % 763 | % Archontis Politis, 12/06/2018 764 | % archontis.politis@aalto.fi 765 | 766 | if length(fcutoffs) == 0 %#ok 767 | h_filt = 1; 768 | 769 | elseif length(fcutoffs) == 1 770 | h_filt = zeros(filterOrder+1, 2); 771 | 772 | % lowpass 773 | f_ll = fcutoffs(1); 774 | w_ll = f_ll/(fs/2); 775 | h_filt(:, 1) = fir1(filterOrder, w_ll); 776 | % highpass 777 | f_hh = fcutoffs(2); 778 | w_hh = f_hh/(fs/2); 779 | h_filt(:, 2) = fir1(filterOrder, w_hh, 'high'); 780 | 781 | else 782 | Nfilt = length(fcutoffs)+1; 783 | h_filt = zeros(filterOrder+1, Nfilt); 784 | 785 | % lowpass 786 | f_ll = fcutoffs(1); 787 | w_ll = f_ll/(fs/2); 788 | h_filt(:, 1) = fir1(filterOrder, w_ll); 789 | % highpass 790 | f_hh = fcutoffs(end); 791 | w_hh = f_hh/(fs/2); 792 | h_filt(:, end) = fir1(filterOrder, w_hh, 'high'); 793 | % bandpass 794 | for k = 1:Nfilt-2 795 | fl = fcutoffs(k); 796 | fh = fcutoffs(k+1); 797 | wl = fl/(fs/2); 798 | wh = fh/(fs/2); 799 | w = [wl wh]; 800 | h_filt(:, k+1) = fir1(filterOrder, w, 'bandpass'); 801 | end 802 | end 803 | 804 | end 805 | 806 | function psi = comedie(lambda) 807 | % Implementation based on the COMEDIE estimator as described in: 808 | % Epain, N. and Jin, C.T., 2016. Spherical Harmonic Signal Covariance and 809 | % Sound Field Diffuseness. IEEE/ACM Transactions on Audio, Speech, and 810 | % Language Processing, 24(10), pp.1796-1807. 811 | % 812 | nCH = length(lambda); 813 | if all(lambda==0) 814 | psi = 0; 815 | else 816 | g_0 = 2*(nCH-1); 817 | mean_ev = sum(lambda)/nCH; 818 | g = (1/mean_ev)*sum(abs(lambda-mean_ev)); 819 | psi = 1-g/g_0; 820 | end 821 | end 822 | 823 | 824 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 825 | function rir_filt_flat = equalizeMinphase(rir_filt) 826 | %MAKEFLATVERB Makes the decaying noise spectrally flat 827 | % 828 | % Archontis Politis, 12/06/2018 829 | % archontis.politis@aalto.fi 830 | 831 | Nrir = size(rir_filt,2); 832 | for n=1:Nrir 833 | % equalise TDI by its minimum phase form to unity magnitude response 834 | tdi_f = fft(rir_filt(:,n)); 835 | tdi_min_f = exp(conj(hilbert(log(abs(tdi_f))))); 836 | tdi_eq = real(ifft(tdi_f./tdi_min_f)); 837 | rir_filt_flat(:,n) = tdi_eq; 838 | end 839 | 840 | end 841 | 842 | function [est_idx, est_dirs] = peakFind2d(pmap, grid_dirs, grid_xyz, nPeaks) 843 | 844 | kappa = 50; 845 | P_minus_peak = pmap; 846 | est_dirs = zeros(nPeaks, 2); 847 | est_idx = zeros(nPeaks, 1); 848 | for k = 1:nPeaks 849 | [~, peak_idx] = max(P_minus_peak); 850 | est_dirs(k,:) = grid_dirs(peak_idx,:); 851 | est_idx(k) = peak_idx; 852 | VM_mean = grid_xyz(peak_idx,:); % orientation of VM distribution 853 | VM_mask = kappa/(2*pi*exp(kappa)-exp(-kappa)) * exp(kappa*grid_xyz*VM_mean'); % VM distribution 854 | VM_mask = 1./(0.00001+VM_mask); % inverse VM distribution 855 | P_minus_peak = P_minus_peak.*VM_mask; 856 | end 857 | 858 | end 859 | 860 | 861 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 862 | function M_interp = interpolateFilters(M, fftsize) 863 | % M filter matrix with y = M*x, size NxLxK, 864 | % N output channels, M input channels, K frequency bins 865 | % 866 | % Archontis Politis, 12/06/2018 867 | % archontis.politis@aalto.fi 868 | 869 | winsize = 2*(size(M,3)-1); 870 | M_conj = conj(M(:,:,end-1:-1:2)); 871 | M_ifft = ifft(cat(3, M, M_conj), [], 3); 872 | M_ifft = M_ifft(:,:, [(winsize/2+1:end) (1:winsize/2)]); % flip 873 | M_interp = fft(M_ifft, fftsize, 3); % interpolate to fftsize 874 | M_interp = M_interp(:,:,1:fftsize/2+1); % keep up to nyquist 875 | end 876 | 877 | function P_music = sphMUSIC(A_grid, Vn) 878 | % SPHMUSIC DOA estimator based on MUltiple SIgnal Classification (MUSIC) 879 | VnA = Vn'*A_grid; 880 | P_music = 1./sum(conj(VnA).*VnA); 881 | P_music = P_music.'; 882 | 883 | end 884 | 885 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 886 | function [K, obfunc] = SORTE(lambda) 887 | % SORTE Second ORder sTatistic of Eigenvalues estimator of source number 888 | % Implementation based on the SORTE estimator as described in: 889 | % He, Z., Cichocki, A., Xie, S., & Choi, K. (2010). 890 | % Detecting the number of clusters in n-way probabilistic clustering. 891 | % IEEE Transactions on Pattern Analysis and Machine Intelligence, 32(11), 892 | % pp.2006-2021. 893 | % 894 | % INPUT ARGUMENTS 895 | % lambda % vector of eigenvalues of the spatial covariance matrix of 896 | % ambisonic or (diffuse-whitened) microphone array signals 897 | % 898 | % OUTPUT ARGUMENTS 899 | % K % the number of detected sources in the mixtures 900 | % obfunc % the values of the SORTE criteria for the different source 901 | % numbers - its minimum indicates theestimated source number 902 | % 903 | % Author: Archontis Politis (archontis.politis@gmail.com) 904 | % Copyright (C) 2021 - Archontis Politis 905 | % 906 | if all(lambda==0) 907 | K = 0; 908 | else 909 | N = length(lambda); 910 | Dlambda = lambda(1:end-1) - lambda(2:end); 911 | for k=1:N-1 912 | meanDlambda = 1/(N-k)*sum(Dlambda(k:N-1)); 913 | sigma2(k) = 1/(N-k)*sum( (Dlambda(k:N-1) - meanDlambda).^2 ); 914 | end 915 | for k=1:N-2 916 | if sigma2(k)>0, obfunc(k) = sigma2(k+1)/sigma2(k); 917 | elseif sigma2(k) == 0, obfunc(k) = Inf; 918 | end 919 | end 920 | obfunc(end) = Inf; 921 | [~,K] = min(obfunc); 922 | end 923 | 924 | end 925 | 926 | function [oct_idx, oct_freqs] = findOctavePartitions(bandfreqs, maxFreqLim) 927 | if (nargin<2), maxFreqLim = 20000; end 928 | if (maxFreqLim>20000), maxFreqLim = 20000; end 929 | oct_freqs = 250; 930 | oct_idx = 1; 931 | 932 | counter = 1; 933 | while oct_freqs(counter)*2 < maxFreqLim 934 | oct_freqs(counter+1) = oct_freqs(counter) * 2; % Upper frequency limit of band 935 | % find closest band frequency as upper partition limit 936 | [~, oct_idx(counter+1)] = min(abs(sqrt(1/2)*oct_freqs(counter+1) - bandfreqs)); 937 | % force at least one bin bands (for low frequencies) 938 | if (oct_idx(counter+1) == oct_idx(counter)), oct_idx(counter+1) = oct_idx(counter+1)+1; end 939 | % erb_freqs(counter+1) = bandfreqs(erb_idx(counter+1)); % quantize new band limit to bins 940 | 941 | counter = counter+1; 942 | end 943 | % last limit set at last band 944 | oct_freqs(counter+1) = bandfreqs(end); 945 | oct_idx(counter+1) = length(bandfreqs); 946 | 947 | end 948 | 949 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 950 | function [M, Cr] = formulate_M_and_Cr(Cx, Cy, Q, flag, reg) 951 | % FORMULATE_M_AND_CR Computes optimal mixing matrix based on input-output 952 | % covariance matrix 953 | % 954 | % J.Vilkamo's "Covariance Domain Framework for Spatial Audio Processing". 955 | % 956 | if nargin == 4 957 | reg=0.2; 958 | end 959 | % flag = 0: Expect usage of residuals 960 | % flag = 1: Fix energies instead 961 | lambda=eye(length(Cy),length(Cx)); 962 | 963 | % Decomposition of Cy 964 | [U_Cy,S_Cy]=svd(Cy); 965 | Ky=U_Cy*sqrt(S_Cy); 966 | 967 | % Decomposition of Cx 968 | [U_Cx,S_Cx]=svd(Cx); 969 | Kx=U_Cx*sqrt(S_Cx); 970 | 971 | %SVD of Kx 972 | Ux=U_Cx; 973 | Sx=sqrt(S_Cx); 974 | % Vx = identity matrix 975 | 976 | % Regularization Sx 977 | Sx_diag=diag(Sx); 978 | limit=max(Sx_diag)*reg+1e-20; 979 | Sx_reg_diag=max(Sx_diag,limit); 980 | 981 | % Formulate regularized Kx^-1 982 | Kx_reg_inverse=diag(1./Sx_reg_diag)*Ux'; 983 | 984 | % Formulate normalization matrix G_hat 985 | Cy_hat_diag=diag(Q*Cx*Q'); 986 | limit=max(Cy_hat_diag)*0.001+1e-20; 987 | Cy_hat_diag=real(max(Cy_hat_diag,limit)); 988 | G_hat=diag(real(sqrt(diag(Cy)./Cy_hat_diag))); 989 | 990 | % Formulate optimal P 991 | [U,~,V]=svd(Kx'*Q'*G_hat'*Ky); 992 | P=V*lambda*U'; 993 | 994 | % Formulate M 995 | M=Ky*P*Kx_reg_inverse; 996 | 997 | % Formulate residual covariance matrix 998 | Cy_tilde = M*Cx*M'; 999 | Cr=Cy-Cy_tilde; 1000 | 1001 | % Use energy compensation instead of residuals 1002 | if flag==1 1003 | adjustment=diag(Cy)./diag(Cy_tilde + 1e-20); 1004 | G=diag(sqrt(adjustment)); 1005 | M=G*M; 1006 | Cr='unnecessary'; 1007 | end 1008 | 1009 | end 1010 | 1011 | function p = getPvalue(DTT,f) 1012 | % f: frequency vector 1013 | % DTT = 0: normal room (p=2) 1014 | % DTT = 1: anechoic room (p<2) 1015 | a1 = 0.00045; 1016 | a2 = 0.000085; 1017 | p0 = 1.5 - 0.5 * cos(4.7*tanh(a1*f)).*max(0,1-a2*f); 1018 | p = (p0-2)*sqrt(DTT)+2; 1019 | 1020 | end 1021 | --------------------------------------------------------------------------------