├── .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 |
--------------------------------------------------------------------------------