Aufgrund einer Wartung wird GitLab am 26.10. zwischen 8:00 und 9:00 Uhr kurzzeitig nicht zur Verfügung stehen. / Due to maintenance, GitLab will be temporarily unavailable on 26.10. between 8:00 and 9:00 am.

Commit ad3f6de7 authored by Michael Kohnen's avatar Michael Kohnen
Browse files

added metadata handling to itaHRTF

parents 9b2df9f3 2a77cc7c
...@@ -19,6 +19,11 @@ function cThis = interp(this,varargin) ...@@ -19,6 +19,11 @@ function cThis = interp(this,varargin)
% set to 1 if no range extrapolation is required % set to 1 if no range extrapolation is required
% order ... order of spherical harmonics matrix (default: 50) % order ... order of spherical harmonics matrix (default: 50)
% epsilon ... regularization coefficient (default: 1e-8) % epsilon ... regularization coefficient (default: 1e-8)
% shiftToEar to a shift to approximate ear position to
% improve sh transformation (see [2])
% shiftAxis shift along this axis ('x','y' (default),'z')
% shiftOffset shift ears (L - R) by these values
% (default: [-0.0725 0.0725])
% %
% OUTPUT: % OUTPUT:
% itaHRTF object % itaHRTF object
...@@ -26,28 +31,34 @@ function cThis = interp(this,varargin) ...@@ -26,28 +31,34 @@ function cThis = interp(this,varargin)
% .timeData: interpolated / range-extrapolated HRIRs for defined field points % .timeData: interpolated / range-extrapolated HRIRs for defined field points
% .dirCoord: itaCoordinates object % .dirCoord: itaCoordinates object
% %
% Required: SphericalHarmonics functions of ITA Toolbox %
% %
% [1] Pollow, Martin et al., "Calculation of Head-Related Transfer Functions % [1] Pollow, Martin et al., "Calculation of Head-Related Transfer Functions
% for Arbitrary Field Points Using Spherical Harmonics Decomposition", % for Arbitrary Field Points Using Spherical Harmonics Decomposition",
% Acta Acustica united with Acustica, Volume 98, Number 1, January/February 2012, % Acta Acustica united with Acustica, Volume 98, Number 1, January/February 2012,
% pp. 72-82(11) % pp. 72-82(11)
% %
% [2] Richter, Jan-Gerrit et al. "Spherical harmonics based hrtf datasets:
% Implementation and evaluation for real-time auralization",
% Acta Acustica united with Acustica, Volume 100, Number 4, July/August 2014,
% pp. 667-675(9)
%
%
%
% Author: Florian Pausch <fpa@akustik.rwth-aachen.de> % Author: Florian Pausch <fpa@akustik.rwth-aachen.de>
% Version: 2016-02-05 % Version: 2016-02-05
% TODO: check why this is still not working (coordinate assignment???) sArgs = struct('order',50,'eps',1e-5,'shiftToEar',false,'shiftAxis','y','shiftOffset',[-0.0725 0.0725]);
sArgs = struct('order',50,'eps',1e-5);
sArgs = ita_parse_arguments(sArgs,varargin,2); sArgs = ita_parse_arguments(sArgs,varargin,2);
if ~isa(varargin{1},'itaCoordinates'),error('itaHRTF:interp', ' An itaCoordinate object is needed!') if isempty(varargin) || ~isa(varargin{1},'itaCoordinates')
error('itaHRTF:interp', ' An itaCoordinate object is needed!')
end end
field_in = varargin{1}; field_in = varargin{1};
% only take unique direction coordinates (round to 0.01deg resolution) % only take unique direction coordinates (round to 0.01deg resolution)
tempfield = unique(round([field_in.phi_deg*100 field_in.theta_deg*100]),'rows'); % may cause problems with older Matlab versions (<=R2013)! tempfield = unique(round([field_in.phi_deg*100 field_in.theta_deg*100]),'rows'); % may cause problems with older Matlab versions (<=R2013)!
tempfield = tempfield./100; tempfield = tempfield./100;
temp_r = this.dirCoord.r(1); temp_r = field_in.r(1);
field = itaCoordinates(size(tempfield,1)); field = itaCoordinates(size(tempfield,1));
field.r = repmat(temp_r,size(tempfield,1),1); field.r = repmat(temp_r,size(tempfield,1),1);
field.phi_deg = tempfield(:,1); field.phi_deg = tempfield(:,1);
...@@ -67,46 +78,34 @@ if ~isequal(this.dirCoord.r(1),field.r(1)) ...@@ -67,46 +78,34 @@ if ~isequal(this.dirCoord.r(1),field.r(1))
kr0 = k*this.dirCoord.r(1); % measurement radius kr0 = k*this.dirCoord.r(1); % measurement radius
kr1 = k*field.r(1); % extrapolation radius kr1 = k*field.r(1); % extrapolation radius
hankel_r0 = ita_sph_besselh(1:Nmax,2,kr0); hankel_r0 = ita_sph_besselh(ita_sph_linear2degreeorder(1:Nmax),2,kr0);
hankel_r1 = ita_sph_besselh(1:Nmax,2,kr1); hankel_r1 = ita_sph_besselh(ita_sph_linear2degreeorder(1:Nmax),2,kr1);
hankel_div = hankel_r1 ./ hankel_r0; hankel_div = hankel_r1 ./ hankel_r0;
hankel_rep = hankel_div(:,1); hankel_rep = hankel_div(:,1);
end end
dweights = 1 + (0:Nmax).*((0:Nmax)+1); % calculate regularization weights nSH = (Nmax+1).^2;
I = sparse(eye(nSH));
n = ita_sph_linear2degreeorder(1:round(nSH)).';
dweights_rep = zeros(sum(2*(0:Nmax)'+1),1); %% move data from earcenter
dweights_rep(1)=dweights(1); copyData = this;
counter = 2; if sArgs.shiftToEar
for n=1:Nmax ear_d = sArgs.shiftOffset;
nTimes = 2*n+1; for ear=1:2
dweights_rep(counter:counter+nTimes-1)=dweights(n+1)*ones(nTimes,1); movedData = moveFullDataSet(this.ch(ear:2:this.nChannels),sArgs,ear_d(ear));
copyData.freqData(:,ear:2:copyData.nChannels) = movedData;
if ~isequal(this.dirCoord.r(1),field.r(1))
hankel_rep=[hankel_rep, repmat(hankel_div(:,n),1,2*n+1)];
end end
counter = counter + nTimes;
end end
%% move data from earcenter
ear_d = [-0.07 0.07];
%% Weights %% Weights
[~,w]= this.dirCoord.spherical_voronoi; % calculate weighting coefficients (Voronoi surfaces <-> measurement points) [~,w]= this.dirCoord.spherical_voronoi; % calculate weighting coefficients (Voronoi surfaces <-> measurement points)
% sG_full = ita_sph_sampling_equiangular(73,144,'theta_type','[]','phi_type','[)');
% % sG_full = ita_sph_sampling_gaussian(71);
% sG = sG_full;
% isOnArc = sG.theta < 2.75;
% sG.cart = sG.cart(isOnArc,:);
% sG.weights = sG.weights(isOnArc);
% % rescale the weights for the sum to be 4*pi again
% w = sG.weights .* 4.*pi ./ sum(sG.weights);
W = sparse(diag(w)); % diagonal matrix containing weights W = sparse(diag(w)); % diagonal matrix containing weights
D = sparse(diag(dweights_rep)); % decomposition order-dependent Tikhonov regularization D = I .* diag(1 + n.*(n+1)); % decomposition order-dependent Tikhonov regularization
Y = ita_sph_base(this.dirCoord,Nmax,'real'); % calculate real-valued SHs using the measurement grid Y = ita_sph_base(this.dirCoord,Nmax,'orthonormal',true); % calculate real-valued SHs using the measurement grid
%% Calculate HRTF data for field points %% Calculate HRTF data for field points
if Nmax > 25 if Nmax > 25
...@@ -117,73 +116,58 @@ end ...@@ -117,73 +116,58 @@ end
hrtf_arbi = zeros(this.nBins,2*field.nPoints); % columns: LRLRLR... hrtf_arbi = zeros(this.nBins,2*field.nPoints); % columns: LRLRLR...
for ear=1:2 for ear=1:2
freqData_temp = this.freqData(:,ear:2:end); % sh transformation
freqData_temp = copyData.freqData(:,ear:2:end);
a0 = (Y.'*W*Y + epsilon*D) \ Y.'*W * freqData_temp.';
% newCoords = this.dirCoord; % %% test the sh transformation results by plotting both spatial and sh data with surf
% newCoords.y = newCoords.y + ear_d(ear); % s = itaSamplingSph(field);
% % s.nmax = Nmax;
% data.channelCoordinates = newCoords; % figure
% data.freqData = freqData_temp; % surf(s,a0(:,10))
% data.freqVector = this.freqVector; % figure
% data.c_meas = 344; % surf(s,freqData_temp(10,:))
% data = process_result_delay_correction(data,this.dirCoord.r(1));
% % calculate weighted SH coefficients using a decomposition order-dependent Tikhonov regularization
%
% freqData_temp = data.freqData;
% Y = ita_sph_base(data.channelCoordinates,Nmax,'real');
a0 = (Y.'*W*Y + epsilon*D) \ Y.'*W * freqData_temp.';
%a0 = (Y.'*W*Y + epsilon*D) \ Y.' *
%this.freqData(:,ear:2:end).'; % fpa version
%a0 = pinv(Y)* this.freqData(:,ear:2:end).'; % jck version
% range extrapolation
if ~isequal(this.dirCoord.r(1),field.r(1)) if ~isequal(this.dirCoord.r(1),field.r(1))
% calculate range-extrapolated HRTFs % calculate range-extrapolated HRTFs
a1 = a0 .* hankel_rep.'; a1 = a0 .* hankel_rep.';
Yest = ita_sph_base(field,N,'real'); % use real-valued SH's %%% test here to see extrapolation results in spatial domain
% surf(s,a1(:,10))
% reconstruction to spatial data
Yest = ita_sph_base(field,N,'orthonormal',true); % use real-valued SH's
hrtf_arbi(:,ear:2:end) = (Yest*a1).'; % interpolated + range-extrapolated HRTFs hrtf_arbi(:,ear:2:end) = (Yest*a1).'; % interpolated + range-extrapolated HRTFs
else else
Yest = ita_sph_base(field,Nmax,'real'); % use real-valued SH's % reconstruction to spatial data
Yest = ita_sph_base(field,Nmax,'orthonormal',true); % use real-valued SH's
hrtf_arbi(:,ear:2:end) = (Yest*a0).'; % interpolated HRTFs hrtf_arbi(:,ear:2:end) = (Yest*a0).'; % interpolated HRTFs
end end
end end
%% move back to head center
% todo
% for ear=1:2
%
% newCoords = field;
% newCoords.y = newCoords.y - ear_d(ear);
%
% freqData = hrtf_arbi(:,ear:2:end);
%
%
% data.channelCoordinates = newCoords;
% data.freqData = freqData;
% data.freqVector = this.freqVector;
% data.c_meas = 344;
% data = process_result_delay_correction(data,this.dirCoord.r(1));
%
% hrtf_arbi(:,ear:2:end) = data.freqData;
%
% end
% set new direction coordinates % set new direction coordinates
sph = zeros(field.nPoints*2 ,3); sph = zeros(field.nPoints*2 ,3);
sph(1:2:end,:) = field.sph; sph(1:2:end,:) = field.sph;
sph(2:2:end,:) = field.sph; sph(2:2:end,:) = field.sph;
% write new HRTF data set % write new HRTF data set
cAudio = itaAudio(hrtf_arbi, 44100, 'freq'); cThis = this;
cAudio.channelCoordinates.sph= sph; cThis.freqData = hrtf_arbi;
cThis.channelCoordinates.sph= sph;
cThis = itaHRTF(cAudio);
cThis.freqData = hrtf_arbi; %% move back to head center
if sArgs.shiftToEar
ear_d_back = -ear_d;
% movedData = zeros(size(hrtf_arbi));
for ear=1:2
movedData = moveFullDataSet(cThis.ch(ear:2:cThis.nChannels),sArgs,ear_d_back(ear));
cThis.freqData(:,ear:2:cThis.nChannels) = movedData;
end
end
if ~isequal(cThis.dirCoord.r(1),field.r(1))%??? if ~isequal(cThis.dirCoord.r(1),field.r(1))%???
cThis.dirCoord.r = field.r; cThis.dirCoord.r = field.r;
...@@ -195,15 +179,44 @@ end ...@@ -195,15 +179,44 @@ end
end end
function result = process_result_delay_correction(result, target_d)
% shift every measurement point to target_d by applying a
% phase shift to the channel: (simplified!)
freq_L = result.freqVector;
add_phase_L = (result.channelCoordinates.r - target_d)* (freq_L./result.c_meas)' .* 2.*pi; function [ data ] = moveFullDataSet(data,options,offsetShift)
fullCoords = data.channelCoordinates;
freqVector = data.freqVector;
shiftedData = zeros(size(data.freqData));
axis = options.shiftAxis;
for index = 1:length(freqVector)
shiftedData(index,:) = moveHRTF(fullCoords,data.freqData(index,:),freqVector(index),axis,offsetShift);
end
data = shiftedData;
end
function [data] = moveHRTF(s, data, frequency, axis, offset)
% the offset is given in m
result.freqData = result.freqData .* exp(1i.*add_phase_L'); origAxis = s.r;
if (size(data,2) > size(data,1))
data = data.';
end
offset = real(offset); % ??
switch axis
case 'x'
s.x = s.x + offset;
case 'y'
s.y = s.y + offset;
case 'z'
s.z = s.z + offset;
end
result.channelCoordinates.r = target_d; newAxis = s.r;
k = 2*pi*frequency/340;
% the phase is moved by the difference of the axis points
data = data .* exp(1i*k*(newAxis - origAxis));
% amplitude manipulation did not yield better results
% data = data .* newAxis ./ origAxis;
end end
function [ cThis ] = reduce_spatial( this, newCoordinates, varargin )
%
% This function is used to reduce the spatial sampling from the current
% directions. This is done with a findnearest search. For a reduction to
% interpolated values use interp
%
% INPUT:
%
%
% OUTPUT:
%
%
%
% Author: Jan-Gerrit Richter <jri@akustik.rwth-aachen.de>
% Version: 2017-11-23
oldCoords = this.getEar('L').channelCoordinates;
% if the desired sampling has more points, its probably unfeasable with
% findnearest search. Abort
if oldCoords.nPoints < newCoordinates.nPoints
error('There are more points in the wanted sampling than are available. You probably want the interp function');
end
% the new coords should have the same radius as the old ones to reduce
% errors
newCoordinates.r = mean(oldCoords.r);
% don't use the mex file to make use of bugfix as poles
% oldCoords = oldCoords.build_search_database;
newIndex = oldCoords.findnearest(newCoordinates);
% calculate all distances from the wanted points to the found points
pointDistances = getVectorLength(newCoordinates,oldCoords.n(newIndex));
% calculate the distance between two neighboring points of the new sampling
newSamplingDistance = getVectorLength(newCoordinates.n(1),newCoordinates.n(2));
% the maximum of the found points should always be smaller
if max(pointDistances) > newSamplingDistance
ita_verbose_info('The found points are further apart than the sampling allows. Something is wrong',0)
end
cThis = this.direction(newIndex);
end
function length = getVectorLength(pointsA, pointsB)
pointsA.r = pointsB.r;
vector = pointsA - pointsB;
length = sqrt(vector.x.^2 + vector.y.^2 + vector.z.^2);
end
\ No newline at end of file
function writeDAFFFile( this, file_path, metadata_user ) function writeDAFFFile( this, file_path, varargin )
% Exports itaHRTF to a DAFF file % Exports itaHRTF to a DAFF file using daffv17_write
%
% Will export the entire angle range of the itaHRTF data set from
% minimum angle to maximum angle in theta and phi by an angular
% resulution of the range divided by the number of available spatial
% points as a equi-angular grid (regular grid, Gaussian sampling).
% %
% Input: file_path (string) [optional] % Input: file_path (string) [optional]
% user metadata (struct created with daff_add_metadata) [optional] % write_daff_args optional arguments (passed to daffv17_write) [optional]
% %
% Required: OpenDAFF matlab scripts, http://www.opendaff.org % Required: OpenDAFF matlab scripts, http://www.opendaff.org
% (but included in ITA-Toolbox) % (but included in ITA-Toolbox)
% %
% Output: none % Output: none
metadata = this.mMetadata;
if nargin >= 3
metadata = metadata_user;
end
hrtf_variable_name = inputname( 1 ); hrtf_variable_name = inputname( 1 );
file_name = [ hrtf_variable_name '_' int2str( this.nSamples ) 'samples_' int2str( this.resAzimuth ) 'x' int2str( this.resElevation ) '.daff']; file_name = [ hrtf_variable_name '_' int2str( this.nSamples ) 'samples_' int2str( this.resAzimuth ) 'x' int2str( this.resElevation ) '.daff'];
if nargin >= 2 if nargin >= 2
...@@ -25,6 +25,37 @@ if nargin == 0 ...@@ -25,6 +25,37 @@ if nargin == 0
end end
%% Prepare daff_write arguments
if strcmp( this.domain, 'time' )
% Content type switcher between time domain (ir) and frequency domain (dft)
% (requires different data functions and content descriptor)
sIn.content = 'ir';
sIn.datafunc = @dfitaHRIRDAFFDataFunc;
sIn.zthreshold = -400; % zero threshold for discarding samples in beginning and end region of IR (where only noise is present)
elseif strcmp( this.domain, 'freq' )
sIn.content = 'dft';
sIn.datafunc = @dfitaHRTFDAFFDataFunc;
end
sIn.metadata = this.mMetadata;
sIn.quantization = 'float32';
sIn.userdata = this;
sIn.orient = [ 0 0 0 ];
sIn.quiet = false;
if ~isempty( varargin )
daff_write_args = ita_parse_arguments( sIn, varargin{ 1 } );
else
daff_write_args = ita_parse_arguments( sIn, varargin );
end
%% Inject content type indicator 'ir' or 'dft' into file name %% Inject content type indicator 'ir' or 'dft' into file name
ct_indicator = 'ir'; ct_indicator = 'ir';
...@@ -33,12 +64,14 @@ if strcmp( this.domain, 'freq' ) ...@@ -33,12 +64,14 @@ if strcmp( this.domain, 'freq' )
end end
[ file_path, file_base_name, file_suffix ] = fileparts( file_name ); [ file_path, file_base_name, file_suffix ] = fileparts( file_name );
if ~strcmp( file_suffix, '.daff' ) if ~strcmp( file_suffix, '.daff' ) && ~isempty( file_suffix )
file_path = fullfile( file_path, strjoin( {file_base_name file_suffix 'v17' ct_indicator 'daff' }, '.' ) ); file_path = fullfile( file_path, strjoin( {file_base_name file_suffix 'v17' ct_indicator 'daff' }, '.' ) );
else else
file_path = fullfile( file_path, strjoin( {file_base_name 'v17' ct_indicator 'daff'}, '.' ) ); file_path = fullfile( file_path, strjoin( {file_base_name 'v17' ct_indicator 'daff'}, '.' ) );
end end
daff_write_args.filename = file_path;
%% Prepare angle ranges and resolution %% Prepare angle ranges and resolution
...@@ -70,49 +103,58 @@ betarange = 180 - [ theta_start_deg theta_end_deg ]; % Flip poles (DAFF starts a ...@@ -70,49 +103,58 @@ betarange = 180 - [ theta_start_deg theta_end_deg ]; % Flip poles (DAFF starts a
assert( betarange( 2 ) >= 0.0 ) assert( betarange( 2 ) >= 0.0 )
assert( betarange( 1 ) <= 180.0 ) assert( betarange( 1 ) <= 180.0 )
daff_write_args.betarange = alpharange;
daff_write_args.alphares = alphares;
daff_write_args.betarange = betarange;
daff_write_args.betares = betares;
%% Assemble metadata %% Assemble metadata (if not already present)
keyname = 'Generation script';
if isempty(daff_write_args.metadata) || ~any( strcmpi( { daff_write_args.metadata(:).name }, keyname ) )
daff_write_args.metadata = daffv17_add_metadata( daff_write_args.metadata, keyname, 'String', 'writeDAFFFile.m' );
end
keyname = 'Generation toolkit';
if ~any( strcmpi( { daff_write_args.metadata(:).name }, keyname ) )
daff_write_args.metadata = daffv17_add_metadata( daff_write_args.metadata, keyname, 'String', 'ITA-Toolkit' );
end
keyname = 'Generation date';
if ~any( strcmpi( { daff_write_args.metadata(:).name }, keyname ) )
daff_write_args.metadata = daffv17_add_metadata( daff_write_args.metadata, keyname, 'String', date );
end
keyname = 'Git Version';
if ~any( strcmpi( { daff_write_args.metadata(:).name }, keyname ) )
versionHash = ita_git_getMasterCommitHash;
daff_write_args.metadata = daffv17_add_metadata( daff_write_args.metadata, keyname, 'String', versionHash );
end
keyname = 'Web resource';
if ~any( strcmpi( { daff_write_args.metadata(:).name }, keyname ) )
daff_write_args.metadata = daffv17_add_metadata( daff_write_args.metadata, keyname, 'String', 'http://www.ita-toolkit.org' );
end
%% Channels
metadata = daffv17_add_metadata( metadata, 'Generation script', 'String', 'writeDAFFFile.m' );
metadata = daffv17_add_metadata( metadata, 'Generation toolkit', 'String', 'ITA-Toolkit' );
metadata = daffv17_add_metadata( metadata, 'Generation date', 'String', date );
metadata = daffv17_add_metadata( metadata, 'Web resource', 'String', 'http://www.ita-toolkit.org' );
channels=this.nChannels/this.nDirections; channels=this.nChannels/this.nDirections;
if(channels<1) if( channels < 1 )
warning('Number of channels per record was not detected correctly, assuming 2 channel records'); warning( 'Number of channels per record was smaller than one, assuming 2 channel records' );
channels = 2; channels = 2;
elseif( mod( channels, 2 ) ~= 0 )
warning( [ 'Number of channels per record was not and integer number, trying floor() of ' num2str( channels ) ] );
channels = floor( channels );
end end
% Content type switcher between time domain (ir) and frequency domain (dft) daff_write_args.channels = channels;
% (requires different data functions)
if strcmp( this.domain, 'time' )
%% Call daff_write and pass argument list
daffv17_write( daff_write_args );
daffv17_write('filename', file_path, ...
'content', 'ir', ...
'datafunc', @dfitaHRIRDAFFDataFunc, ...
'channels', channels, ...
'alphares', alphares, ...
'alpharange', alpharange, ...
'betares', betares, ...
'betarange', betarange, ...
'orient', [ 0 0 0 ], ...
'metadata', metadata, ...
'userdata', this, ...
'quantization', 'float32' );
elseif strcmp( this.domain, 'freq' )
daffv17_write('filename', file_path, ...
'content', 'dft', ...
'datafunc', @dfitaHRTFDAFFDataFunc, ...
'channels', channels, ...