├── README.md ├── ambisonic_crossover.m ├── analyse_interpolate_SRIRs.m ├── analyse_interpolate_SRIRs_source.m ├── demo_interpolate_SRIRs_1D.m ├── demo_interpolate_SRIRs_2D.m ├── demo_interpolate_SRIRs_source.m ├── designMinPhase.m ├── evalSH.m ├── interpolate_SRIRs.m ├── interpolate_SRIRs_source.m └── octdsgn.m /README.md: -------------------------------------------------------------------------------- 1 | # Interpolation of Spatial Room Impulse Responses for Room Transitions 2 | 3 | This repository contains MATLAB functions to interpolate between spatial room impulse responses at different receiver positions, suitable for room transitions and other non-simple recording setups. 4 | Please [refer to the publication](https://www.aes.org/e-lib/browse.cfm?elib=22140) for more information: 5 | ``` 6 | T. McKenzie, N. Meyer-Kahlen, C. Hold, S. J. Schlecht, and V. Pulkki: "Auralisation of measured room transitions in virtual 7 | reality." Journal of the Audio Engineering Society, doi: 10.17743/jaes.2022.0084, 2023. 8 | ``` 9 | ``` 10 | T. McKenzie, N. Meyer-Kahlen, R. Daugintis, L. McCormack, S. J. Schlecht, and V. Pulkki: "Perceptually informed interpolation and 11 | rendering of spatial room impulse responses for room transitions." International Congress on Acoustics, Gyeongju, pp. 1-11. 2022. 12 | ``` 13 | 14 | 15 | Two demo scripts 'demo_interpolate_SRIRs_function' are included: one for 1D (ie along a line) measurements for the transition between two coupled rooms, and one for 2D measurements inside a single room. The script also includes an example of formatting for saving a sofa file of the resulting SRIRs. 16 | 17 | For listening and auralising sofa files in this format, please see the [Sparta 6DOFConv VST plugin](https://leomccormack.github.io/sparta-site/docs/plugins/sparta-suite/#6dofconv). 18 | 19 | # Source Interpolation of Spatial Room Impulse Responses 20 | 21 | This repository also contains MATLAB functions to interpolate between spatial room impulse responses at different source positions. 22 | Please [refer to the publication](https://www.aes.org/e-lib/browse.cfm?elib=22044) for more information: 23 | 24 | ``` 25 | T. McKenzie, S. J. Schlecht: "Source position interpolation of spatial room impulse responses" AES 154th Convention, Espoo, 26 | Helsinki, Finland, 2023. 27 | ``` 28 | 29 | A demonstration script 'demo_interpolate_SRIRs_source.m' is included, which along with 'analyse_interpolate_SRIRs_source_AES.m' plots the figures from the AES convention paper. An example of formatting for saving a sofa file of the resulting SRIRs is included, which is compatible with the [Sparta 6DoFConv VST plugin](https://leomccormack.github.io/sparta-site/docs/plugins/sparta-suite/#6dofconv), for listening and auralising up to 6DoF sofa files. 30 | -------------------------------------------------------------------------------- /ambisonic_crossover.m: -------------------------------------------------------------------------------- 1 | function [filtLo,filtHi,fcHz] = ambisonic_crossover(ambisonic_order_or_fc,Fs) 2 | %{ 3 | Function to produce linear-phase crossover network for dual-band Ambisonic 4 | decoding. 5 | 6 | Thomas McKenzie, University of York, 2019. 7 | 8 | References: 9 | See Thomas McKenzie PhD thesis. 10 | %} 11 | 12 | if ambisonic_order_or_fc <= 49 % if the first argument is a value of 49 or smaller, it is assumed the input is the ambisonic order. Otherwise, it is assumed the input is the cut off frequency. 13 | R = 0.09; % radius of reproduction area in metres (Neumann KU 100 is stated on the website as having a width of 18cm) 14 | fcHz = (ambisonic_order_or_fc * 343) / (4*R*(ambisonic_order_or_fc + 1) * sin(pi / (2*ambisonic_order_or_fc+2))); % this is equation (2) from \cite{Bertet2013} 15 | else 16 | fcHz = ambisonic_order_or_fc; 17 | end 18 | fcNorm = fcHz/(Fs/2); % convert to normalised frequency 19 | 20 | XoverOrder = 128; % crossover order 21 | ripple1 = 50; % ripple in dB 22 | 23 | % using chebyshev windows 24 | filtLo = fir1(XoverOrder,fcNorm,'low',chebwin((XoverOrder+1),ripple1), 'noscale'); 25 | filtHi = fir1(XoverOrder,fcNorm,'high',chebwin((XoverOrder+1),ripple1), 'noscale'); 26 | end 27 | 28 | -------------------------------------------------------------------------------- /analyse_interpolate_SRIRs.m: -------------------------------------------------------------------------------- 1 | % Analyse interpolated IRs 2 | % 3 | % The user is directed to the ICA 2022 paper for details: 4 | % McKenzie, T., Meyer-Kahlen, N., Daugintis, R., McCormack, L., Schlecht, S. 5 | % J., & Pulkki, V. (2022). Perceptual interpolation and rendering of coupled 6 | % room spatial room impulse responses. International Congress on Acoustics, 7 | % Korea. 8 | % 9 | % Thomas McKenzie, 2022. thomas.mckenzie@aalto.fi / tom.mckenzie07@gmail.com 10 | 11 | directSoundLength_samp = 200; 12 | earlyRefsCutoff_samp = 10000; 13 | N_interp = 4; 14 | 15 | figure; 16 | title(['RMS and DRR values. Measurement reduction = ',num2str(measReduction),'.']); 17 | 18 | 19 | subplot(2,4,1) 20 | plot(rms(abs(squeeze(srirs_interp(1:directSoundLength_samp,1,:))))); 21 | hold on 22 | plot(rms(abs(squeeze(hCorrect(1:directSoundLength_samp,1,:))))); 23 | title('Direct sound RMS W channel') 24 | 25 | subplot(2,4,2) 26 | plot(rms(abs(squeeze(srirs_interp(directSoundLength_samp:earlyRefsCutoff_samp,1,:))))); 27 | hold on 28 | plot(rms(abs(squeeze(hCorrect(directSoundLength_samp:earlyRefsCutoff_samp,1,:))))); 29 | title('Early Reflections RMS W channel') 30 | 31 | erEnd = earlyRefsCutoff_samp+directSoundLength_samp; 32 | subplot(2,4,3) 33 | plot(rms((squeeze(srirs_interp(erEnd:end,1,:))))); 34 | hold on 35 | plot(rms((squeeze(hCorrect(erEnd:end,1,:))))); 36 | title('Late Reverb RMS W channel') 37 | 38 | subplot(2,4,4) 39 | plot(rms(abs(squeeze(srirs_interp(1:directSoundLength_samp,1,:))))./rms(abs(squeeze(srirs_interp(directSoundLength_samp:end,1,:))))); 40 | hold on;plot(rms(abs(squeeze(hCorrect(1:directSoundLength_samp,1,:))))./rms(abs(squeeze(hCorrect(directSoundLength_samp:end,1,:))))); 41 | title('DRR W channel') 42 | 43 | 44 | subplot(2,4,5) 45 | plot(squeeze(mean(rms(srirs_interp(1:directSoundLength_samp,:,:)),2))); 46 | hold on 47 | plot(squeeze(mean(rms(hCorrect(1:directSoundLength_samp,:,:)),2))); 48 | title('Direct sound RMS All channels') 49 | 50 | subplot(2,4,6) 51 | plot(squeeze(mean(rms(srirs_interp(directSoundLength_samp:earlyRefsCutoff_samp,:,:)),2))); 52 | hold on 53 | plot(squeeze(mean(rms(hCorrect(directSoundLength_samp:earlyRefsCutoff_samp,:,:)),2))); 54 | title('Early Reflections RMS All channels') 55 | 56 | subplot(2,4,7) 57 | plot(squeeze(mean(rms(srirs_interp(erEnd:end,:,:)),2))); 58 | hold on 59 | plot(squeeze(mean(rms(hCorrect(erEnd:end,:,:)),2))); 60 | title('Late Reverb RMS All channels') 61 | 62 | 63 | subplot(2,4,8) 64 | plot(mean(squeeze(rms(abs(squeeze(srirs_interp(1:directSoundLength_samp,:,:))))./rms(abs(squeeze(srirs_interp(directSoundLength_samp:end,:,:))))))); 65 | hold on;plot(mean(squeeze(rms(abs(squeeze(hCorrect(1:directSoundLength_samp,:,:))))./rms(abs(squeeze(hCorrect(directSoundLength_samp:end,:,:))))))); 66 | title('DRR All channels') 67 | 68 | %% time domain plots 69 | % channelToPlot = lastChannel; % good to check not just W channel (1) to see if the higher order channels are good too 70 | channelToPlot = 1; % good to check not just W channel (1) to see if the higher order channels are good too 71 | plot_length = 300; % plot length in ms 72 | 73 | plot_length_samples = plot_length / 1000 * fs; 74 | [X,Y] = meshgrid(-250:5:250,1/48:1/48:plot_length); 75 | irTrunc = hCorrect; 76 | figure;subplot(1,2,1) 77 | surf(X,Y,real(10*log10(abs(squeeze(irTrunc(1:plot_length_samples,channelToPlot,:))))),'EdgeColor','none'); 78 | 79 | ylabel('Time (ms)'); 80 | c = flip(bone);caxis([-40 0]);colormap(c);h = colorbar;ylabel(h, 'Amplitude (dB)'); 81 | view(0,90); 82 | set(gcf, 'Color', 'w');pbaspect([1.7 1 1]);set(gca, 'fontsize', 12); 83 | title(['time-domain - original']); 84 | 85 | % interpolated 86 | irTrunc = srirs_interp; 87 | subplot(1,2,2) 88 | surf(X,Y,real(10*log10(abs(squeeze(irTrunc(1:plot_length_samples,channelToPlot,:))))),'EdgeColor','none'); 89 | 90 | ylabel('Time (ms)'); 91 | c = flip(bone);caxis([-40 0]);colormap(c);h = colorbar;ylabel(h, 'Amplitude (dB)'); 92 | view(0,90); 93 | set(gcf, 'Color', 'w');pbaspect([1.7 1 1]);set(gca, 'fontsize', 12); 94 | title(['time-domain - interpolated. Measurement reduction = ',num2str(measReduction),'.']); 95 | 96 | %% DOA 97 | 98 | % original 99 | irTrunc = hCorrect(:,1:(N_interp+1)^2,:); 100 | 101 | % Tuneable parameters: 102 | degreeResolution = 5; 103 | order = N_interp; 104 | nSrc = 7; 105 | numSamps = 4800*3; % 0.3 seconds 106 | highPassFilterFreq = 3000; 107 | kappa = 40; 108 | 109 | grid_dirs = grid2dirs(degreeResolution,degreeResolution,0,0); % Grid of directions to evaluate DoA estimation 110 | P_src = diag(ones(numSamps,1)); 111 | [~,filtHi,~] = ambisonic_crossover(highPassFilterFreq,fs); 112 | 113 | doa_est = zeros(nSrc,2,length(irTrunc(1,1,:))); 114 | doa_est_P = zeros(nSrc,length(irTrunc(1,1,:))); 115 | 116 | for i = 1:length(irTrunc(1,1,:)) 117 | Y_src = filter(filtHi,1,irTrunc(1:numSamps,:,i)); % high pass filter 118 | 119 | stVec = Y_src'; 120 | sphCOV = stVec*P_src*stVec' + 1*eye((order+1)^2)/(4*pi); 121 | 122 | % DoA estimation 123 | [~, est_dirs_pwd,est_dirs_P] = sphPWDmap(sphCOV, grid_dirs, nSrc); 124 | 125 | % convert to degs from rads 126 | est_dirs_pwd = est_dirs_pwd*180/pi; 127 | 128 | % flip -ve values near -180 to +ve values 129 | negativeFlipLimit = -170; 130 | for j = 1:length(est_dirs_pwd(:,1)) 131 | for k = 1:length(est_dirs_pwd(1,:)) 132 | if est_dirs_pwd(j,k) < negativeFlipLimit 133 | est_dirs_pwd(j,k) = est_dirs_pwd(j,k) + 360; 134 | end 135 | end 136 | end 137 | doa_est(:,:,i) = est_dirs_pwd; 138 | doa_est_P(:,i) = est_dirs_P; 139 | end 140 | 141 | normalized_doa_est_P = doa_est_P ./ max(doa_est_P,[],1); 142 | normalized_doa_est_P_dB = mag2db(normalized_doa_est_P)/2; 143 | 144 | plot_thresh = -10; 145 | normalized_doa_est_P_dB( normalized_doa_est_P_dB < plot_thresh ) = plot_thresh; 146 | c_truncation = 20; 147 | cmap = flip(parula(256)); 148 | c = cmap(c_truncation:end,:); % truncate yellow 149 | 150 | doa_01 = rescale(normalized_doa_est_P_dB, 'InputMin',plot_thresh); 151 | cspace = linspace(0,1,size(c,1)); 152 | doa_color(:,:,1) = interp1(cspace, c(:,1), doa_01); 153 | doa_color(:,:,2) = interp1(cspace, c(:,2), doa_01); 154 | doa_color(:,:,3) = interp1(cspace, c(:,3), doa_01); 155 | 156 | h= figure;subplot(2,1,1) 157 | for i = nSrc:-1:1 158 | s = scatter(-250:5:250,squeeze(doa_est(i,1,:)),25,... 159 | squeeze(doa_color(i,:,:)),'filled'); 160 | hold on 161 | end 162 | 163 | ylabel('Azimuth (°)'); 164 | xlabel('<-- Storage Measurement position (cm) Stairwell -->'); 165 | 166 | ylim([negativeFlipLimit (negativeFlipLimit+360)]);yticks(-180:45:180); 167 | xlim([-250 250]);xticks(-250:50:250);colormap(c); 168 | k = colorbar;ylabel(k,'Normalised power (dB)');caxis([plot_thresh 0]); 169 | set(gcf, 'Color', 'w');pbaspect([1.7 1 1]); 170 | box on;grid on;set(gca,'FontSize',16) 171 | title('DoA - original'); 172 | 173 | % Interpolated 174 | irTrunc = srirs_interp; 175 | doa_est = zeros(nSrc,2,length(irTrunc(1,1,:))); 176 | doa_est_P = zeros(nSrc,length(irTrunc(1,1,:))); 177 | for i = 1:length(irTrunc(1,1,:)) 178 | Y_src = filter(filtHi,1,irTrunc(1:numSamps,:,i)); % high pass filter 179 | 180 | stVec = Y_src'; 181 | sphCOV = stVec*P_src*stVec' + 1*eye((order+1)^2)/(4*pi); 182 | 183 | % DoA estimation 184 | [~, est_dirs_pwd,est_dirs_P] = sphPWDmap(sphCOV, grid_dirs, nSrc); 185 | 186 | % convert to degs from rads 187 | est_dirs_pwd = est_dirs_pwd*180/pi; 188 | 189 | % flip -ve values near -180 to +ve values 190 | negativeFlipLimit = -170; 191 | for j = 1:length(est_dirs_pwd(:,1)) 192 | for k = 1:length(est_dirs_pwd(1,:)) 193 | if est_dirs_pwd(j,k) < negativeFlipLimit 194 | est_dirs_pwd(j,k) = est_dirs_pwd(j,k) + 360; 195 | end 196 | end 197 | end 198 | doa_est(:,:,i) = est_dirs_pwd; 199 | doa_est_P(:,i) = est_dirs_P; 200 | end 201 | 202 | normalized_doa_est_P = doa_est_P ./ max(doa_est_P,[],1); 203 | normalized_doa_est_P_dB = mag2db(normalized_doa_est_P)/2; 204 | 205 | normalized_doa_est_P_dB( normalized_doa_est_P_dB < plot_thresh ) = plot_thresh; 206 | 207 | doa_01 = rescale(normalized_doa_est_P_dB, 'InputMin',plot_thresh); 208 | 209 | cspace = linspace(0,1,size(c,1)); 210 | doa_color(:,:,1) = interp1(cspace, c(:,1), doa_01); 211 | doa_color(:,:,2) = interp1(cspace, c(:,2), doa_01); 212 | doa_color(:,:,3) = interp1(cspace, c(:,3), doa_01); 213 | 214 | subplot(2,1,2) 215 | for i = nSrc:-1:1 216 | s = scatter(-250:5:250,squeeze(doa_est(i,1,:)),25,... 217 | squeeze(doa_color(i,:,:)),'filled'); 218 | hold on 219 | end 220 | 221 | ylabel('Azimuth (°)'); 222 | xlabel('<-- Storage Measurement position (cm) Stairwell -->'); 223 | ylim([negativeFlipLimit (negativeFlipLimit+360)]);yticks(-180:45:180); 224 | xlim([-250 250]);xticks(-250:50:250);colormap(c); 225 | k = colorbar;ylabel(k,'Normalised power (dB)');caxis([plot_thresh 0]); 226 | set(gcf, 'Color', 'w');pbaspect([1.7 1 1]); 227 | box on;grid on;set(gca,'FontSize',16) 228 | 229 | title(['DoA - interpolated. Measurement reduction = ',num2str(measReduction),'.']); 230 | -------------------------------------------------------------------------------- /analyse_interpolate_SRIRs_source.m: -------------------------------------------------------------------------------- 1 | % Analysis script for interpolate_SRIRs_source.m 2 | % 3 | % The user is directed to the following paper: 4 | % McKenzie, T. and Schlecht, S. J. "Source position interpolation of 5 | % spatial room impulse responses" AES 154th Convention, Espoo, Helsinki, 6 | % Finland, 2023. 7 | % 8 | % Thomas McKenzie, University of Edinburgh, 2023. thomas.mckenzie@ed.ac.uk 9 | 10 | 11 | %% RMS plot 12 | t_interp = 1:size(srirs_interp,3); 13 | figure; 14 | plot(t_interp,db(rms((squeeze(srirs_interp(:,1,:,1))))),'LineWidth',2,'Marker','^','MarkerSize',10,'LineStyle','none'); 15 | hold on 16 | plot(t_interp,db(rms((squeeze(srirs_interp(:,1,:,end))))),'LineWidth',2,'Marker','^','MarkerSize',10,'LineStyle','none'); 17 | numInterpResp = size(srirs_interp,4)-2; 18 | for i = 2:size(srirs_interp,4)-1 19 | plot(t_interp,db(rms((squeeze(srirs_interp(:,1,:,i))))),':','Color',[((i-1)/numInterpResp - 1/numInterpResp/2) 0 1-((i-1)/numInterpResp-1/numInterpResp/2)],'Marker','^','MarkerSize',10,'LineStyle','none'); 20 | end 21 | 22 | xlim([0.5 size(srirs_interp,3)+0.5]) 23 | ylabel('Magnitude (dB)'); xlabel('Receiver Number') 24 | pbaspect([2.2 1 1]) 25 | ylim([-49 -38]);box on 26 | title(['RMS W channel: ', methodType, ' interpolation method']) 27 | 28 | %% Frequency-domain plot 29 | irToPlot = 1; channelToPlot = 1; 30 | figure; 31 | freqplot_smooth(srirs_interp(:,channelToPlot,irToPlot,1),fs,2,'-', 2); 32 | hold on 33 | freqplot_smooth(srirs_interp(:,channelToPlot,irToPlot,end),fs,2,'-', 2); 34 | for i = 2:size(srirs_interp,4)-1 35 | freqplot_smooth(srirs_interp(:,channelToPlot,irToPlot,i),fs,2,'k:',1); 36 | end 37 | 38 | pbaspect([2.2 1 1]); ylim([-15 25]); 39 | title(['Frequency response W channel: ', methodType, ' interpolation method']) 40 | 41 | %% Direct sound DoA 42 | 43 | figure; 44 | hold on;grid on; 45 | 46 | m = SRIR_Measurement(); m.setShMicArray(4, 0.032); m.fs = fs; 47 | numMeasurements = size(srirs_interp,3); 48 | directSoundLength_samp = 150; 49 | 50 | % DoA, first source position 51 | directSoundDirections = zeros(numMeasurements, 3); 52 | aziMeasuredDeg = zeros(numMeasurements, 1); 53 | for iMeas = 1:numMeasurements 54 | m.srir = squeeze(srirs_interp(1:directSoundLength_samp, :, iMeas,1)); 55 | opts.analysisMethod = 'iv'; 56 | a = SRIR_Analysis(m, opts); a.run(); 57 | 58 | idxNonZero = find(sum(a.doa~=0, 2)); 59 | doa = mean(a.doa(idxNonZero, :)); 60 | doa = doa ./ vecnorm(doa, 2, 2); 61 | 62 | directSoundDirections(iMeas, :) = doa; 63 | aziMeasuredDeg(iMeas) = atan2(doa(2), doa(1)) * 180 / pi; 64 | if aziMeasuredDeg(iMeas)<0 65 | aziMeasuredDeg(iMeas) = aziMeasuredDeg(iMeas) + 360; 66 | end 67 | end 68 | quiver3(recPosns(:, 1) / 100, recPosns(:, 2) / 100, recPosns(:, 3) / 100, ... 69 | directSoundDirections(:, 1), directSoundDirections(:, 2) ,directSoundDirections(:, 3),0,'Linewidth',2) 70 | 71 | % DoA, second source position 72 | directSoundDirections = zeros(numMeasurements, 3); 73 | aziMeasuredDeg = zeros(numMeasurements, 1); 74 | for iMeas = 1:numMeasurements 75 | m.srir = squeeze(srirs_interp(1:directSoundLength_samp, :, iMeas,end)); 76 | a = SRIR_Analysis(m, opts); a.run(); 77 | 78 | idxNonZero = find(sum(a.doa~=0, 2)); 79 | doa = mean(a.doa(idxNonZero, :)); 80 | doa = doa ./ vecnorm(doa, 2, 2); 81 | 82 | directSoundDirections(iMeas, :) = doa; 83 | aziMeasuredDeg(iMeas) = atan2(doa(2), doa(1)) * 180 / pi; 84 | if aziMeasuredDeg(iMeas)<0 85 | aziMeasuredDeg(iMeas) = aziMeasuredDeg(iMeas) + 360; 86 | end 87 | end 88 | quiver3(recPosns(:, 1) / 100, recPosns(:, 2) / 100, recPosns(:, 3) / 100, ... 89 | directSoundDirections(:, 1), directSoundDirections(:, 2) ,directSoundDirections(:, 3),0,'Linewidth',2) 90 | 91 | % DoA, interpolated source positions 92 | for iIntSrc = 2:size(srirs_interp,4)-1 93 | directSoundDirections = zeros(numMeasurements, 3); 94 | aziMeasuredDeg = zeros(numMeasurements, 1); 95 | for iMeas = 1:numMeasurements 96 | m.srir = squeeze(srirs_interp(1:directSoundLength_samp, :, iMeas,iIntSrc)); 97 | a = SRIR_Analysis(m, opts); a.run(); 98 | 99 | idxNonZero = find(sum(a.doa~=0, 2)); 100 | doa = mean(a.doa(idxNonZero, :)); 101 | doa = doa ./ vecnorm(doa, 2, 2); 102 | 103 | directSoundDirections(iMeas, :) = doa; 104 | aziMeasuredDeg(iMeas) = atan2(doa(2), doa(1)) * 180 / pi; 105 | if aziMeasuredDeg(iMeas)<0 106 | aziMeasuredDeg(iMeas) = aziMeasuredDeg(iMeas) + 360; 107 | end 108 | end 109 | quiver3(recPosns(:, 1) / 100, recPosns(:, 2) / 100, recPosns(:, 3) / 100, ... 110 | directSoundDirections(:, 1), directSoundDirections(:, 2) ,directSoundDirections(:, 3),0,'Color',[((iIntSrc-1)/numInterpResp - 1/numInterpResp/2) 0 1-((iIntSrc-1)/numInterpResp-1/numInterpResp/2)]) 111 | axis equal 112 | xlabel('x (m)'), ylabel('y (m)'), zlabel('z (m)') 113 | end 114 | 115 | % plot actual source positions (and interpolated 'correct' source positions) 116 | scatter3(sofa1.SourcePosition(src1,1),sofa1.SourcePosition(src1,2),sofa1.SourcePosition(src1,3),50,[0, 0.4470, 0.7410],'LineWidth',2,'Marker','^') 117 | scatter3(sofa1.SourcePosition(src2,1),sofa1.SourcePosition(src2,2),sofa1.SourcePosition(src2,3),50,[0.8500, 0.3250, 0.0980],'LineWidth',2,'Marker','^') 118 | for i = 2:size(srirs_interp,4)-1 119 | scatter3(pos_interp(i,1)/100,pos_interp(i,2)/100,pos_interp(i,3)/100,50,[((i-1)/numInterpResp - 1/numInterpResp/2) 0 1-((i-1)/numInterpResp-1/numInterpResp/2)],'Marker','^'); 120 | end 121 | 122 | xlim([1 7.87]);ylim([1 5]);zlim([1 2]);view([0 90]);box on; 123 | title(['Direct sound DoA: ', methodType, ' interpolation method']) 124 | 125 | %% Plot Horizontal DoA 126 | 127 | irTrunc = squeeze(srirs_interp(:,:,1,:)); % first receiver 128 | for i = 1:size(irTrunc,3) % rotate by -90 degrees (compatibility requirement) 129 | irTrunc(:,:,i) = rotateHOA_N3D(irTrunc(:,:,i),-90,0,0); 130 | end 131 | 132 | degreeResolution = 5; 133 | nSrc = 7; 134 | numSamps = fs*0.3; % 0.3 seconds 135 | highPassFilterFreq = 3000; 136 | kappa = 40; 137 | plot_thresh = -10; 138 | 139 | grid_dirs = grid2dirs(degreeResolution,90,0,0); % Grid of directions to evaluate DoA estimation 140 | P_src = diag(ones(numSamps,1)); 141 | [~,filtHi,~] = ambisonic_crossover(highPassFilterFreq,fs); 142 | 143 | doa_est = zeros(nSrc,2,length(irTrunc(1,1,:))); 144 | doa_est_P = zeros(nSrc,length(irTrunc(1,1,:))); 145 | P_pwd = zeros(size(grid_dirs,1),length(irTrunc(1,1,:))); 146 | for i = 1:length(irTrunc(1,1,:)) 147 | Y_src = filter(filtHi,1,irTrunc(1:numSamps,:,i)); % high pass filter 148 | stVec = Y_src'; 149 | sphCOV = stVec*P_src*stVec' + 1*eye((4+1)^2)/(4*pi); 150 | 151 | % DoA estimation 152 | [P_pwd(2:end-1,i), est_dirs_pwd,est_dirs_P] = sphPWDmap(sphCOV, grid_dirs(2:end-1,:), nSrc,kappa); 153 | end 154 | normalized_doa_est_P = P_pwd./ max(P_pwd,[],1); 155 | normalized_doa_est_P_dB = mag2db(normalized_doa_est_P)/2; 156 | normalized_doa_est_P_dB( normalized_doa_est_P_dB < plot_thresh ) = plot_thresh; 157 | P_pwd_n = rescale(normalized_doa_est_P_dB, 'InputMin',plot_thresh); 158 | 159 | figure; 160 | x = 1:length(irTrunc(1,1,:)); 161 | y = -180:degreeResolution:180-degreeResolution; 162 | [X,Y] = meshgrid(x,y); 163 | Z = griddata(x,y,P_pwd_n(2:end-1,:),X,Y,'cubic'); 164 | 165 | surf(X,Y,Z,'EdgeColor','none'); 166 | 167 | ylabel('Azimuth (°)'); xlabel('RIR number'); 168 | shading interp; view ([0 90]); axis tight; 169 | colormap(flipud(bone)); 170 | set(gca, 'YTick', -150:75:150); ylim([-180 180]); 171 | pbaspect([1.7 1 1]); box on; 172 | 173 | c2 = colorbar; c2.Label.String = 'Normalised Power'; 174 | title(['Normalised power response, receiver 1: ', methodType, ' method']) 175 | 176 | -------------------------------------------------------------------------------- /demo_interpolate_SRIRs_1D.m: -------------------------------------------------------------------------------- 1 | % Demo script for interpolate_SRIRs.m 2 | % 3 | % The user is directed to the ICA 2022 paper for details: 4 | % McKenzie, T., Meyer-Kahlen, N., Daugintis, R., McCormack, L., Schlecht, S. 5 | % J., & Pulkki, V. (2022). Perceptual interpolation and rendering of coupled 6 | % room spatial room impulse responses. International Congress on Acoustics, 7 | % Korea. 8 | % 9 | % This demo script uses the room transition SRIR dataset: 10 | % McKenzie, T., Schlecht, S. J., & Pulkki, V. (2021). Acoustic analysis 11 | % and dataset of transitions between coupled rooms. IEEE International 12 | % Conference on Acoustics, Speech and Signal Processing, 481–485. 13 | % - download link: http://doi.org/10.5281/zenodo.4095493 14 | % 15 | % Thomas McKenzie, 2022. thomas.mckenzie@aalto.fi / tom.mckenzie07@gmail.com 16 | 17 | % close all 18 | clear 19 | clc 20 | 21 | % Dataset 22 | irPath = '/Users/mckenzt1/Documents/RT Dataset/rt dataset SOFA/SOFA Files/Office to Stairwell SOFA - Dataset of Room Impulse Responses for the Transition between Coupled Rooms/'; 23 | irName = 'Room Transition RIRs_Office to Stairwell_Source in Office_No Line of Sight.sofa'; 24 | 25 | % positions are in the motive system with the "z up" setting which is the 26 | % same, right handed, x in the front system that sofa and the 6DoFconv VST 27 | % use x front y left z up 28 | 29 | sofa = SOFAload([irPath,irName]); 30 | 31 | %% configure 32 | 33 | % choose new inter-measurement distance 34 | resolution_new = 5; 35 | 36 | % minPhase or meanSpectrum direct sound interpolation method 37 | INTERPOLATION_MODE_DS = 'minPhase'; 38 | 39 | fs = sofa.Data.SamplingRate; 40 | 41 | %% REFERENCE DATASET (for evaluating the method) 42 | % CORRECT = non-interpolated full resolution (for evaluating the method) 43 | hCorrect = permute(sofa.Data.IR,[3,2,1]); 44 | positionsCorrect = round(sofa.ListenerPosition*100); % in cm not m 45 | 46 | %% MAKE SPARSE DATASET (for evaluating the method) 47 | % The amount to which to downsample the original measurements 48 | % (2 would mean using 1 of every 2, 10 means 1 in every 10, 49 | % 25 means 1 in every 25, so for 101 measurements it would just be using 5) 50 | measReduction = 25; 51 | 52 | % get the RIR dataset (for evaluating the method) - this would normally 53 | % just be: 54 | % srirs_input = permute(sofa.Data.IR,[3,2,1]); 55 | srirs_input = hCorrect(:,:,1:measReduction:end); 56 | 57 | % reduce the reference dataset (for evaluating the method) --- this should 58 | % probably normally just be: 59 | % pos_input = round(sofa.ListenerPosition*100); 60 | pos_input = positionsCorrect(1:measReduction:end,:); 61 | 62 | %% RUN THE FUNCTION 63 | [srirs_interp,pos_interp] = interpolate_SRIRs(srirs_input,pos_input,resolution_new,fs,INTERPOLATION_MODE_DS); 64 | 65 | %% evaluate 66 | run analyse_interpolate_SRIRs.m 67 | 68 | %% Save to sofa 69 | %{ 70 | SOFAstart() 71 | 72 | Obj = sofa; 73 | Obj.Data.IR = permute(srirs_interp, [3 2 1]); 74 | 75 | % Update dimensions 76 | Obj=SOFAupdateDimensions(Obj); 77 | 78 | % Fill with attributes 79 | Obj.GLOBAL_ListenerShortName = 'EM'; 80 | Obj.GLOBAL_History = 'created on 07.03.2022'; 81 | Obj.GLOBAL_DatabaseName = 'none'; 82 | Obj.GLOBAL_ApplicationName = 'SOFA API'; 83 | Obj.GLOBAL_ApplicationVersion = SOFAgetVersion('API'); 84 | Obj.GLOBAL_Organization = 'Aalto Acoustics Lab'; 85 | Obj.GLOBAL_AuthorContact = 'thomas.mckenzie@aalto.fi'; 86 | Obj.GLOBAL_Comment = ' '; 87 | Obj.GLOABL_Title = 'Responses on a line, interpolated'; 88 | 89 | % save the SOFA file 90 | switch INTERPOLATION_MODE_DS 91 | case 'meanSpectrum' 92 | SOFAfn = fullfile(['srir_interp_ms_erb_',num2str(measReduction),'.sofa']); 93 | case 'minPhase' 94 | SOFAfn = fullfile(['srirInterp_mp_',num2str(measReduction),'.sofa']); 95 | end 96 | 97 | disp(['Saving: ' SOFAfn]); 98 | Obj = SOFAsave(SOFAfn, Obj, 1); 99 | %} 100 | % end 101 | -------------------------------------------------------------------------------- /demo_interpolate_SRIRs_2D.m: -------------------------------------------------------------------------------- 1 | % The user is directed to the ICA 2022 paper for details: 2 | % McKenzie, T., Meyer-Kahlen, N., Daugintis, R., McCormack, L., Schlecht, S. 3 | % J., & Pulkki, V. (2022). Perceptual interpolation and rendering of coupled 4 | % room spatial room impulse responses. International Congress on Acoustics, 5 | % Korea. 6 | % 7 | % This demo script uses a 2D dataset of SRIRs. 8 | % 9 | % Thomas McKenzie, 2022. thomas.mckenzie@aalto.fi / tom.mckenzie07@gmail.com 10 | 11 | % positions are in the motive system with the "z up" setting which is the 12 | % same, right handed, x in the front system that sofa and the 6DoFconv VST 13 | % use x front y left z up 14 | 15 | % close all 16 | clear 17 | clc 18 | 19 | sofa = SOFAload('arni_dataset_no_interp.sofa'); 20 | 21 | %% configure 22 | 23 | % choose new inter-measurement distance 24 | resolution_new = 10; 25 | 26 | % minPhase, rotationOnly or fixedSpectrum or meanSpectrum (direct sound 27 | % interpolation method) 28 | INTERPOLATION_MODE_DS = 'meanSpectrum'; 29 | 30 | % directSoundLength_samp = 200; % empirically chosen 31 | % fade_samp_DS2ER = 20; % DS direct sound ER early refs LR late reverb 32 | % fade_samp_ER2LR = sofa.Data.SamplingRate / 100; % 480 samples 33 | % 34 | % % SH order for interpolated signals 35 | % NshInterp = 4; 36 | 37 | fs = sofa.Data.SamplingRate; 38 | 39 | % get the RIR dataset 40 | srirs_input = permute(sofa.Data.IR,[3,2,1]); 41 | % get positions 42 | pos_input = round(sofa.ListenerPosition*100); 43 | 44 | %% RUN THE FUNCTION 45 | 46 | [srirs_interp,pos_interp] = interpolate_SRIRs(srirs_input,pos_input,resolution_new,fs,INTERPOLATION_MODE_DS); 47 | 48 | %% evaluate 49 | interpType = 'sectors, no time windows'; 50 | 51 | run analyse_interpolated_SRIRs_function_noCorrect.m 52 | 53 | 54 | %% Save to sofa 55 | % %{ 56 | SOFAstart() 57 | 58 | Obj = sofa; 59 | Obj.Data.IR = permute(srirs_interp, [3 2 1]); 60 | 61 | % Update dimensions 62 | Obj=SOFAupdateDimensions(Obj); 63 | 64 | % Fill with attributes 65 | Obj.GLOBAL_ListenerShortName = 'EM'; 66 | Obj.GLOBAL_History = 'created on 07.03.2022'; 67 | Obj.GLOBAL_DatabaseName = 'none'; 68 | Obj.GLOBAL_ApplicationName = 'SOFA API'; 69 | Obj.GLOBAL_ApplicationVersion = SOFAgetVersion('API'); 70 | Obj.GLOBAL_Organization = 'Aalto Acoustics Lab'; 71 | Obj.GLOBAL_AuthorContact = 'thomas.mckenzie@aalto.fi'; 72 | Obj.GLOBAL_Comment = ' '; 73 | Obj.GLOABL_Title = 'Responses on a line, interpolated'; 74 | 75 | % save the SOFA file 76 | % switch INTERPOLATION_MODE_DS 77 | % case 'meanSpectrum' 78 | SOFAfn = fullfile(['srirInterp_2d_noWin.sofa']); 79 | % case 'minPhase' 80 | % SOFAfn = fullfile(['srirInterp_mp_',num2str(measReduction),'.sofa']); 81 | % end 82 | 83 | disp(['Saving: ' SOFAfn]); 84 | Obj = SOFAsave(SOFAfn, Obj, 1); 85 | %} 86 | % end 87 | -------------------------------------------------------------------------------- /demo_interpolate_SRIRs_source.m: -------------------------------------------------------------------------------- 1 | % Demo script for interpolate_SRIRs_source.m 2 | % 3 | % The user is directed to the following paper: 4 | % McKenzie, T. and Schlecht, S. J. "Source position interpolation of 5 | % spatial room impulse responses" AES 154th Convention, Espoo, Helsinki, 6 | % Finland, 2023. 7 | % 8 | % This script uses the following dataset of spatial room impulse response 9 | % measurements: 10 | % McKenzie, T., McCormack, L., and Hold, C., "Dataset of spatial room 11 | % impulse responses in a variable acoustics room for six degrees-of- 12 | % freedom rendering and analysis," in arXiv preprint, pp. 1–3, 2021, 13 | % doi:10.48550/arXiv.2111.11882. 14 | % Dataset download link: https://doi.org/10.5281/zenodo.5720723 15 | % 16 | % Thomas McKenzie, University of Edinburgh, 2023. thomas.mckenzie@ed.ac.uk 17 | 18 | close all 19 | clear 20 | clc 21 | 22 | % Load Variable Acoustics 6DoF Dataset (most reverberant set) 23 | irPath1 = '/Users/tmckenzi/Documents/6DoF Dataset/6dof_SRIRs_eigenmike_SH/'; 24 | irName1 = '6DoF_SRIRs_eigenmike_SH_0percent_absorbers_enabled.sofa'; 25 | 26 | sofa1 = SOFAload([irPath1,irName1]); 27 | fs = sofa1.Data.SamplingRate; 28 | SOFAplotGeometry(sofa1); 29 | 30 | %% Configure interpolation 31 | 32 | division = 5; % divide into x amount of sections (ie 2: one new srir in the middle, 3: 2 new srirs, 4: 3 new srirs, 10: 9 new srirs) 33 | 34 | % how many receiver positions (and which ones in index) 35 | recPosnSel = 1:7; % all positions 36 | % recPosnSel = [2,4,6,7]; % rectangle 37 | % recPosnSel = 1:5; % line 38 | 39 | % which two sources to use: in the 6DoF dataset, 1 is bottom right, 2 is bottom left, and 3 is upper middle 40 | src1 = 1; src2 = 2; 41 | 42 | %% Arrange Input DATASET (for evaluating the method) 43 | 44 | src1_ind = recPosnSel*3-3+src1; 45 | src2_ind = recPosnSel*3-3+src2; 46 | 47 | sofaIRdata = permute(sofa1.Data.IR,[3,2,1]); 48 | 49 | for i = 1:size(sofaIRdata,3) % pre-process of 6DoF SRIRs - 90 degree rotation 50 | sofaIRdata(:,:,i) = rotateHOA_N3D(sofaIRdata(:,:,i),90,0,0); 51 | end 52 | 53 | h1 = sofaIRdata(:,:,src1_ind); 54 | h2 = sofaIRdata(:,:,src2_ind); 55 | 56 | positionsCorrect = round(sofa1.ListenerPosition(src1_ind,:)*100); 57 | 58 | source1_position = round(sofa1.SourcePosition(src1_ind,:)*100); 59 | source2_position = round(sofa1.SourcePosition(src2_ind,:)*100); 60 | 61 | srirs_input1 = h1; 62 | srirs_input2 = h2; 63 | 64 | recPosns = positionsCorrect; 65 | srirs_input = cat(4,srirs_input1, srirs_input2); % if interp all receiver posns, cat 66 | pos_input = [source1_position(1,:);source2_position(1,:)]; % assuming source position doesn't change with receiver position 67 | 68 | %% RUN THE FUNCTION -- BASIC 69 | [srirs_interp,pos_interp] = interpolate_SRIRs_source_basic(srirs_input,pos_input,division,fs); 70 | 71 | %% Evaluate 72 | methodType = 'Basic'; 73 | run analyse_interpolate_SRIRs_source.m 74 | 75 | %% RUN THE FUNCTION -- PROPOSED 76 | [srirs_interp,pos_interp,dir_interp_src] = interpolate_SRIRs_source(srirs_input,pos_input,division,fs); 77 | 78 | %% Evaluate 79 | methodType = 'Proposed'; 80 | run analyse_interpolate_SRIRs_source.m 81 | 82 | %% Save to sofa 83 | 84 | %{ 85 | % Need to decide what to save: ie to save either the rotating source for 86 | % one receiver position, or a fixed (interpolated) source for moving receiver 87 | % positions. 88 | % fixed receiver position, moving source position: 89 | receiver_position = 1; 90 | srirs_interp = squeeze(srirs_interp(:,:,receiver_position,:)); 91 | % fixed source position (interpolated), moving receiver position: 92 | % interp_source_position = round(size(srirs_interp,4)/2); % take middle one 93 | % srirs_interp = squeeze(srirs_interp(:,:,:, interp_source_position)); 94 | 95 | SOFAstart() 96 | Obj = sofa1; 97 | Obj.Data.IR = permute(srirs_interp, [3 2 1]); 98 | 99 | % Get listener and source position for the SOFA file: 100 | % Correct VERSION - This is the actual right way to do it 101 | % Obj.ListenerPosition = repmat(sofa1.ListenerPosition(inputsrir,:),division+1,1); 102 | % Obj.SourcePosition = pos_interp/100; 103 | % Compatibility VERSION - For compatibility with the Sparta 6DoFconv 104 | plugin, which is only made for switching between listener positions 105 | Obj.ListenerPosition = pos_interp/100; 106 | Obj.SourcePosition = repmat(sofa1.ListenerPosition(inputsrir,:),division+1,1); 107 | 108 | % useful for analysis - shows the movement of the direct path from source 109 | % to receiver, but not for the SOFA file - this isn't the 'absolute' source 110 | % view. --- paper shows the arrow plot from the interpolated source 111 | % positions. 112 | % [dir_in_src_cart(1,:),dir_in_src_cart(2,:),dir_in_src_cart(3,:)] = sph2cart(dir_interp_src(1,:),dir_interp_src(2,:),1); 113 | 114 | % Rotate source view 115 | point1 = sofa1.SourceView; 116 | point2 = sofa2.SourceView; 117 | [p1(1), p1(2) ] = cart2sph(point1(1),point1(2),point1(3)); 118 | [p2(1), p2(2) ] = cart2sph(point2(1),point2(2),point2(3)); 119 | p_diff = angdiff(p1,p2); 120 | for i = 1:2 121 | if p_diff(i) == 0 122 | p_diff_div(:,i) = p1(i); 123 | else 124 | p_diff_div(:,i) = p1(i):-p_diff(i)/division:p2(i); 125 | end 126 | end 127 | [p3(:,1),p3(:,2),p3(:,3)] = sph2cart(p_diff_div(:,1),p_diff_div(:,2),1); 128 | 129 | Obj.SourceView = p3; 130 | Obj=SOFAupdateDimensions(Obj); 131 | 132 | SOFAplotGeometry(Obj); 133 | 134 | % Update sofa dimensions 135 | 136 | % Fill with attributes 137 | Obj.GLOBAL_ListenerShortName = 'EM'; 138 | Obj.GLOBAL_History = 'created on 07.04.2023'; 139 | Obj.GLOBAL_DatabaseName = 'none'; 140 | Obj.GLOBAL_ApplicationName = 'SOFA API'; 141 | Obj.GLOBAL_ApplicationVersion = SOFAgetVersion('API'); 142 | Obj.GLOBAL_Organization = 'Acoustics and Audio Group, University of Edinburgh'; 143 | Obj.GLOBAL_AuthorContact = 'thomas.mckenzie@ed.ac.uk'; 144 | Obj.GLOBAL_Comment = ' '; 145 | Obj.GLOABL_Title = 'Source position interpolation'; 146 | 147 | % save the SOFA file 148 | SOFAfn = fullfile(['srir_src_interp.sofa']); 149 | 150 | disp(['Saving: ' SOFAfn]); 151 | Obj = SOFAsave(SOFAfn, Obj, 1); 152 | %} 153 | 154 | 155 | -------------------------------------------------------------------------------- /designMinPhase.m: -------------------------------------------------------------------------------- 1 | function [hmin] = designMinPhase(H, varargin) 2 | % creates a minimum phase FIR Filter to a given 3 | % magnitude frequency response using cepstral windowing. 4 | % cepstral window can be adjusted 5 | % inputs: H ... Magnitude Frequency Responses for positive 6 | % Frequencies dim(H) = (nfft/2+1) x D 7 | % cepsWindowFraction ... default is 2, increase makes filter 8 | % even shorter 9 | % nmk20 10 | 11 | nfft = (size(H, 1) - 1) * 2; 12 | 13 | cwf = 2; 14 | 15 | if nargin > 1 16 | cwf = varargin{1}; 17 | end 18 | 19 | H = abs(H); 20 | H = [H; conj(flipud(H(2:end-1, :)))]; 21 | ceps = ifft(log(H)); 22 | w = [1; 2*ones(nfft/cwf-1,1); 1; zeros(nfft/cwf-1,1)]; 23 | Hmin = exp(fft(ceps.*w)); 24 | hmin = ifft(Hmin); 25 | 26 | end 27 | 28 | -------------------------------------------------------------------------------- /evalSH.m: -------------------------------------------------------------------------------- 1 | function [Ysh] = evalSH(maxOrder, dirs_sph) 2 | % Evaluate real-valued Spherical Harmonics 3 | % maxOrder ... maximal Ambisonics order 4 | % dirs_sph ... directions in sperical coordiates: 5 | % [azi, ele], angles in radiant 6 | % dim(dirs_sph) = Q x 2 7 | % output: Ysh ... matrix of spherical harmonics 8 | % dim(Ysh) = Q x (N + 1)^2 9 | 10 | numDirs = size(dirs_sph, 1); 11 | assert(size(dirs_sph, 2)== 2, ... 12 | 'evalSH needs D x 2 matrix with columns [azimuth, elevation]'); 13 | 14 | phi = dirs_sph(:, 1); % azimuth 15 | theta = dirs_sph(:, 2); % elevation 16 | 17 | % initialize SH matrix 18 | Ysh = zeros(numDirs, (maxOrder+1)^2); 19 | 20 | % compute SHs for every order and sort them into the matrix 21 | for n = 0:maxOrder 22 | 23 | % Azimuthal part 24 | Yazi = [ sqrt(2) * sin(phi * (n:-1:1)), ... 25 | ones(numDirs, 1), sqrt(2) * cos( phi * (1:n))]; 26 | 27 | % Zenithal part 28 | Yzen = legendre(n, cos(pi / 2 - theta))'; 29 | csPhase = ones(numDirs,1) * (-1).^(-n:n) ; 30 | 31 | % normalization 32 | normlz = sqrt( (2*n+1)*factorial(n-abs(-n:n)) ./ ... 33 | (4*pi*factorial(n+abs(-n:n))))'; 34 | 35 | % put together 36 | idxSh = n^2 + n + (-n:n) + 1; 37 | Ysh(:, idxSh) = (Yazi .* csPhase .* [Yzen(:, end:-1:1), Yzen(:, 2:end)] ) .* normlz'; 38 | end 39 | 40 | end 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /interpolate_SRIRs.m: -------------------------------------------------------------------------------- 1 | function [srirs_interp,pos_interp] = interpolate_SRIRs(srirs_input,pos_input,resolution_new,fs,interp_modeDS,length_sampDS,fade_sampDS2ER,fade_sampER2LR,N_interp) 2 | % Perceptual interpolation of SRIRs 3 | % interpolate_SRIRs() 4 | % srirs_input - input format [samples, sh_channels, num_measurements] 5 | % 6 | % The user is directed to the ICA 2022 paper for details: 7 | % McKenzie, T., Meyer-Kahlen, N., Daugintis, R., McCormack, L., Schlecht, S. 8 | % J., & Pulkki, V. (2022). Perceptually informed interpolation and 9 | % rendering of spatial room impulse responses for room transitions. 10 | % International Congress on Acoustics, South Korea. 11 | % 12 | % Requires Spherical Array Processing Toolbox (Politis, 2015) 13 | % (https://github.com/polarch/Spherical-Array-Processing) 14 | % 15 | % Thomas McKenzie, 2022. thomas.mckenzie@ed.ac.uk / tom.mckenzie07@gmail.com 16 | 17 | if nargin<9; N_interp = sqrt(size(srirs_input,2))-1; end 18 | if nargin<8; fade_sampER2LR = fs/100; end 19 | if nargin<7; fade_sampDS2ER = 20; end 20 | if nargin<6; length_sampDS = 200; end 21 | if nargin<5; interp_modeDS = 'minPhase'; end 22 | 23 | %% init 24 | 25 | irLength_samp = size(srirs_input,1); 26 | numMeasurements = size(srirs_input,3); 27 | numAmbiChannels = (N_interp+1)^2; 28 | 29 | nfftDS = 2^nextpow2(length_sampDS); 30 | 31 | % ERB parameters (Glasberg and Moore) 32 | erb_Q = 9.26449; 33 | erb_min = 24.7; 34 | erb_low_freq = 10; 35 | n_bandsER = 48; 36 | n_bandsLR = 48; 37 | 38 | %% Interpolated positions 39 | 40 | % work out whether interpolation should be 1D, 2D or 3D 41 | isPlaneUsed=zeros(size(pos_input,2),1); 42 | for i = 1:size(pos_input,2) 43 | if all(pos_input(:,i) == pos_input(1,i)); isPlaneUsed(i) = 0; 44 | else; isPlaneUsed(i) = 1; end 45 | end 46 | mode_dimension = sum(isPlaneUsed); % 1 = 1D (planar), 2 = 2D, 3 = 3D 47 | 48 | % calculate the interp positions 49 | if mode_dimension == 1 % 1D - assumes interpolating on X axis 50 | positionsInterpolatedX_cm = min(pos_input(:,1)):resolution_new:max(pos_input(:,1)); 51 | positionsInterpolatedY_cm = pos_input(1,2); 52 | positionsInterpolatedZ_cm = pos_input(1,3); 53 | elseif mode_dimension == 2 % 2D - assumes interpolating on X and Y axes 54 | positionsInterpolatedX_cm = min(pos_input(:,1)):resolution_new:max(pos_input(:,1)); 55 | positionsInterpolatedY_cm = min(pos_input(:,2)):resolution_new:max(pos_input(:,2)); 56 | positionsInterpolatedZ_cm = pos_input(1,3); 57 | else % 3D 58 | positionsInterpolatedX_cm = min(pos_input(:,1)):resolution_new:max(pos_input(:,1)); 59 | positionsInterpolatedY_cm = min(pos_input(:,2)):resolution_new:max(pos_input(:,2)); 60 | positionsInterpolatedZ_cm = min(pos_input(:,3)):resolution_new:max(pos_input(:,3)); 61 | end 62 | 63 | % Define a grid of interpolated positions 64 | [interpolatedPositionsX_cm, interpolatedPositionsY_cm, interpolatedPositionsZ_cm] = ... 65 | meshgrid(positionsInterpolatedX_cm, positionsInterpolatedY_cm, positionsInterpolatedZ_cm); 66 | 67 | interpolatedPositionsX_cm = interpolatedPositionsX_cm(:); 68 | interpolatedPositionsY_cm = interpolatedPositionsY_cm(:); 69 | interpolatedPositionsZ_cm = interpolatedPositionsZ_cm(:); 70 | 71 | pos_interp = [interpolatedPositionsX_cm, interpolatedPositionsY_cm, ... 72 | interpolatedPositionsZ_cm]; 73 | numInterpolatedPoints = length(pos_interp); 74 | 75 | %% Calculate the cutoff between early refs and late reverb 76 | % based on when the energy decay curve amplitude is lower than a target 77 | 78 | ER_target = 0.1; % empirically chosen value. could be 1 / exp(1); 79 | earlyRefsCutoff_samp = zeros(1,numMeasurements); 80 | [B_decay, A_decay] = octdsgn(1000, fs, 3); 81 | for i = 1:numMeasurements 82 | % filter and schroeder integration to get EDC. W channel, 1kHz freq band 83 | srir_input_filt = filter(B_decay,A_decay,srirs_input(:,1,i)); 84 | int_sch = 1/length(srir_input_filt) * cumtrapz(flip(srir_input_filt/max(abs(srir_input_filt))).^2); 85 | edc = flip(int_sch(2:end)); 86 | edc = edc/max(edc); 87 | 88 | ind_at_t_L = find(edc=ERBfreqs(i),1); 269 | end 270 | freqsInd(1) = 1; freqsInd(end) = find(freqs>=22000,1); % limit the first and last 271 | 272 | % over frequency bands, obtain the RMS magnitude difference of the interpolated and nearest IRs 273 | rmsER_target = zeros(n_bandsER-1,1); 274 | rmsER_current = zeros(n_bandsER-1,1); 275 | for j = 1:n_bandsER-1 % calculate rms of freq bands 276 | rmsER_target(j,:) = sum(rms(XERnearestMeas_gain_sec(freqsInd(j):freqsInd(j+1),1,:)),3); 277 | rmsER_current(j,:) = rms(XERnearestMeas_interp_sec(freqsInd(j):freqsInd(j+1),1)); 278 | end 279 | rmsDiffER = rmsER_target ./ rmsER_current; 280 | 281 | % create array of RMS equalisation gains 282 | rmsGainsER = zeros(length(XERnearestMeas_interp_sec),1); 283 | for j = 1:n_bandsER-1 284 | if j == n_bandsER-1 % last band 285 | rmsGainsER(freqsInd(j):freqsInd(j+1),1) = ... 286 | linspace(rmsDiffER(j,1),rmsDiffER(j,1),length(freqsInd(j):freqsInd(j+1))); 287 | else 288 | rmsGainsER(freqsInd(j):freqsInd(j+1),1) = ... 289 | linspace(rmsDiffER(j,1),rmsDiffER(j+1,1),length(freqsInd(j):freqsInd(j+1))); 290 | end 291 | end 292 | % fade out the 20khz band to the nfft/2 band with a cosine window 293 | rmsGainsER(freqsInd(end)+1:nfftER/2) = (cos((0:length(rmsGainsER(freqsInd(end)+1:nfftER/2))-1)' /... 294 | (length(rmsGainsER(freqsInd(end)+1:nfftER/2))-1) * pi / 2).^2)*rmsDiffER(end); 295 | rmsGainsER(nfftER/2+1:end) = flip(rmsGainsER(1:nfftER/2)); 296 | 297 | % apply gains 298 | HER_interp_sec = XERnearestMeas_interp_sec.*rmsGainsER; 299 | HER_interp_secs(:,secIdx) = HER_interp_sec; 300 | end 301 | 302 | % back into SH domain (still in Freq domain though) 303 | HER_interp = HER_interp_secs * w_nm'; 304 | 305 | % needs some normalisation due to the beamWeightsMaxEV 306 | w_n_gain = w_n.^2 * length(secDirs(:,1)) ./ ((0:1:N_interp) * 2 + 1)'; 307 | for i = 1:length(HER_interp(1,:)) 308 | n = ceil(sqrt(i)-1); 309 | HER_interp(:,i) = HER_interp(:,i) / w_n_gain(n+1); 310 | end 311 | 312 | % return to time-domain and window 313 | hER_interp = real(ifft(HER_interp)); 314 | 315 | % normalise interpolated early reflections -- for each SH channel 316 | rmsNormValueER = sum(rms(earlyRefsIsolated) .* repmat(gainsSH,1,numAmbiChannels),3); 317 | hER_interp = hER_interp ./ rms(hER_interp) .* rmsNormValueER; 318 | 319 | hAllER_interp(earlyRefsStart_samp:earlyRefsEnd_samp, :, iInterp) = ... 320 | hER_interp; 321 | end 322 | 323 | %% Interpolate late reverb 324 | if sortedDistances(1) == 0 % if it's an exact point, don't interpolate 325 | hAllLR_interp(lateRevStart_samp:end,:,iInterp) = ... 326 | lateRevIsolated(:, 1:numAmbiChannels, 1); 327 | else 328 | XLRnearestMeas = fft(lateRevIsolated); 329 | 330 | % linear interpolation 331 | XLRnearestMeas_gain = XLRnearestMeas .* gainsSH; 332 | XLRnearestMeas_interp = sum(XLRnearestMeas_gain,3); 333 | 334 | % ERB frequency bands 335 | ERBfreqs = flip(-(erb_Q*erb_min)+exp((1:n_bandsLR)'*(-log(fs/2 + erb_Q*erb_min) + ... 336 | log(erb_low_freq + erb_Q*erb_min))/n_bandsLR) * (fs/2 + erb_Q*erb_min)); 337 | freqs = 1:fs/nfftLR:fs; 338 | freqsInd = zeros(n_bandsLR,1); 339 | for i = 1:length(ERBfreqs) 340 | freqsInd(i) = find(freqs>=ERBfreqs(i),1); 341 | end 342 | freqsInd(1) = 1; freqsInd(end) = find(freqs>=22000,1); % limit the first and last 343 | 344 | % over frequency bands, obtain the RMS magnitude difference of the interpolated and nearest IRs 345 | rmsLR_target = zeros(n_bandsLR-1,1); 346 | rmsLR_current = zeros(n_bandsLR-1,1); 347 | for j = 1:n_bandsLR-1 % calculate rms of freq bands 348 | rmsLR_target(j,:) = sum(rms(XLRnearestMeas_gain(freqsInd(j):freqsInd(j+1),1,:)),3); 349 | rmsLR_current(j,:) = rms(XLRnearestMeas_interp(freqsInd(j):freqsInd(j+1),1)); 350 | end 351 | rmsDiffLR = rmsLR_target ./ rmsLR_current; 352 | 353 | % create array of RMS equalisation gains 354 | rmsGainsLR = zeros(length(XLRnearestMeas_interp),1); 355 | for j = 1:n_bandsLR-1 356 | if j == n_bandsLR-1 % last band 357 | rmsGainsLR(freqsInd(j):freqsInd(j+1),1) = ... 358 | linspace(rmsDiffLR(j,1),rmsDiffLR(j,1),length(freqsInd(j):freqsInd(j+1))); 359 | else 360 | rmsGainsLR(freqsInd(j):freqsInd(j+1),1) = ... 361 | linspace(rmsDiffLR(j,1),rmsDiffLR(j+1,1),length(freqsInd(j):freqsInd(j+1))); 362 | end 363 | end 364 | % fade out the 20khz band to the nfft/2 band with a cosine window 365 | rmsGainsLR(freqsInd(end)+1:nfftLR/2) = (cos((0:length(rmsGainsLR(freqsInd(end)+1:nfftLR/2))-1)' /... 366 | (length(rmsGainsLR(freqsInd(end)+1:nfftLR/2))-1) * pi / 2).^2)*rmsDiffLR(end); 367 | rmsGainsLR(nfftLR/2+1:end) = flip(rmsGainsLR(1:nfftLR/2)); 368 | 369 | % apply gains, return to time domain and windowing at start and end of LR 370 | hLR_interp = real(ifft(XLRnearestMeas_interp.*repmat(rmsGainsLR,[1,numAmbiChannels]))); 371 | 372 | % normalise late reverb for each SH channel 373 | rmsNormValueLR = sum(rms(lateRevIsolated) .* repmat(gainsSH,1,numAmbiChannels),3); 374 | hLR_interp = hLR_interp ./ rms(hLR_interp) .* rmsNormValueLR; 375 | 376 | hAllLR_interp(lateRevStart_samp:end, :, iInterp) = ... 377 | hLR_interp; 378 | end 379 | 380 | %% Construct the final IRs 381 | srirs_interp(:, :, iInterp) = hAllDS_interp(:, :, iInterp) + ... 382 | hAllER_interp(:, :, iInterp) +... 383 | hAllLR_interp(:, :, iInterp); 384 | 385 | end 386 | end -------------------------------------------------------------------------------- /interpolate_SRIRs_source.m: -------------------------------------------------------------------------------- 1 | function [srirs_interp,pos_interp_src,dir_interp_src] = interpolate_SRIRs_source(srirs_input,pos_input,division,fs,interp_modeDS,length_sampDS,fade_sampDS2ER,fade_sampER2LR,N_interp) 2 | % Perceptually informed interpolation of SRIRs with different source 3 | % positions 4 | % 5 | % interpolate_SRIRs_source() 6 | % srirs_input - input format [samples, sh_channels, num_measurements] 7 | % 8 | % The user is directed to the following paper: 9 | % McKenzie, T. and Schlecht, S. J. "Source position interpolation of 10 | % spatial room impulse responses" AES 154th Convention, Espoo, Helsinki, 11 | % Finland, 2023. 12 | % 13 | % Requires Spherical Array Processing Toolbox (Politis, 2015) 14 | % (https://github.com/polarch/Spherical-Array-Processing) 15 | % 16 | % Thomas McKenzie, University of Edinburgh, 2023. thomas.mckenzie@ed.ac.uk 17 | 18 | if nargin<9; N_interp = sqrt(size(srirs_input,2))-1; end 19 | if nargin<8; fade_sampER2LR = fs/100; end 20 | if nargin<7; fade_sampDS2ER = 20; end 21 | if nargin<6; length_sampDS = 200; end 22 | if nargin<5; interp_modeDS = 'meanSpectrum'; end 23 | 24 | reinsertTimeAlignmentDelayAfterInterp = 0; 25 | 26 | %% init 27 | irLength_samp = size(srirs_input,1); 28 | numMeas = size(srirs_input,4); % 4th dimension for num of interpolation measurements 29 | numRecPosns = size(srirs_input,3); % 3rd dimension for number of receiver positions 30 | numAmbiChans = (N_interp+1)^2; % number of Ambisonic channels 31 | nfftDS = 2^nextpow2(length_sampDS); 32 | 33 | % ERB parameters (Glasberg and Moore) 34 | erb_Q = 9.26449;erb_min = 24.7;erb_low_freq = 10; 35 | n_bandsDS = 48;n_bandsER = 48;n_bandsLR = 48; 36 | 37 | %% Interpolated positions 38 | mode_dimension = 1; % fixed for now 39 | 40 | % for the division: interval is how many RIRs are we interpolating here 41 | interval = (pos_input(1,:) - pos_input(2,:))/(division); 42 | for i = 1:3 43 | if interval(i) == 0 44 | pos_interp_src(:,i) = pos_input(1,i); 45 | else 46 | pos_interp_src(:,i) = pos_input(1,i) : - interval(i) : pos_input(2,i); 47 | end 48 | end 49 | numInterpPts = size(pos_interp_src,1); 50 | 51 | %% Reorder input SRIRs 52 | srirs_input = permute(srirs_input,[1 2 4 3]); % reorder here: indexes are now [samples, SH channels, source positions, receiver positions] 53 | srirs_interp = zeros(irLength_samp, numAmbiChans, numInterpPts,numRecPosns); 54 | 55 | %% Time align onsets 56 | initialInputGap = 50; % pre-delay before the detected onset for IRs 57 | onsetThreshold = 0.1; % empirically chosen threshold used for truncation of IRs, chosen by visual observation of onset and noise floor amplitudes 58 | 59 | indexMin = zeros(size(srirs_input,3),size(srirs_input,4)); 60 | for i = 1:size(srirs_input,3) 61 | for j = 1:size(srirs_input,4) 62 | indexMin(i,j) = find(abs(srirs_input(:,1,i,j)/max(abs(srirs_input(:,1,i,j))))>onsetThreshold,1); 63 | if indexMin(i,j) <= initialInputGap 64 | initialInputGap = 0; % catch it if the onset is already below the input gap. 65 | end 66 | srirs_input(:,:,i,j) = [srirs_input(indexMin(i,j) - initialInputGap:end,:,i,j) ; zeros((indexMin(i,j) - initialInputGap-1),numAmbiChans)]; 67 | end 68 | end 69 | 70 | %% Interpolate between source positions for each receiver position 71 | for iRec = 1:numRecPosns 72 | 73 | %% Calculate the cutoff between early refs and late reverb 74 | % based on when the energy decay curve amplitude is lower than a target 75 | 76 | ER_target = 0.1; % empirically chosen value. could be 1 / exp(1); 77 | ERCutoff_samp = zeros(1,numMeas); 78 | [B_decay, A_decay] = octave_filter(1000, fs, 3); 79 | for i = 1:numMeas 80 | % filter and schroeder integration to get EDC. W channel, 1kHz freq band 81 | srir_input_filt = filter(B_decay,A_decay,srirs_input(:,1,i,iRec)); 82 | int_sch = 1/length(srir_input_filt) * cumtrapz(flip(srir_input_filt/max(abs(srir_input_filt))).^2); 83 | edc = flip(int_sch(2:end)); 84 | edc = edc/max(edc); 85 | 86 | ind_at_t_L = find(edc=ERBfreqs(i),1); 215 | end 216 | freqsInd(1) = 1; freqsInd(end) = find(freqs>=22000,1); % limit the first and last 217 | 218 | % over frequency bands, obtain the RMS magnitude difference of the interpolated and nearest IRs 219 | rmsDS_target = zeros(n_bandsDS-1,1); 220 | rmsDS_current = zeros(n_bandsDS-1,1); 221 | for j = 1:n_bandsDS-1 % calculate rms of freq bands 222 | rmsDS_target(j,:) = sum(rms(XDSnearestMeas_gain(freqsInd(j):freqsInd(j+1),1,:)),3); 223 | rmsDS_current(j,:) = rms(XDSnearestMeas_interp(freqsInd(j):freqsInd(j+1),1)); 224 | end 225 | rmsDiffDS = rmsDS_target ./ rmsDS_current; 226 | 227 | % create array of RMS equalisation gains 228 | rmsGainsDS = zeros(length(XDSnearestMeas_interp),1); 229 | for j = 1:n_bandsDS-1 230 | if j == n_bandsDS-1 % last band 231 | rmsGainsDS(freqsInd(j):freqsInd(j+1),1) = ... 232 | repmat(rmsDiffDS(j,1),1,length(freqsInd(j):freqsInd(j+1))); 233 | else 234 | rmsGainsDS(freqsInd(j):freqsInd(j+1),1) = ... 235 | linspace(rmsDiffDS(j,1),rmsDiffDS(j+1,1),length(freqsInd(j):freqsInd(j+1))); 236 | end 237 | end 238 | % continue the 20khz band to the nfft/2 band and flip 2nd half 239 | rmsGainsDS(freqsInd(end)+1:nfftDS/2) = rmsDiffDS(end); 240 | rmsGainsDS(nfftDS/2+1:end) = flip(rmsGainsDS(1:nfftDS/2)); 241 | 242 | % apply gains, return to time domain and windowing at start and end of DS 243 | hDS_enc = real(ifft(XDSnearestMeas_interp.*repmat(rmsGainsDS,[1,numAmbiChans]))); 244 | hDS_enc = hDS_enc(1:length_sampDS,:); 245 | 246 | % normalise RMS of direct sound 247 | rmsNormValue = sum(rms(squeeze(nearestMeasDS(:,1,:))) * gains); 248 | hDS_enc = hDS_enc / rms(hDS_enc(:, 1)) * rmsNormValue; 249 | hAllDS_interp(1:length_sampDS, :, iInterp) = hDS_enc; 250 | end 251 | 252 | case 'minPhase' 253 | % interpolate direct sound spectrum of the nearest 4 points 254 | % (if 2d) or 2 points (if 1d) 255 | nearestMeasDS = ... 256 | squeeze(hAllDS(1:length_sampDS, 1, idxSorted(1:2^mode_dimension))); 257 | XDSnearestMeas = fft(nearestMeasDS, nfftDS); 258 | XDSnearestMeas_smooth = ... 259 | smoothSpectrum(abs(XDSnearestMeas(1:nfftDS/2+1, :)), fax(:), 3); 260 | XDSnearestMeas_interp = XDSnearestMeas_smooth * gains; 261 | hMinPhase = designMinPhase(XDSnearestMeas_interp); 262 | 263 | % encode to correct rotation. Remove the convert function if 264 | % the SRIRs are in N3D normalisation 265 | hDS_enc = hMinPhase(1:length_sampDS) * ... 266 | convert_N3D_SN3D(evalSH(N_interp, dsTarget),'n2sn'); 267 | 268 | % normalise RMS of direct sound 269 | rmsNormValue = sum(rms(squeeze(nearestMeasDS(:,1,:))) * gains); 270 | hDS_enc = hDS_enc / rms(hDS_enc(:, 1)) * rmsNormValue; 271 | hAllDS_interp(1:length_sampDS, :, iInterp) = hDS_enc; 272 | end 273 | 274 | %% interpolate early reflections 275 | if sortedDistances(1) == 0 % if it's an exact point, don't interpolate 276 | hAllER_interp(earlyRefsStart_samp:earlyRefsEnd_samp,:,iInterp) = ... 277 | earlyRefsIsolated(:,1:numAmbiChans,1); 278 | else 279 | % take fft of nearest measurements, weight by gains 280 | XERnearestMeas = fft(earlyRefsIsolated); 281 | XERnearestMeas_gain = XERnearestMeas .* gainsSH; 282 | 283 | % then get the weighted nearest measurements in sectors 284 | XERnearestMeas_gain_secs = ... 285 | zeros(size(XERnearestMeas_gain,1),numSecs,size(XERnearestMeas_gain,3)); 286 | for i = 1:2^mode_dimension 287 | XERnearestMeas_gain_secs(:,:,i) = XERnearestMeas_gain(:,:,i) * w_nm; 288 | end 289 | 290 | HER_interp_secs = zeros(size(XERnearestMeas_gain,1),numSecs); 291 | for secIdx = 1:numSecs % for each sector: 292 | XERnearestMeas_gain_sec = XERnearestMeas_gain_secs(:, secIdx,:); 293 | XERnearestMeas_interp_sec = sum(XERnearestMeas_gain_sec,3); 294 | 295 | % ERB frequency bands 296 | ERBfreqs = flip(-(erb_Q*erb_min)+exp((1:n_bandsER)'*(-log(fs/2 + erb_Q*erb_min) + ... 297 | log(erb_low_freq + erb_Q*erb_min))/n_bandsER) * (fs/2 + erb_Q*erb_min)); 298 | freqs = 1:fs/nfftER:fs; 299 | freqsInd = zeros(n_bandsER,1); 300 | for i = 1:length(ERBfreqs) 301 | freqsInd(i) = find(freqs>=ERBfreqs(i),1); 302 | end 303 | freqsInd(1) = 1; freqsInd(end) = find(freqs>=22000,1); % limit the first and last 304 | 305 | % over frequency bands, obtain the RMS magnitude difference of the interpolated and nearest IRs 306 | rmsER_target = zeros(n_bandsER-1,1); 307 | rmsER_current = zeros(n_bandsER-1,1); 308 | for j = 1:n_bandsER-1 % calculate rms of freq bands 309 | rmsER_target(j,:) = sum(rms(XERnearestMeas_gain_sec(freqsInd(j):freqsInd(j+1),1,:)),3); 310 | rmsER_current(j,:) = rms(XERnearestMeas_interp_sec(freqsInd(j):freqsInd(j+1),1)); 311 | end 312 | rmsDiffER = rmsER_target ./ rmsER_current; 313 | 314 | % create array of RMS equalisation gains 315 | rmsGainsER = zeros(length(XERnearestMeas_interp_sec),1); 316 | for j = 1:n_bandsER-1 317 | if j == n_bandsER-1 % last band 318 | rmsGainsER(freqsInd(j):freqsInd(j+1),1) = ... 319 | repmat(rmsDiffER(j,1),1,length(freqsInd(j):freqsInd(j+1))); 320 | else 321 | rmsGainsER(freqsInd(j):freqsInd(j+1),1) = ... 322 | linspace(rmsDiffER(j,1),rmsDiffER(j+1,1),length(freqsInd(j):freqsInd(j+1))); 323 | end 324 | end 325 | % fade out the 20khz band to the nfft/2 band with a cosine window 326 | rmsGainsER(freqsInd(end)+1:nfftER/2) = (cos((0:length(rmsGainsER(freqsInd(end)+1:nfftER/2))-1)' /... 327 | (length(rmsGainsER(freqsInd(end)+1:nfftER/2))-1) * pi / 2).^2)*rmsDiffER(end); 328 | rmsGainsER(nfftER/2+1:end) = flip(rmsGainsER(1:nfftER/2)); 329 | 330 | % apply gains 331 | HER_interp_sec = XERnearestMeas_interp_sec.*rmsGainsER; 332 | HER_interp_secs(:,secIdx) = HER_interp_sec; 333 | end 334 | 335 | % back into SH domain (still in Freq domain though) 336 | HER_interp = HER_interp_secs * w_nm'; 337 | 338 | % needs some normalisation due to the beamWeightsMaxEV 339 | w_n_gain = w_n.^2 * length(secDirs(:,1)) ./ ((0:1:N_interp) * 2 + 1)'; 340 | for i = 1:length(HER_interp(1,:)) 341 | n = ceil(sqrt(i)-1); 342 | HER_interp(:,i) = HER_interp(:,i) / w_n_gain(n+1); 343 | end 344 | 345 | % return to time-domain and window 346 | hER_interp = real(ifft(HER_interp)); 347 | 348 | % normalise interpolated early reflections -- for each SH channel 349 | rmsNormValueER = sum(rms(earlyRefsIsolated) .* repmat(gainsSH,1,numAmbiChans),3); 350 | hER_interp = hER_interp ./ rms(hER_interp) .* rmsNormValueER; 351 | 352 | hAllER_interp(earlyRefsStart_samp:earlyRefsEnd_samp, :, iInterp) = ... 353 | hER_interp; 354 | end 355 | 356 | %% Interpolate late reverb 357 | if sortedDistances(1) == 0 % if it's an exact point, don't interpolate 358 | hAllLR_interp(lateRevStart_samp:end,:,iInterp) = ... 359 | lateRevIsolated(:, 1:numAmbiChans, 1); 360 | else 361 | XLRnearestMeas = fft(lateRevIsolated); 362 | 363 | % linear interpolation 364 | XLRnearestMeas_gain = XLRnearestMeas .* gainsSH; 365 | XLRnearestMeas_interp = sum(XLRnearestMeas_gain,3); 366 | 367 | % ERB frequency bands 368 | ERBfreqs = flip(-(erb_Q*erb_min)+exp((1:n_bandsLR)'*(-log(fs/2 + erb_Q*erb_min) + ... 369 | log(erb_low_freq + erb_Q*erb_min))/n_bandsLR) * (fs/2 + erb_Q*erb_min)); 370 | freqs = 1:fs/nfftLR:fs; 371 | freqsInd = zeros(n_bandsLR,1); 372 | for i = 1:length(ERBfreqs) 373 | freqsInd(i) = find(freqs>=ERBfreqs(i),1); 374 | end 375 | freqsInd(1) = 1; freqsInd(end) = find(freqs>=22000,1); % limit the first and last 376 | 377 | % over frequency bands, obtain the RMS magnitude difference of the interpolated and nearest IRs 378 | rmsLR_target = zeros(n_bandsLR-1,1); 379 | rmsLR_current = zeros(n_bandsLR-1,1); 380 | for j = 1:n_bandsLR-1 % calculate rms of freq bands 381 | rmsLR_target(j,:) = sum(rms(XLRnearestMeas_gain(freqsInd(j):freqsInd(j+1),1,:)),3); 382 | rmsLR_current(j,:) = rms(XLRnearestMeas_interp(freqsInd(j):freqsInd(j+1),1)); 383 | end 384 | rmsDiffLR = rmsLR_target ./ rmsLR_current; 385 | 386 | % create array of RMS equalisation gains 387 | rmsGainsLR = zeros(length(XLRnearestMeas_interp),1); 388 | for j = 1:n_bandsLR-1 389 | if j == n_bandsLR-1 % last band 390 | rmsGainsLR(freqsInd(j):freqsInd(j+1),1) = ... 391 | repmat(rmsDiffLR(j,1),1,length(freqsInd(j):freqsInd(j+1))); 392 | else 393 | rmsGainsLR(freqsInd(j):freqsInd(j+1),1) = ... 394 | linspace(rmsDiffLR(j,1),rmsDiffLR(j+1,1),length(freqsInd(j):freqsInd(j+1))); 395 | end 396 | end 397 | % fade out the 20khz band to the nfft/2 band with a cosine window 398 | rmsGainsLR(freqsInd(end)+1:nfftLR/2) = (cos((0:length(rmsGainsLR(freqsInd(end)+1:nfftLR/2))-1)' /... 399 | (length(rmsGainsLR(freqsInd(end)+1:nfftLR/2))-1) * pi / 2).^2)*rmsDiffLR(end); 400 | rmsGainsLR(nfftLR/2+1:end) = flip(rmsGainsLR(1:nfftLR/2)); 401 | 402 | % apply gains, return to time domain and windowing at start and end of LR 403 | hLR_interp = real(ifft(XLRnearestMeas_interp.*repmat(rmsGainsLR,[1,numAmbiChans]))); 404 | 405 | % normalise late reverb for each SH channel 406 | rmsNormValueLR = sum(rms(lateRevIsolated) .* repmat(gainsSH,1,numAmbiChans),3); 407 | hLR_interp = hLR_interp ./ rms(hLR_interp) .* rmsNormValueLR; 408 | 409 | hAllLR_interp(lateRevStart_samp:end, :, iInterp) = hLR_interp; 410 | end 411 | 412 | %% Construct the final IRs 413 | srirs_interp_1rec(:, :, iInterp) = hAllDS_interp(:, :, iInterp) + ... 414 | hAllER_interp(:, :, iInterp) + hAllLR_interp(:, :, iInterp); 415 | 416 | end 417 | srirs_interp(:,:,:,iRec) = srirs_interp_1rec; 418 | end 419 | 420 | %% reinsert delay from time-alignment? 421 | if reinsertTimeAlignmentDelayAfterInterp 422 | for j = 1:size(srirs_interp,4) 423 | delay_interp = round(linspace(indexMin(1,j),indexMin(2,j),size(srirs_interp,3))); 424 | for i = 1:size(srirs_interp,3) 425 | srirs_interp(:,:,i,j) = [zeros(delay_interp(i),numAmbiChans) ; srirs_interp(1: end-delay_interp(i),:,i,j)]; 426 | end 427 | end 428 | end 429 | 430 | %% Change ordering back 431 | srirs_interp = permute(srirs_interp,[1 2 4 3]); 432 | 433 | end 434 | 435 | %% Extra functions 436 | function [B,A] = octave_filter(fc,fs,N) 437 | % Requires the Signal Processing Toolbox. 438 | % Based on the filter design by Christophe Couvreur, Faculte Polytechnique de Mons (Belgium) 439 | % Designs Butterworth 2Nth-order octave filter based on a bilinear transformation (ANSI S1.1-1986) 440 | 441 | if nargin < 3; N = 3; end 442 | if (fc > 0.7*(fs/2)); error('Design not possible'); end 443 | 444 | b = pi/2/N/sin(pi/2/N); 445 | a = (1+sqrt(1+8*b^2))/4/b; 446 | W1 = fc/(fs/2)*sqrt(1/2)/a; 447 | W2 = fc/(fs/2)*sqrt(2)*a; 448 | [B,A] = butter(N,[W1,W2]); 449 | end 450 | 451 | 452 | -------------------------------------------------------------------------------- /octdsgn.m: -------------------------------------------------------------------------------- 1 | function [B,A] = octdsgn(Fc,Fs,N); 2 | % OCTDSGN Design of an octave filter. 3 | % [B,A] = OCTDSGN(Fc,Fs,N) designs a digital octave filter with 4 | % center frequency Fc for sampling frequency Fs. 5 | % The filter are designed according to the Order-N specification 6 | % of the ANSI S1.1-1986 standard. Default value for N is 3. 7 | % Warning: for meaningful design results, center values used 8 | % should preferably be in range Fs/200 < Fc < Fs/5. 9 | % Usage of the filter: Y = FILTER(B,A,X). 10 | % 11 | % Requires the Signal Processing Toolbox. 12 | % 13 | % See also OCTSPEC, OCT3DSGN, OCT3SPEC. 14 | 15 | % Author: Christophe Couvreur, Faculte Polytechnique de Mons (Belgium) 16 | % couvreur@thor.fpms.ac.be 17 | % Last modification: Aug. 22, 1997, 9:00pm. 18 | 19 | % References: 20 | % [1] ANSI S1.1-1986 (ASA 65-1986): Specifications for 21 | % Octave-Band and Fractional-Octave-Band Analog and 22 | % Digital Filters, 1993. 23 | 24 | if (nargin > 3) | (nargin < 2) 25 | error('Invalide number of arguments.'); 26 | end 27 | if (nargin == 2) 28 | N = 3; 29 | end 30 | if (Fc > 0.70*(Fs/2)) 31 | error('Design not possible. Check frequencies.'); 32 | end 33 | 34 | % Design Butterworth 2Nth-order octave filter 35 | % Note: BUTTER is based on a bilinear transformation, as suggested in [1]. 36 | %W1 = Fc/(Fs/2)*sqrt(1/2); 37 | %W2 = Fc/(Fs/2)*sqrt(2); 38 | pi = 3.14159265358979; 39 | beta = pi/2/N/sin(pi/2/N); 40 | alpha = (1+sqrt(1+8*beta^2))/4/beta; 41 | W1 = Fc/(Fs/2)*sqrt(1/2)/alpha; 42 | W2 = Fc/(Fs/2)*sqrt(2)*alpha; 43 | [B,A] = butter(N,[W1,W2]); 44 | --------------------------------------------------------------------------------