Mesh2hrtf

Mar 25, 2022 at 7:52 PM Post #16 of 93
There is no .sofa file? The .out files contain the impulse response data?
one of the NC1-1.out file has this text

Start time: 25/3/2022 3:22:13

>> G E N E R A L P A R A M E T E R S <<
3D analysis
BE mesh and evaluation mesh have common node!
Number of the common node = 301526

The be.out anf fe.out folders are empty I think theyre only used when using Matlab. There is no .sofa file

the promising file is NC.inp with this data


##-------------------------------------------
## This file was created by export_mesh2hrtf
## Date: 2022-03-24
##-------------------------------------------
Mesh2HRTF 1.0.0
##
Head-Related Transfer Functions
##
## Controlparameter I
0 0 0 0 7 0
##
## Controlparameter II
1 160 0.000001 0.00e+00 1 0 0
##
## Load Frequency Curve
0 161
0.000000 0.000000e+00 0.0
0.000001 0.015000e+04 0.0
0.000002 0.030000e+04 0.0
0.000003 0.045000e+04 0.0
0.000004 0.060000e+04 0.0
0.000005 0.075000e+04 0.0
0.000006 0.090000e+04 0.0
0.000007 0.105000e+04 0.0
0.000008 0.120000e+04 0.0
0.000009 0.135000e+04 0.0
0.000010 0.150000e+04 0.0
0.000011 0.165000e+04 0.0
0.000012 0.180000e+04 0.0
0.000013 0.195000e+04 0.0
0.000014 0.210000e+04 0.0
0.000015 0.225000e+04 0.0
0.000016 0.240000e+04 0.0
0.000017 0.255000e+04 0.0
0.000018 0.270000e+04 0.0
0.000019 0.285000e+04 0.0
0.000020 0.300000e+04 0.0
0.000021 0.315000e+04 0.0
0.000022 0.330000e+04 0.0
0.000023 0.345000e+04 0.0
0.000024 0.360000e+04 0.0
0.000025 0.375000e+04 0.0
0.000026 0.390000e+04 0.0
0.000027 0.405000e+04 0.0
0.000028 0.420000e+04 0.0
0.000029 0.435000e+04 0.0
0.000030 0.450000e+04 0.0
0.000031 0.465000e+04 0.0
0.000032 0.480000e+04 0.0
0.000033 0.495000e+04 0.0
0.000034 0.510000e+04 0.0
0.000035 0.525000e+04 0.0
0.000036 0.540000e+04 0.0
0.000037 0.555000e+04 0.0
0.000038 0.570000e+04 0.0
0.000039 0.585000e+04 0.0
0.000040 0.600000e+04 0.0
0.000041 0.615000e+04 0.0
0.000042 0.630000e+04 0.0
0.000043 0.645000e+04 0.0
0.000044 0.660000e+04 0.0
0.000045 0.675000e+04 0.0
0.000046 0.690000e+04 0.0
0.000047 0.705000e+04 0.0
0.000048 0.720000e+04 0.0
0.000049 0.735000e+04 0.0
0.000050 0.750000e+04 0.0
0.000051 0.765000e+04 0.0
0.000052 0.780000e+04 0.0
0.000053 0.795000e+04 0.0
0.000054 0.810000e+04 0.0
0.000055 0.825000e+04 0.0
0.000056 0.840000e+04 0.0
0.000057 0.855000e+04 0.0
0.000058 0.870000e+04 0.0
0.000059 0.885000e+04 0.0
0.000060 0.900000e+04 0.0
0.000061 0.915000e+04 0.0
0.000062 0.930000e+04 0.0
0.000063 0.945000e+04 0.0
0.000064 0.960000e+04 0.0
0.000065 0.975000e+04 0.0
0.000066 0.990000e+04 0.0
0.000067 1.005000e+04 0.0
0.000068 1.020000e+04 0.0
0.000069 1.035000e+04 0.0
0.000070 1.050000e+04 0.0
0.000071 1.065000e+04 0.0
0.000072 1.080000e+04 0.0
0.000073 1.095000e+04 0.0
0.000074 1.110000e+04 0.0
0.000075 1.125000e+04 0.0
0.000076 1.140000e+04 0.0
0.000077 1.155000e+04 0.0
0.000078 1.170000e+04 0.0
0.000079 1.185000e+04 0.0
0.000080 1.200000e+04 0.0
0.000081 1.215000e+04 0.0
0.000082 1.230000e+04 0.0
0.000083 1.245000e+04 0.0
0.000084 1.260000e+04 0.0
0.000085 1.275000e+04 0.0
0.000086 1.290000e+04 0.0
0.000087 1.305000e+04 0.0
0.000088 1.320000e+04 0.0
0.000089 1.335000e+04 0.0
0.000090 1.350000e+04 0.0
0.000091 1.365000e+04 0.0
0.000092 1.380000e+04 0.0
0.000093 1.395000e+04 0.0
0.000094 1.410000e+04 0.0
0.000095 1.425000e+04 0.0
0.000096 1.440000e+04 0.0
0.000097 1.455000e+04 0.0
0.000098 1.470000e+04 0.0
0.000099 1.485000e+04 0.0
0.000100 1.500000e+04 0.0
0.000101 1.515000e+04 0.0
0.000102 1.530000e+04 0.0
0.000103 1.545000e+04 0.0
0.000104 1.560000e+04 0.0
0.000105 1.575000e+04 0.0
0.000106 1.590000e+04 0.0
0.000107 1.605000e+04 0.0
0.000108 1.620000e+04 0.0
0.000109 1.635000e+04 0.0
0.000110 1.650000e+04 0.0
0.000111 1.665000e+04 0.0
0.000112 1.680000e+04 0.0
0.000113 1.695000e+04 0.0
0.000114 1.710000e+04 0.0
0.000115 1.725000e+04 0.0
0.000116 1.740000e+04 0.0
0.000117 1.755000e+04 0.0
0.000118 1.770000e+04 0.0
0.000119 1.785000e+04 0.0
0.000120 1.800000e+04 0.0
0.000121 1.815000e+04 0.0
0.000122 1.830000e+04 0.0
0.000123 1.845000e+04 0.0
0.000124 1.860000e+04 0.0
0.000125 1.875000e+04 0.0
0.000126 1.890000e+04 0.0
0.000127 1.905000e+04 0.0
0.000128 1.920000e+04 0.0
0.000129 1.935000e+04 0.0
0.000130 1.950000e+04 0.0
0.000131 1.965000e+04 0.0
0.000132 1.980000e+04 0.0
0.000133 1.995000e+04 0.0
0.000134 2.010000e+04 0.0
0.000135 2.025000e+04 0.0
0.000136 2.040000e+04 0.0
0.000137 2.055000e+04 0.0
0.000138 2.070000e+04 0.0
0.000139 2.085000e+04 0.0
0.000140 2.100000e+04 0.0
0.000141 2.115000e+04 0.0
0.000142 2.130000e+04 0.0
0.000143 2.145000e+04 0.0
0.000144 2.160000e+04 0.0
0.000145 2.175000e+04 0.0
0.000146 2.190000e+04 0.0
0.000147 2.205000e+04 0.0
0.000148 2.220000e+04 0.0
0.000149 2.235000e+04 0.0
0.000150 2.250000e+04 0.0
0.000151 2.265000e+04 0.0
0.000152 2.280000e+04 0.0
0.000153 2.295000e+04 0.0
0.000154 2.310000e+04 0.0
0.000155 2.325000e+04 0.0
0.000156 2.340000e+04 0.0
0.000157 2.355000e+04 0.0
0.000158 2.370000e+04 0.0
0.000159 2.385000e+04 0.0
0.000160 2.400000e+04 0.0
##
## 1. Main Parameters I
2 279432 829157 0 0 2 1 4 0
##
## 2. Main Parameters II
0 0 0 0.0000e+00 0 0 0
##
## 3. Main Parameters III
0 0 0 0
##
## 4. Main Parameters IV
346.18 1.1839e+00 1.0 0.0e+00 0.0 e+00 0.0e+00 0.0e+00
##
NODES
../../ObjectMeshes/Reference/Nodes.txt
../../EvaluationGrids/ARI/Nodes.txt
##
ELEMENTS
../../ObjectMeshes/Reference/Elements.txt
../../EvaluationGrids/ARI/Elements.txt
##
# SYMMETRY
# 0 0 0
# 0.0000e+00 0.0000e+00 0.0000e+00
##
BOUNDARY
# Left ear velocity source
ELEM 276334 TO 276334 VELO 0.1 -1 0.0 -1
RETU
##
##
# CURVES
##
POST PROCESS
##
END
 
Last edited:
Mar 25, 2022 at 7:58 PM Post #17 of 93
Send me how user settings code part looks after your path update
import os
import numpy
import sofar as sf # https://pypi.org/project/sofar/ pip install sofar
import subprocess
import shutil

# ---------------------------------------- User Settings: --------------------------------------------

Input1 = r"C:\mesh2hrtf-tools\SOFA_Merge_Folder\left"
Input2 = r"C:\mesh2hrtf-tools\SOFA_Merge_Folder\right"
# Input1 = r"C:\hrtf\SOFA_Merge_Folder" # example (use r"..." raw strings without "\" in the end!)


SavePath = os.path.dirname(os.path.realpath(__file__)) # Automatically detect the working directory
# Result files will be saved in a sub-folder created in "SavePath"
# SavePath = r"C:\hrtf\SOFA_Merge_Folder" # alternatively specify a fixed folder (use r"..." raw strings!)

Convert_to_spherical = True # keep this True, unless you have very specific demand for cartesian coordinates.
# Some software only works with "spherical" coordinates of "SourcePosition" "in SOFA files

Diagnostic_plot = True # shows and saves to disk the plot of the merged HRIR.sofa file: great for visual quality check.
# requires: --- pyfar --- and --- matplotlib ---

# ---------------------------------------- End of User Settings -----------------------------------------

#
# "join_sofa_files.py" merges SOFA data from two separate Mesh2HRTF simulations for LEFT & RIGHT ear.
# it uses a lot of example code from Mesh2HRTF "Output2HRTF_Main.py" & "NumCalcManager.py"
#
# HOW TO USE:
# Main tutorial is here: https://sourceforge.net/p/mesh2hrtf/wiki/Basic_HRTF_post_processing
#
# mode A0 - no inputs = (recommended usage mode) same as A1, but actually executes "Output2HRTF.py" as well to run full
# pre-processing in one go and applies fixes to the possible "Non-convergence issue".
#
# mode A1 - no inputs = scan start folder for 2 projects to merge.
# 1- run this .py file from a dedicated folder (for example use folder "SOFA_Merge_Folder")
# 2- Move into the "SOFA_Merge_Folder" exactly the 2 project folders that need to be merged.
# 3- "open/run" this "join_sofa_files.py" file with Python. (Python3 with "sofar" & "numpy" must be installed)
# (an easy Drag&Drop way to run this script is from Spyder which can be installed by Anaconda)
# Without input arguments this script automatically merges the SOFA files of the 2 projects it finds.
# 4- Merged SOFA files will be saved in a folder next to the project that was found. DONE.
#
# mode A2 - Input1 only = scan Input1 folder for 2 projects to merge.
# 1- Move into any folder exactly the 2 project folders that need to be merged.
# 2- "run" this "join_sofa_files.py" file and specify "Input1" path to the folder that contains projects to merge.
# This script searches and merges the SOFA files from the 2 projects it finds inside "input1" path.
# 3- Merged SOFA files will be saved in a folder next to the project that was found. DONE.
#
# NOT_IMPLEMENTED: mode B - 1 input = Use the given path to identify a pair of projects to merge
# where folder-name for LEFT ends on '_L' and RIGHT ends on '_R'.
#
# mode C - 2 inputs = Merge the 2 projects that were given as input.
#
# made for Python 3.7+ see license details at the end of the script.
# v0.50 2022-01-23 First working version. Tested on Windows 10. (by Sergejs Dombrovskis)
# v0.90 2022-01-24 Mode-A1 is ready + most descriptions in place. (by S.D.)
# v0.93 2022-02-06 added Mode-A2 and Mode-C. (by S.D.)
# v1.00 2022-03-15 added Mode-A0 with automatic execution of "Output2HRTF.py" + small improvements +
# added multi-SamplingRate HRIR outputs + extra sanity checks (by S.D.)
# v1.10 2022-03-17 added diagnostic plotting of the merged HRIR.sofa file (by S.D.)
# v1.20 2022-03-20 added effective workaround to produce usable data even if simulation encountered the
# "Non-Convergence issue" - only in A0 mode (by S.D.)
#


def merge_write_sofa(sofa_left, sofa_right, basepath, filename, sofa_type='HRTF'):
"""Write complex pressure or impulse responses as SOFA file."""
# adapted from "write_to_sofa()" in "Output2HRTF_Main.py"

# create empty SOFA object

if sofa_type == 'HRTF':
convention = "SimpleFreeFieldHRTF" # if numSources == 2 else "GeneralTF"
elif sofa_type == 'HRIR':
convention = "SimpleFreeFieldHRIR" # if numSources == 2 else "GeneralFIR"
else:
convention = 'Error - Unrecognized sofa_type provided.'

sofa = sf.Sofa(convention)

# write meta data
sofa.GLOBAL_ApplicationName = sofa_left.GLOBAL_ApplicationName # 'Mesh2HRTF'
sofa.GLOBAL_ApplicationVersion = sofa_left.GLOBAL_ApplicationVersion
sofa.GLOBAL_History = sofa_left.GLOBAL_History # "numerically simulated data"

if len(sofa_left.GLOBAL_Title) == 0: # title added to improve compatibility with Matlab SOFA API 1.1.3
sofa.GLOBAL_Title = 'untitled ' + sofa_type + ' SOFA data'
else:
sofa.GLOBAL_Title = sofa_left.GLOBAL_Title

# Source and receiver data
if Convert_to_spherical and \
sofa_left.SourcePosition_Type == 'cartesian' and sofa_left.SourcePosition_Units == 'meter':

sofa.SourcePosition = cart2sph_in_deg(sofa_left.SourcePosition) # see sub-function
sofa.SourcePosition_Units = 'degree, degree, metre' # alternatively 'degrees'
sofa.SourcePosition_Type = 'spherical'
else:
sofa.SourcePosition = sofa_left.SourcePosition
sofa.SourcePosition_Units = sofa_left.SourcePosition_Units # "meter"
sofa.SourcePosition_Type = sofa_left.SourcePosition_Type # "cartesian"

sofa.ReceiverPosition = [sofa_left.ReceiverPosition.reshape(3, 1),
sofa_right.ReceiverPosition.reshape(3, 1)]
sofa.ReceiverPosition_Units = sofa_left.ReceiverPosition_Units # "meter"
sofa.ReceiverPosition_Type = sofa_left.ReceiverPosition_Type # "cartesian"

if sofa_type == 'HRTF':
tmp_data_real = numpy.zeros((sofa_left.Data_Real.shape[0], 2,
sofa_left.Data_Real.shape[2])) # init
tmp_data_imag = tmp_data_real # init
for jj in range(sofa_left.Data_Real.shape[2]): # merge arrays
tmp_data_real[:, 0, jj] = sofa_left.Data_Real[:, 0, jj]
tmp_data_real[:, 1, jj] = sofa_right.Data_Real[:, 0, jj]
tmp_data_imag[:, 0, jj] = sofa_left.Data_Imag[:, 0, jj]
tmp_data_imag[:, 1, jj] = sofa_right.Data_Imag[:, 0, jj]

sofa.Data_Real = tmp_data_real
sofa.Data_Imag = tmp_data_imag
sofa.N = sofa_left.N # frequencies

# init defaults for HRTF:
output_file_sampling_rates = [int(2 * sofa.N[-1])]

elif sofa_type == 'HRIR':
tmp_data_ir = numpy.zeros((sofa_left.Data_IR.shape[0], 2, sofa_left.Data_IR.shape[2])) # init
for jj in range(sofa_left.Data_IR.shape[2]): # merge arrays
tmp_data_ir[:, 0, jj] = sofa_left.Data_IR[:, 0, jj]
tmp_data_ir[:, 1, jj] = sofa_right.Data_IR[:, 0, jj]

sofa.Data_IR = tmp_data_ir
sofa.Data_SamplingRate = sofa_left.Data_SamplingRate # fs
sofa.Data_Delay = numpy.ones((1, 2)) * sofa_left.Data_Delay

# init defaults for HRTF:
output_file_sampling_rates = [int(sofa.Data_SamplingRate)]
output_data_length = [sofa.Data_IR.shape[2]]

# extra "multi-sample rate HRIR" output processing
if numpy.round(sofa.Data_SamplingRate / sofa.Data_IR.shape[2]) == 150:
frequency_step = 150 # detected a valid multi-sample rate frequency step
elif numpy.round(sofa.Data_SamplingRate / sofa.Data_IR.shape[2]) == 75:
frequency_step = 75 # detected a valid multi-sample rate frequency step
else:
print('\n --- Note: the Frequency Step of this dataset is not equal to 150 or 75 Hz, therefore ')
print(' "multi-SamplingRate" HRIR saving is not possible. The only available SamplingRate is ' +
str(sofa.Data_SamplingRate) + ' ' + sofa.Data_SamplingRate_Units + '\n')
frequency_step = 0

if frequency_step > 0.5: # "multi-sample rate HRIR" output is possible
common_sampling_rates = [192000, 96000, 88200, 48000, 44100] # edit this DESCENDING list if you have to
for rate in common_sampling_rates:
if sofa.Data_IR.shape[2] > rate / frequency_step:
# add this sampling rate to output:
output_file_sampling_rates.append(rate)
output_data_length.append(int(rate / frequency_step))

else:
raise Exception("impossible :)")

# save the merged files
for f_nr in range(len(output_file_sampling_rates)):

if f_nr > 0: # extra "multi-SamplingRate HRIR" output processing
# trim data down to correct sampling rate:
# noinspection PyUnboundLocalVariable
sofa.Data_IR = sofa.Data_IR[:, :, :output_data_length[f_nr]]
sofa.Data_SamplingRate = float(output_file_sampling_rates[f_nr])

# https://sofa_right.readthedocs.io/en/latest/sofa_right.html#sofa_right-documentation
sofa.verify()

# Save as SOFA file
path = os.path.join(basepath, filename[:-5] + '_' + str(output_file_sampling_rates[f_nr]) + '.sofa')

# Need to delete old FILE first if file already exists
if os.path.exists(path):
os.remove(path)

# Save
sf.write_sofa(path, sofa, version='latest', compression=9) # max compression

if f_nr == 0:
full_reference_path = path

# noinspection PyUnboundLocalVariable
return full_reference_path


# cart2sph conversion with DEGREE output
def cart2sph_in_deg(xyz):
theta_phi_r = numpy.empty(xyz.shape)

# based on pyfar example https://github.com/pyfar/pyfar/blob/main/pyfar/classes/coordinates.py
theta_phi_r[:, 2] = numpy.sqrt(xyz[:, 0] ** 2 + xyz[:, 1] ** 2 + xyz[:, 2] ** 2) # radius
z_div_r = numpy.where(theta_phi_r[:, 2] != 0, xyz[:, 2] / theta_phi_r[:, 2], 0)
theta_phi_r[:, 1] = numpy.rad2deg(numpy.arccos(z_div_r)) # colatitude
theta_phi_r[:, 0] = numpy.rad2deg(numpy.mod(numpy.arctan2(xyz[:, 1], xyz[:, 0]), 2 * numpy.pi)) # azimuth

# added fixes, including rounding to 2-3 decimal places:
theta_phi_r[:, 2] = numpy.around(theta_phi_r[:, 2], 3) # this one round to 3 decimal places (1mm precision)
theta_phi_r[:, 1] = numpy.around(- (theta_phi_r[:, 1] - 90), 2) # to match what "SOFAplotHRTF.m" does
theta_phi_r[:, 0] = numpy.around(theta_phi_r[:, 0], 3)
# theta_phi_r[:, 0] = numpy.around(numpy.where(theta_phi_r[:, 0] > 180,
# theta_phi_r[:, 0] - 360, theta_phi_r[:, 0]), 2)

return theta_phi_r


def scan_for_errors(project_path): # find any broken data in this project

# NOTE: made only to work with 1 source!!!
num_sources = 1

lowest_corrupt_frq = 0 # init

for source in range(num_sources):

full_p = os.path.join(project_path, 'NumCalc', 'source_' + str(source + 1))

# list all NumCalc logs
all_nc_list = [f for f in os.listdir(full_p) if
f.endswith('.out') and f.startswith('NC') and os.path.isfile(os.path.join(full_p, f))]

for file in all_nc_list:

# read the file
with open(os.path.join(full_p, file)) as ff:
lines = ff.readlines()
ff.close()

frequency_key = ', Frequency = '
non_convergence_key = 'Warning: Maximum number of iterations is reached!'
frequencies = [] # init
non_convergence_freq = [] # init

# find line containing the number of frequency steps
for line_nr in range(len(lines)):
if lines[line_nr].__contains__(frequency_key):
where_is_it = lines[line_nr].find(frequency_key)
# note down what is the frequency of this log and on which line
# (because more than 1 frequency may be in the same worker/log-file)
frequencies.append(int(lines[line_nr].strip()[where_is_it + len(frequency_key):-3]))

elif lines[line_nr].startswith(non_convergence_key):
if frequencies[-1] > 24000: # affects very high frequencies only
print('Warning - Non-Convergence detected at over 24kHz (at ' + str(frequencies[-1]) + 'Hz)')
else:
print('PROBLEM!! - Non-Convergence issue in important range: @ ' + str(frequencies[-1]) + 'Hz')

non_convergence_freq.append(frequencies[-1]) # note down which frequency failed

if len(non_convergence_freq) > 0: # we have non-convergence
if lowest_corrupt_frq == 0:
lowest_corrupt_frq = min(non_convergence_freq)

elif lowest_corrupt_frq > min(non_convergence_freq):
lowest_corrupt_frq = min(non_convergence_freq) # keep only the lowest problematic frequency

return lowest_corrupt_frq


def write_new_numFrequencies(path1, path2, corrupt_frq1, corrupt_frq2):
if min(corrupt_frq1, corrupt_frq2) == 0: # ignore the Zero init value
lowest_corrupt_frq = max(corrupt_frq1, corrupt_frq2)
else:
lowest_corrupt_frq = min(corrupt_frq1, corrupt_frq2)

for pro in [1, 2]: # execute for BOTH PROJECTS
if pro == 1:
project_path = path1
else:
project_path = path2
info_path = os.path.join(project_path, 'Info.txt')

# 1 Back up the Info.txt
info_bckp_path = os.path.join(project_path, 'Info_BACKUP_original.txt')
if not os.path.isfile(info_bckp_path): # backup does not yet exist
shutil.copy2(info_path, info_bckp_path)

# 2 read the original file
with open(info_path) as ff:
lines = ff.readlines()
ff.close()

# 3 Detect important information:
for line_nr in range(len(lines)):
if lines[line_nr].startswith('Minimum evaluated Frequency: '):
min_frequency = float(lines[line_nr].strip()[len('Minimum evaluated Frequency: '):])

elif lines[line_nr].startswith('Highest evaluated Frequency: '):
line_of_max_frequency = line_nr
orig_max_frequency = float(lines[line_nr].strip()[len('Highest evaluated Frequency: '):])

elif lines[line_nr].startswith('Frequency Stepsize: '):
frequency_step = float(lines[line_nr].strip()[len('Frequency Stepsize: '):])

elif lines[line_nr].startswith('Frequency Steps: '):
line_of_frequency_steps_nr = line_nr
orig_frequency_steps_nr = int(lines[line_nr].strip()[len('Frequency Steps: '):])

# 4 MODIFY the number of simulated frequency steps in the Info.txt
new_max_frequency = lowest_corrupt_frq - frequency_step
new_frequency_steps_nr = 1 + round((new_max_frequency - min_frequency) / frequency_step)

lines[line_of_frequency_steps_nr] = ('Frequency Steps: ' + str(int(new_frequency_steps_nr)) + '\n')
lines[line_of_max_frequency] = ('Highest evaluated Frequency: ' + str(new_max_frequency) + '\n')

# 5 add extra modification info for the Info.txt
lines.append('\n\n\n--------------------------------------------------------------------------------\n\n')
lines.append('NOTE: this file was MODIFIED by "join_sofa_files.py" to reduce maximum Frequency\n')
lines.append(' because simulation issue(s) were detected at higher frequencies.\n')
lines.append('\n Initial "Highest evaluated Frequency" was ' + str(orig_max_frequency) + '\n')
# noinspection PyUnboundLocalVariable
lines.append(' Initial "Frequency Steps" was ' + str(orig_frequency_steps_nr) + '\n')

# 6 OVER-WRITE the new Info.txt file
f3 = open(info_path, "w")
for li in lines:
f3.write(li)
f3.close()


def plot_hrir_pair(sofa_data_path, noise_floor=-70):
"""
Plots both left and right ear HRIRs in an easy to diagnose format

# based on tutorial: https://sofar.readthedocs.io/en/latest/working_with_sofa_files.html#working-with-sofa

Params:
sofa_data_path : full path to the sofa HRIR file to load and plot

noise_floor : were to limit colormap to provide easy to read image
= -50 # noise floor value used in "SOFAplotHRTF.m" from SOFA Matlab API
= -70 # value that visually matches the look of "SOFAplotHRTF.m" from SOFA MatlabAPI

"""

# read the SOFA file using pyfar
data_ir, source_coordinates, receiver_coordinates = pf.io.read_sofa(sofa_data_path)

# find all source positions on or in the vicinity of the HORIZONTAL plane using the find_slice method
_, mask = source_coordinates.find_slice('elevation', unit='deg', value=0, show=False) # do not Show the plot
angles = source_coordinates.get_sph('top_elev', 'deg')[mask, 0]

# noinspection PyTypeChecker
axes_subplot = plt.subplots(2, 1, figsize=(8, 6), sharex=True, label=sofa_data_path)
axes_subplot[0].set_size_inches(8, 10, forward=True)

for Ear in [0, 1]: # Ear 0 == LEFT and 1 == RIGHT ear

# main plot function
ax_h, qm_h, cb_h = pf.plot.time_2d(data_ir[mask, Ear], indices=angles, dB=True, log_prefix=20,
log_reference=1, unit=None, ax=axes_subplot[1][Ear],
cmap=mpl.cm.get_cmap(name='hot_r'),
orientation='horizontal') # YlOrBr hot hot_r RdBu

natural_limits = qm_h.get_clim() # get the full range of data
qm_h.set_clim([natural_limits[1] + noise_floor, natural_limits[1]]) # set limits according to noise floor
ax_h[0].set_ylabel("Azimuth angle in degrees")
# ax_h.set_ylim(0, 3)
if Ear == 0:
ax_h[0].set_title("Left ear HRIR (Horizontal plane)")
elif Ear == 1:
ax_h[0].set_title("Right ear HRIR (Horizontal plane)")

axes_subplot[0].savefig((sofa_data_path[:-5] + '.png'), dpi=100)


# ToDo - accept different input scenarios:
# A (DONE) - no inputs = scan start folder for 2 projects to merge.
# B - 1 input = Use the given path to identify a pair of projects to merge
# where folder-name for LEFT ends on '_L' and RIGHT ends on '_R'.
# C - 2 inputs = Merge the 2 projects that were given as input.

# # Identify starting conditions
print('join_sofa_files started with SavePath = "' + SavePath + '"')
if len(Input1) > 0 and (os.path.basename(Input1)[-2:] == "_L" or os.path.basename(Input1)[-2:] == "_R"):
ProjectName_withL = True # we can search for a mathing pairs of "_L" and "_R" projects
else:
ProjectName_withL = False # unless "Input2" is specified, search in this folder for 2 merge-able projects

if len(Input1) == 0 or (not ProjectName_withL and len(Input2) == 0):
# mode A = scan folder for 2 projects to merge.
counter = 0 # init
if len(Input1) == 0:
Search_Folder = SavePath # no inputs - search folder where the script is (or any custom SavePath)
else:
Search_Folder = Input1 # search in the first Input1 path

Path1 = '' # init
Path2 = '' # init
for subdir in os.listdir(Search_Folder):
# we only search folders that do not start with "." (extra scripts or files are allowed)
if os.path.isdir(os.path.join(Search_Folder, subdir)) and not subdir.startswith('.'):
counter += 1
if counter == 1:
Path1 = os.path.join(Search_Folder, subdir) # project 1
elif counter == 2:
Path2 = os.path.join(Search_Folder, subdir) # project 2
else:
raise Exception('more than 2 folders to merge in the Search_Folder = "' + Search_Folder + '"')

if len(Path2) == 0:
raise Exception('less than 2 folders to merge in the Search_Folder = "' + Search_Folder + '"')

elif len(Input2) == 0: # mode B - 1 input = Use the given path to identify a pair of projects to merge
# where folder-name for LEFT ends on '_L' and RIGHT ends on '_R'.
raise Exception("not implemented")

else: # mode C - 2 inputs = Merge the 2 projects that were given as input.
Path1 = Input1
Path2 = Input2

# DONE - autodetect details of the input scenario:
# a - folder with Mesh2HRTF project
# b - folder with .sofa files
# c - complete file path to "HRTF_<something>.sofa" of "HRIR_<something>.sofa"
if os.path.isdir(Path1):
# check if on Path1 is a Mesh2HRTF project folder that is not yet post-processed:
Lowest_corrupt_frq_1 = 0 # init
Lowest_corrupt_frq_2 = 0 # init
if not os.path.isdir(os.path.join(Path1, 'Output2HRTF')) and os.path.isfile(os.path.join(Path1, 'Output2HRTF.py')):
# Mode-A0 detected:

print('\nChecking for errors in "' + Path1 + '"...\n')
Lowest_corrupt_frq_1 = scan_for_errors(Path1) # find any broken data on Path1

print('\nChecking for errors in "' + Path2 + '"...\n')
Lowest_corrupt_frq_2 = scan_for_errors(Path2) # find any broken data on Path1

if Lowest_corrupt_frq_1 > 1 or Lowest_corrupt_frq_2 > 1: # there was coppupt data in the simulation
# exclude all frequencies that are corrupt by specifying the last Corrupt one.
write_new_numFrequencies(Path1, Path2, Lowest_corrupt_frq_1, Lowest_corrupt_frq_2)

print('\nRunning Mesh2HRTF post-processing in "' + Path1 + '"...\n')
os.chdir(Path1) # necessary for the 'Output2HRTF.py'
subprocess.call("Output2HRTF.py", shell=True) # run the normal Mesh2HRTF post-processing
# noinspection PyUnboundLocalVariable
os.chdir(Search_Folder) # change back to Search_Folder (just in case)

# check if on Path2 is a Mesh2HRTF project folder that is not yet post-processed:
if not os.path.isdir(os.path.join(Path2, 'Output2HRTF')) and os.path.isfile(os.path.join(Path2, 'Output2HRTF.py')):
print('\nRunning Mesh2HRTF post-processing in "' + Path2 + '"...\n')
os.chdir(Path2) # necessary for the 'Output2HRTF.py'
subprocess.call("Output2HRTF.py", shell=True) # run the normal Mesh2HRTF post-processing
os.chdir(Search_Folder) # change back to Search_Folder (just in case)

if os.path.isdir(os.path.join(Path1, 'Output2HRTF')): # a - folder with Mesh2HRTF project
SofaFLDR_1 = os.path.join(Path1, 'Output2HRTF')
SofaFLDR_2 = os.path.join(Path2, 'Output2HRTF')
else: # b - folder with .sofa files
SofaFLDR_1 = Path1
SofaFLDR_2 = Path2

AllSofaFiles1 = os.listdir(SofaFLDR_1) # searching for '*.sofa' files
AllSofaFiles2 = os.listdir(SofaFLDR_2)
FolderName = os.path.basename(Path1)

else: # os.path.isfile(Path1): # c - complete file path to "HRTF_<something>.sofa" of "HRIR_<something>.sofa"
AllSofaFiles1 = [os.path.basename(Path1)]
AllSofaFiles2 = [os.path.basename(Path2)]
SofaFLDR_1 = os.path.dirname(Path1)
SofaFLDR_2 = os.path.dirname(Path2)
FolderName = os.path.basename(SofaFLDR_1)

if FolderName.endswith('_L') or FolderName.endswith('_R'):
FolderName = FolderName[:-2] # drop the "_L" or "_R" from the folder name (tidy up)
BasePath = os.path.join(SavePath, 'SOFA_' + FolderName + '_merged')

# autodetect which is Left and which is Right based on source coord.
HRTFsofa_read_1 = sf.read_sofa(os.path.join(SofaFLDR_1, AllSofaFiles1[0]))
if HRTFsofa_read_1.ReceiverPosition.shape[0] > 1: # more than one receiver in this SOFA file
raise Exception('The SOFA file is not for a single ear! File in: "' + SofaFLDR_1 + '"')

if HRTFsofa_read_1.ReceiverPosition[0, 1] > 0: # sofa file is for Left ear (positive Y coordinate)
AllSofaFiles_L = AllSofaFiles1
AllSofaFiles_R = AllSofaFiles2
SofaFLDR_L = SofaFLDR_1
SofaFLDR_R = SofaFLDR_2
else: # flip inputs because the 2nd folder contains Left ear
AllSofaFiles_R = AllSofaFiles1
AllSofaFiles_L = AllSofaFiles2
SofaFLDR_R = SofaFLDR_1
SofaFLDR_L = SofaFLDR_2

# Save complex pressure as SOFA file
print('\n Merging HRTF SOFA files from: "' + os.path.basename(Path1) + '" and "' +
os.path.basename(Path2) + '"')

del SofaFLDR_2, SofaFLDR_1, AllSofaFiles2, AllSofaFiles1, Path1, Path2

for ii in range(2): # range(len(AllSofaFiles_L))

# # Load the data
sofa_read_L = sf.read_sofa(os.path.join(SofaFLDR_L, AllSofaFiles_L[ii]))
sofa_read_R = sf.read_sofa(os.path.join(SofaFLDR_R, AllSofaFiles_R[ii]))

# detect data type:
if sofa_read_L.GLOBAL_DataType == 'FIR':
SOFA_Type = 'HRIR'

# sanity checks that the files can be merged
if sofa_read_L.Data_SamplingRate != sofa_read_R.Data_SamplingRate:
raise Exception("The SamplingRates of the two input SOFA files do not match!")
elif sofa_read_L.Data_IR.shape[1] != 1 or sofa_read_R.Data_IR.shape[1] != 1:
raise Exception("The input SOFA files contain more than one ear - impossible to merge")
elif sofa_read_L.Data_IR.shape[2] != sofa_read_R.Data_IR.shape[2]:
raise Exception("The number of steps in the two input SOFA files do not match!")
elif sofa_read_L.ReceiverPosition_Units != sofa_read_R.ReceiverPosition_Units:
raise Exception("The ReceiverPosition_Units in the two input SOFA files do not match!")

else:
SOFA_Type = 'HRTF'

# sanity checks that the files can be merged
if sofa_read_L.N[0] != sofa_read_R.N[0] or sofa_read_L.N[-1] != sofa_read_R.N[-1]:
raise Exception("The sampling Frequencies of the two input SOFA files do not match!")

if ii == 0 and not os.path.isdir(BasePath):
os.mkdir(BasePath) # create new output folder

# noinspection PyTypeChecker
Full_Ref_Path = merge_write_sofa(sofa_read_L, sofa_read_R, BasePath, AllSofaFiles_L[ii], SOFA_Type)

if SOFA_Type == 'HRIR': # note down what should be plotted in the end
SOFA_to_plot = Full_Ref_Path

# make a simple copy of the Info.txt
if os.path.isfile(os.path.join(os.path.dirname(SofaFLDR_L), 'Info.txt')):
shutil.copy2(os.path.join(os.path.dirname(SofaFLDR_L), 'Info.txt'), os.path.join(BasePath, 'Info.txt'))

print('\n Done: Merged SOFA files are saved in:\n "' + BasePath + '"\n')

if Diagnostic_plot:

# noinspection PyBroadException
try:
#############################################################################
# plotting part: it is OK if plotting crashes, because the merging is already complete.

# these imports are here so that key functionality works even without "pyfar" and "matplotlib"
import pyfar as pf # https://pypi.org/project/sofar/ pip install pyfar
import matplotlib as mpl
import matplotlib.pyplot as plt

# noinspection PyUnboundLocalVariable
plot_hrir_pair(SOFA_to_plot, -50)

#############################################################################
except Exception:
print('\n WARNING: plotting command crashed. Likely reasons are:')
print(' 1 - missing "pyfar" or missing "matplotlib"')
print(' (to install "pyfar" in command line type "pip install pyfar" ')
print(' 2 - obsolete python packages (especially numpy) - try running: "pip list --outdated"')
print(' (to update a package use for example: "pip install --upgrade numpy"')
print(' 3 - actual bug in this script (debug, or open an issue if you need this plot)\n')

input('Hit Enter to close - Plotting is complete :)')

else:
input('Hit Enter to exit :)')

#
# made for the Mesh2HRTF codebase
#
# mesh2hrtf.sourceforge.net
#
# Mesh2HRTF is licensed under the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version. Mesh2HRTF is distributed in the hope
# that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. You should have received a
# copy of the GNU LesserGeneral Public License along with Mesh2HRTF. If not,
# see <http://www.gnu.org/licenses/lgpl.html>.
#
# If you use Mesh2HRTF:
# - Provide credits:
# "Mesh2HRTF, H. Ziegelwanger, ARI, OEAW (mesh2hrtf.sourceforge.net)"
# - In your publication, cite both articles:
# [1] Ziegelwanger, H., Kreuzer, W., and Majdak, P. (2015). "Mesh2HRTF:
# Open-source software package for the numerical calculation of head-
# related transfer functions," in Proceedings of the 22nd ICSV,
# Florence, IT.
# [2] Ziegelwanger, H., Majdak, P., and Kreuzer, W. (2015). "Numerical
# calculation of listener-specific head-related transfer functions and
# sound localization: Microphone model and mesh discretization," The
# Journal of the Acoustical Society of America, 138, 208-222.
 
Mar 26, 2022 at 4:21 AM Post #18 of 93
import os
import numpy
import sofar as sf # https://pypi.org/project/sofar/ pip install sofar
import subprocess
import shutil

# ---------------------------------------- User Settings: --------------------------------------------

Input1 = r"C:\mesh2hrtf-tools\SOFA_Merge_Folder\left"
Input2 = r"C:\mesh2hrtf-tools\SOFA_Merge_Folder\right"
# Input1 = r"C:\hrtf\SOFA_Merge_Folder" # example (use r"..." raw strings without "\" in the end!)


SavePath = os.path.dirname(os.path.realpath(__file__)) # Automatically detect the working directory
# Result files will be saved in a sub-folder created in "SavePath"
# SavePath = r"C:\hrtf\SOFA_Merge_Folder" # alternatively specify a fixed folder (use r"..." raw strings!)

Convert_to_spherical = True # keep this True, unless you have very specific demand for cartesian coordinates.
# Some software only works with "spherical" coordinates of "SourcePosition" "in SOFA files

Diagnostic_plot = True # shows and saves to disk the plot of the merged HRIR.sofa file: great for visual quality check.
# requires: --- pyfar --- and --- matplotlib ---

# ---------------------------------------- End of User Settings -----------------------------------------

#
# "join_sofa_files.py" merges SOFA data from two separate Mesh2HRTF simulations for LEFT & RIGHT ear.
# it uses a lot of example code from Mesh2HRTF "Output2HRTF_Main.py" & "NumCalcManager.py"
#
# HOW TO USE:
# Main tutorial is here: https://sourceforge.net/p/mesh2hrtf/wiki/Basic_HRTF_post_processing
#
# mode A0 - no inputs = (recommended usage mode) same as A1, but actually executes "Output2HRTF.py" as well to run full
# pre-processing in one go and applies fixes to the possible "Non-convergence issue".
#
# mode A1 - no inputs = scan start folder for 2 projects to merge.
# 1- run this .py file from a dedicated folder (for example use folder "SOFA_Merge_Folder")
# 2- Move into the "SOFA_Merge_Folder" exactly the 2 project folders that need to be merged.
# 3- "open/run" this "join_sofa_files.py" file with Python. (Python3 with "sofar" & "numpy" must be installed)
# (an easy Drag&Drop way to run this script is from Spyder which can be installed by Anaconda)
# Without input arguments this script automatically merges the SOFA files of the 2 projects it finds.
# 4- Merged SOFA files will be saved in a folder next to the project that was found. DONE.
#
# mode A2 - Input1 only = scan Input1 folder for 2 projects to merge.
# 1- Move into any folder exactly the 2 project folders that need to be merged.
# 2- "run" this "join_sofa_files.py" file and specify "Input1" path to the folder that contains projects to merge.
# This script searches and merges the SOFA files from the 2 projects it finds inside "input1" path.
# 3- Merged SOFA files will be saved in a folder next to the project that was found. DONE.
#
# NOT_IMPLEMENTED: mode B - 1 input = Use the given path to identify a pair of projects to merge
# where folder-name for LEFT ends on '_L' and RIGHT ends on '_R'.
#
# mode C - 2 inputs = Merge the 2 projects that were given as input.
#
# made for Python 3.7+ see license details at the end of the script.
# v0.50 2022-01-23 First working version. Tested on Windows 10. (by Sergejs Dombrovskis)
# v0.90 2022-01-24 Mode-A1 is ready + most descriptions in place. (by S.D.)
# v0.93 2022-02-06 added Mode-A2 and Mode-C. (by S.D.)
# v1.00 2022-03-15 added Mode-A0 with automatic execution of "Output2HRTF.py" + small improvements +
# added multi-SamplingRate HRIR outputs + extra sanity checks (by S.D.)
# v1.10 2022-03-17 added diagnostic plotting of the merged HRIR.sofa file (by S.D.)
# v1.20 2022-03-20 added effective workaround to produce usable data even if simulation encountered the
# "Non-Convergence issue" - only in A0 mode (by S.D.)
#


def merge_write_sofa(sofa_left, sofa_right, basepath, filename, sofa_type='HRTF'):
"""Write complex pressure or impulse responses as SOFA file."""
# adapted from "write_to_sofa()" in "Output2HRTF_Main.py"

# create empty SOFA object

if sofa_type == 'HRTF':
convention = "SimpleFreeFieldHRTF" # if numSources == 2 else "GeneralTF"
elif sofa_type == 'HRIR':
convention = "SimpleFreeFieldHRIR" # if numSources == 2 else "GeneralFIR"
else:
convention = 'Error - Unrecognized sofa_type provided.'

sofa = sf.Sofa(convention)

# write meta data
sofa.GLOBAL_ApplicationName = sofa_left.GLOBAL_ApplicationName # 'Mesh2HRTF'
sofa.GLOBAL_ApplicationVersion = sofa_left.GLOBAL_ApplicationVersion
sofa.GLOBAL_History = sofa_left.GLOBAL_History # "numerically simulated data"

if len(sofa_left.GLOBAL_Title) == 0: # title added to improve compatibility with Matlab SOFA API 1.1.3
sofa.GLOBAL_Title = 'untitled ' + sofa_type + ' SOFA data'
else:
sofa.GLOBAL_Title = sofa_left.GLOBAL_Title

# Source and receiver data
if Convert_to_spherical and \
sofa_left.SourcePosition_Type == 'cartesian' and sofa_left.SourcePosition_Units == 'meter':

sofa.SourcePosition = cart2sph_in_deg(sofa_left.SourcePosition) # see sub-function
sofa.SourcePosition_Units = 'degree, degree, metre' # alternatively 'degrees'
sofa.SourcePosition_Type = 'spherical'
else:
sofa.SourcePosition = sofa_left.SourcePosition
sofa.SourcePosition_Units = sofa_left.SourcePosition_Units # "meter"
sofa.SourcePosition_Type = sofa_left.SourcePosition_Type # "cartesian"

sofa.ReceiverPosition = [sofa_left.ReceiverPosition.reshape(3, 1),
sofa_right.ReceiverPosition.reshape(3, 1)]
sofa.ReceiverPosition_Units = sofa_left.ReceiverPosition_Units # "meter"
sofa.ReceiverPosition_Type = sofa_left.ReceiverPosition_Type # "cartesian"

if sofa_type == 'HRTF':
tmp_data_real = numpy.zeros((sofa_left.Data_Real.shape[0], 2,
sofa_left.Data_Real.shape[2])) # init
tmp_data_imag = tmp_data_real # init
for jj in range(sofa_left.Data_Real.shape[2]): # merge arrays
tmp_data_real[:, 0, jj] = sofa_left.Data_Real[:, 0, jj]
tmp_data_real[:, 1, jj] = sofa_right.Data_Real[:, 0, jj]
tmp_data_imag[:, 0, jj] = sofa_left.Data_Imag[:, 0, jj]
tmp_data_imag[:, 1, jj] = sofa_right.Data_Imag[:, 0, jj]

sofa.Data_Real = tmp_data_real
sofa.Data_Imag = tmp_data_imag
sofa.N = sofa_left.N # frequencies

# init defaults for HRTF:
output_file_sampling_rates = [int(2 * sofa.N[-1])]

elif sofa_type == 'HRIR':
tmp_data_ir = numpy.zeros((sofa_left.Data_IR.shape[0], 2, sofa_left.Data_IR.shape[2])) # init
for jj in range(sofa_left.Data_IR.shape[2]): # merge arrays
tmp_data_ir[:, 0, jj] = sofa_left.Data_IR[:, 0, jj]
tmp_data_ir[:, 1, jj] = sofa_right.Data_IR[:, 0, jj]

sofa.Data_IR = tmp_data_ir
sofa.Data_SamplingRate = sofa_left.Data_SamplingRate # fs
sofa.Data_Delay = numpy.ones((1, 2)) * sofa_left.Data_Delay

# init defaults for HRTF:
output_file_sampling_rates = [int(sofa.Data_SamplingRate)]
output_data_length = [sofa.Data_IR.shape[2]]

# extra "multi-sample rate HRIR" output processing
if numpy.round(sofa.Data_SamplingRate / sofa.Data_IR.shape[2]) == 150:
frequency_step = 150 # detected a valid multi-sample rate frequency step
elif numpy.round(sofa.Data_SamplingRate / sofa.Data_IR.shape[2]) == 75:
frequency_step = 75 # detected a valid multi-sample rate frequency step
else:
print('\n --- Note: the Frequency Step of this dataset is not equal to 150 or 75 Hz, therefore ')
print(' "multi-SamplingRate" HRIR saving is not possible. The only available SamplingRate is ' +
str(sofa.Data_SamplingRate) + ' ' + sofa.Data_SamplingRate_Units + '\n')
frequency_step = 0

if frequency_step > 0.5: # "multi-sample rate HRIR" output is possible
common_sampling_rates = [192000, 96000, 88200, 48000, 44100] # edit this DESCENDING list if you have to
for rate in common_sampling_rates:
if sofa.Data_IR.shape[2] > rate / frequency_step:
# add this sampling rate to output:
output_file_sampling_rates.append(rate)
output_data_length.append(int(rate / frequency_step))

else:
raise Exception("impossible :)")

# save the merged files
for f_nr in range(len(output_file_sampling_rates)):

if f_nr > 0: # extra "multi-SamplingRate HRIR" output processing
# trim data down to correct sampling rate:
# noinspection PyUnboundLocalVariable
sofa.Data_IR = sofa.Data_IR[:, :, :xf_eek:utput_data_length[f_nr]]
sofa.Data_SamplingRate = float(output_file_sampling_rates[f_nr])

# https://sofa_right.readthedocs.io/en/latest/sofa_right.html#sofa_right-documentation
sofa.verify()

# Save as SOFA file
path = os.path.join(basepath, filename[:-5] + '_' + str(output_file_sampling_rates[f_nr]) + '.sofa')

# Need to delete old FILE first if file already exists
if os.path.exists(path):
os.remove(path)

# Save
sf.write_sofa(path, sofa, version='latest', compression=9) # max compression

if f_nr == 0:
full_reference_path = path

# noinspection PyUnboundLocalVariable
return full_reference_path


# cart2sph conversion with DEGREE output
def cart2sph_in_deg(xyz):
theta_phi_r = numpy.empty(xyz.shape)

# based on pyfar example https://github.com/pyfar/pyfar/blob/main/pyfar/classes/coordinates.py
theta_phi_r[:, 2] = numpy.sqrt(xyz[:, 0] ** 2 + xyz[:, 1] ** 2 + xyz[:, 2] ** 2) # radius
z_div_r = numpy.where(theta_phi_r[:, 2] != 0, xyz[:, 2] / theta_phi_r[:, 2], 0)
theta_phi_r[:, 1] = numpy.rad2deg(numpy.arccos(z_div_r)) # colatitude
theta_phi_r[:, 0] = numpy.rad2deg(numpy.mod(numpy.arctan2(xyz[:, 1], xyz[:, 0]), 2 * numpy.pi)) # azimuth

# added fixes, including rounding to 2-3 decimal places:
theta_phi_r[:, 2] = numpy.around(theta_phi_r[:, 2], 3) # this one round to 3 decimal places (1mm precision)
theta_phi_r[:, 1] = numpy.around(- (theta_phi_r[:, 1] - 90), 2) # to match what "SOFAplotHRTF.m" does
theta_phi_r[:, 0] = numpy.around(theta_phi_r[:, 0], 3)
# theta_phi_r[:, 0] = numpy.around(numpy.where(theta_phi_r[:, 0] > 180,
# theta_phi_r[:, 0] - 360, theta_phi_r[:, 0]), 2)

return theta_phi_r


def scan_for_errors(project_path): # find any broken data in this project

# NOTE: made only to work with 1 source!!!
num_sources = 1

lowest_corrupt_frq = 0 # init

for source in range(num_sources):

full_p = os.path.join(project_path, 'NumCalc', 'source_' + str(source + 1))

# list all NumCalc logs
all_nc_list = [f for f in os.listdir(full_p) if
f.endswith('.out') and f.startswith('NC') and os.path.isfile(os.path.join(full_p, f))]

for file in all_nc_list:

# read the file
with open(os.path.join(full_p, file)) as ff:
lines = ff.readlines()
ff.close()

frequency_key = ', Frequency = '
non_convergence_key = 'Warning: Maximum number of iterations is reached!'
frequencies = [] # init
non_convergence_freq = [] # init

# find line containing the number of frequency steps
for line_nr in range(len(lines)):
if lines[line_nr].__contains__(frequency_key):
where_is_it = lines[line_nr].find(frequency_key)
# note down what is the frequency of this log and on which line
# (because more than 1 frequency may be in the same worker/log-file)
frequencies.append(int(lines[line_nr].strip()[where_is_it + len(frequency_key):-3]))

elif lines[line_nr].startswith(non_convergence_key):
if frequencies[-1] > 24000: # affects very high frequencies only
print('Warning - Non-Convergence detected at over 24kHz (at ' + str(frequencies[-1]) + 'Hz)')
else:
print('PROBLEM!! - Non-Convergence issue in important range: @ ' + str(frequencies[-1]) + 'Hz')

non_convergence_freq.append(frequencies[-1]) # note down which frequency failed

if len(non_convergence_freq) > 0: # we have non-convergence
if lowest_corrupt_frq == 0:
lowest_corrupt_frq = min(non_convergence_freq)

elif lowest_corrupt_frq > min(non_convergence_freq):
lowest_corrupt_frq = min(non_convergence_freq) # keep only the lowest problematic frequency

return lowest_corrupt_frq


def write_new_numFrequencies(path1, path2, corrupt_frq1, corrupt_frq2):
if min(corrupt_frq1, corrupt_frq2) == 0: # ignore the Zero init value
lowest_corrupt_frq = max(corrupt_frq1, corrupt_frq2)
else:
lowest_corrupt_frq = min(corrupt_frq1, corrupt_frq2)

for pro in [1, 2]: # execute for BOTH PROJECTS
if pro == 1:
project_path = path1
else:
project_path = path2
info_path = os.path.join(project_path, 'Info.txt')

# 1 Back up the Info.txt
info_bckp_path = os.path.join(project_path, 'Info_BACKUP_original.txt')
if not os.path.isfile(info_bckp_path): # backup does not yet exist
shutil.copy2(info_path, info_bckp_path)

# 2 read the original file
with open(info_path) as ff:
lines = ff.readlines()
ff.close()

# 3 Detect important information:
for line_nr in range(len(lines)):
if lines[line_nr].startswith('Minimum evaluated Frequency: '):
min_frequency = float(lines[line_nr].strip()[len('Minimum evaluated Frequency: '):])

elif lines[line_nr].startswith('Highest evaluated Frequency: '):
line_of_max_frequency = line_nr
orig_max_frequency = float(lines[line_nr].strip()[len('Highest evaluated Frequency: '):])

elif lines[line_nr].startswith('Frequency Stepsize: '):
frequency_step = float(lines[line_nr].strip()[len('Frequency Stepsize: '):])

elif lines[line_nr].startswith('Frequency Steps: '):
line_of_frequency_steps_nr = line_nr
orig_frequency_steps_nr = int(lines[line_nr].strip()[len('Frequency Steps: '):])

# 4 MODIFY the number of simulated frequency steps in the Info.txt
new_max_frequency = lowest_corrupt_frq - frequency_step
new_frequency_steps_nr = 1 + round((new_max_frequency - min_frequency) / frequency_step)

lines[line_of_frequency_steps_nr] = ('Frequency Steps: ' + str(int(new_frequency_steps_nr)) + '\n')
lines[line_of_max_frequency] = ('Highest evaluated Frequency: ' + str(new_max_frequency) + '\n')

# 5 add extra modification info for the Info.txt
lines.append('\n\n\n--------------------------------------------------------------------------------\n\n')
lines.append('NOTE: this file was MODIFIED by "join_sofa_files.py" to reduce maximum Frequency\n')
lines.append(' because simulation issue(s) were detected at higher frequencies.\n')
lines.append('\n Initial "Highest evaluated Frequency" was ' + str(orig_max_frequency) + '\n')
# noinspection PyUnboundLocalVariable
lines.append(' Initial "Frequency Steps" was ' + str(orig_frequency_steps_nr) + '\n')

# 6 OVER-WRITE the new Info.txt file
f3 = open(info_path, "w")
for li in lines:
f3.write(li)
f3.close()


def plot_hrir_pair(sofa_data_path, noise_floor=-70):
"""
Plots both left and right ear HRIRs in an easy to diagnose format

# based on tutorial: https://sofar.readthedocs.io/en/latest/working_with_sofa_files.html#working-with-sofa

Params:
sofa_data_path : full path to the sofa HRIR file to load and plot

noise_floor : were to limit colormap to provide easy to read image
= -50 # noise floor value used in "SOFAplotHRTF.m" from SOFA Matlab API
= -70 # value that visually matches the look of "SOFAplotHRTF.m" from SOFA MatlabAPI

"""

# read the SOFA file using pyfar
data_ir, source_coordinates, receiver_coordinates = pf.io.read_sofa(sofa_data_path)

# find all source positions on or in the vicinity of the HORIZONTAL plane using the find_slice method
_, mask = source_coordinates.find_slice('elevation', unit='deg', value=0, show=False) # do not Show the plot
angles = source_coordinates.get_sph('top_elev', 'deg')[mask, 0]

# noinspection PyTypeChecker
axes_subplot = plt.subplots(2, 1, figsize=(8, 6), sharex=True, label=sofa_data_path)
axes_subplot[0].set_size_inches(8, 10, forward=True)

for Ear in [0, 1]: # Ear 0 == LEFT and 1 == RIGHT ear

# main plot function
ax_h, qm_h, cb_h = pf.plot.time_2d(data_ir[mask, Ear], indices=angles, dB=True, log_prefix=20,
log_reference=1, unit=None, ax=axes_subplot[1][Ear],
cmap=mpl.cm.get_cmap(name='hot_r'),
orientation='horizontal') # YlOrBr hot hot_r RdBu

natural_limits = qm_h.get_clim() # get the full range of data
qm_h.set_clim([natural_limits[1] + noise_floor, natural_limits[1]]) # set limits according to noise floor
ax_h[0].set_ylabel("Azimuth angle in degrees")
# ax_h.set_ylim(0, 3)
if Ear == 0:
ax_h[0].set_title("Left ear HRIR (Horizontal plane)")
elif Ear == 1:
ax_h[0].set_title("Right ear HRIR (Horizontal plane)")

axes_subplot[0].savefig((sofa_data_path[:-5] + '.png'), dpi=100)


# ToDo - accept different input scenarios:
# A (DONE) - no inputs = scan start folder for 2 projects to merge.
# B - 1 input = Use the given path to identify a pair of projects to merge
# where folder-name for LEFT ends on '_L' and RIGHT ends on '_R'.
# C - 2 inputs = Merge the 2 projects that were given as input.

# # Identify starting conditions
print('join_sofa_files started with SavePath = "' + SavePath + '"')
if len(Input1) > 0 and (os.path.basename(Input1)[-2:] == "_L" or os.path.basename(Input1)[-2:] == "_R"):
ProjectName_withL = True # we can search for a mathing pairs of "_L" and "_R" projects
else:
ProjectName_withL = False # unless "Input2" is specified, search in this folder for 2 merge-able projects

if len(Input1) == 0 or (not ProjectName_withL and len(Input2) == 0):
# mode A = scan folder for 2 projects to merge.
counter = 0 # init
if len(Input1) == 0:
Search_Folder = SavePath # no inputs - search folder where the script is (or any custom SavePath)
else:
Search_Folder = Input1 # search in the first Input1 path

Path1 = '' # init
Path2 = '' # init
for subdir in os.listdir(Search_Folder):
# we only search folders that do not start with "." (extra scripts or files are allowed)
if os.path.isdir(os.path.join(Search_Folder, subdir)) and not subdir.startswith('.'):
counter += 1
if counter == 1:
Path1 = os.path.join(Search_Folder, subdir) # project 1
elif counter == 2:
Path2 = os.path.join(Search_Folder, subdir) # project 2
else:
raise Exception('more than 2 folders to merge in the Search_Folder = "' + Search_Folder + '"')

if len(Path2) == 0:
raise Exception('less than 2 folders to merge in the Search_Folder = "' + Search_Folder + '"')

elif len(Input2) == 0: # mode B - 1 input = Use the given path to identify a pair of projects to merge
# where folder-name for LEFT ends on '_L' and RIGHT ends on '_R'.
raise Exception("not implemented")

else: # mode C - 2 inputs = Merge the 2 projects that were given as input.
Path1 = Input1
Path2 = Input2

# DONE - autodetect details of the input scenario:
# a - folder with Mesh2HRTF project
# b - folder with .sofa files
# c - complete file path to "HRTF_<something>.sofa" of "HRIR_<something>.sofa"
if os.path.isdir(Path1):
# check if on Path1 is a Mesh2HRTF project folder that is not yet post-processed:
Lowest_corrupt_frq_1 = 0 # init
Lowest_corrupt_frq_2 = 0 # init
if not os.path.isdir(os.path.join(Path1, 'Output2HRTF')) and os.path.isfile(os.path.join(Path1, 'Output2HRTF.py')):
# Mode-A0 detected:

print('\nChecking for errors in "' + Path1 + '"...\n')
Lowest_corrupt_frq_1 = scan_for_errors(Path1) # find any broken data on Path1

print('\nChecking for errors in "' + Path2 + '"...\n')
Lowest_corrupt_frq_2 = scan_for_errors(Path2) # find any broken data on Path1

if Lowest_corrupt_frq_1 > 1 or Lowest_corrupt_frq_2 > 1: # there was coppupt data in the simulation
# exclude all frequencies that are corrupt by specifying the last Corrupt one.
write_new_numFrequencies(Path1, Path2, Lowest_corrupt_frq_1, Lowest_corrupt_frq_2)

print('\nRunning Mesh2HRTF post-processing in "' + Path1 + '"...\n')
os.chdir(Path1) # necessary for the 'Output2HRTF.py'
subprocess.call("Output2HRTF.py", shell=True) # run the normal Mesh2HRTF post-processing
# noinspection PyUnboundLocalVariable
os.chdir(Search_Folder) # change back to Search_Folder (just in case)

# check if on Path2 is a Mesh2HRTF project folder that is not yet post-processed:
if not os.path.isdir(os.path.join(Path2, 'Output2HRTF')) and os.path.isfile(os.path.join(Path2, 'Output2HRTF.py')):
print('\nRunning Mesh2HRTF post-processing in "' + Path2 + '"...\n')
os.chdir(Path2) # necessary for the 'Output2HRTF.py'
subprocess.call("Output2HRTF.py", shell=True) # run the normal Mesh2HRTF post-processing
os.chdir(Search_Folder) # change back to Search_Folder (just in case)

if os.path.isdir(os.path.join(Path1, 'Output2HRTF')): # a - folder with Mesh2HRTF project
SofaFLDR_1 = os.path.join(Path1, 'Output2HRTF')
SofaFLDR_2 = os.path.join(Path2, 'Output2HRTF')
else: # b - folder with .sofa files
SofaFLDR_1 = Path1
SofaFLDR_2 = Path2

AllSofaFiles1 = os.listdir(SofaFLDR_1) # searching for '*.sofa' files
AllSofaFiles2 = os.listdir(SofaFLDR_2)
FolderName = os.path.basename(Path1)

else: # os.path.isfile(Path1): # c - complete file path to "HRTF_<something>.sofa" of "HRIR_<something>.sofa"
AllSofaFiles1 = [os.path.basename(Path1)]
AllSofaFiles2 = [os.path.basename(Path2)]
SofaFLDR_1 = os.path.dirname(Path1)
SofaFLDR_2 = os.path.dirname(Path2)
FolderName = os.path.basename(SofaFLDR_1)

if FolderName.endswith('_L') or FolderName.endswith('_R'):
FolderName = FolderName[:-2] # drop the "_L" or "_R" from the folder name (tidy up)
BasePath = os.path.join(SavePath, 'SOFA_' + FolderName + '_merged')

# autodetect which is Left and which is Right based on source coord.
HRTFsofa_read_1 = sf.read_sofa(os.path.join(SofaFLDR_1, AllSofaFiles1[0]))
if HRTFsofa_read_1.ReceiverPosition.shape[0] > 1: # more than one receiver in this SOFA file
raise Exception('The SOFA file is not for a single ear! File in: "' + SofaFLDR_1 + '"')

if HRTFsofa_read_1.ReceiverPosition[0, 1] > 0: # sofa file is for Left ear (positive Y coordinate)
AllSofaFiles_L = AllSofaFiles1
AllSofaFiles_R = AllSofaFiles2
SofaFLDR_L = SofaFLDR_1
SofaFLDR_R = SofaFLDR_2
else: # flip inputs because the 2nd folder contains Left ear
AllSofaFiles_R = AllSofaFiles1
AllSofaFiles_L = AllSofaFiles2
SofaFLDR_R = SofaFLDR_1
SofaFLDR_L = SofaFLDR_2

# Save complex pressure as SOFA file
print('\n Merging HRTF SOFA files from: "' + os.path.basename(Path1) + '" and "' +
os.path.basename(Path2) + '"')

del SofaFLDR_2, SofaFLDR_1, AllSofaFiles2, AllSofaFiles1, Path1, Path2

for ii in range(2): # range(len(AllSofaFiles_L))

# # Load the data
sofa_read_L = sf.read_sofa(os.path.join(SofaFLDR_L, AllSofaFiles_L[ii]))
sofa_read_R = sf.read_sofa(os.path.join(SofaFLDR_R, AllSofaFiles_R[ii]))

# detect data type:
if sofa_read_L.GLOBAL_DataType == 'FIR':
SOFA_Type = 'HRIR'

# sanity checks that the files can be merged
if sofa_read_L.Data_SamplingRate != sofa_read_R.Data_SamplingRate:
raise Exception("The SamplingRates of the two input SOFA files do not match!")
elif sofa_read_L.Data_IR.shape[1] != 1 or sofa_read_R.Data_IR.shape[1] != 1:
raise Exception("The input SOFA files contain more than one ear - impossible to merge")
elif sofa_read_L.Data_IR.shape[2] != sofa_read_R.Data_IR.shape[2]:
raise Exception("The number of steps in the two input SOFA files do not match!")
elif sofa_read_L.ReceiverPosition_Units != sofa_read_R.ReceiverPosition_Units:
raise Exception("The ReceiverPosition_Units in the two input SOFA files do not match!")

else:
SOFA_Type = 'HRTF'

# sanity checks that the files can be merged
if sofa_read_L.N[0] != sofa_read_R.N[0] or sofa_read_L.N[-1] != sofa_read_R.N[-1]:
raise Exception("The sampling Frequencies of the two input SOFA files do not match!")

if ii == 0 and not os.path.isdir(BasePath):
os.mkdir(BasePath) # create new output folder

# noinspection PyTypeChecker
Full_Ref_Path = merge_write_sofa(sofa_read_L, sofa_read_R, BasePath, AllSofaFiles_L[ii], SOFA_Type)

if SOFA_Type == 'HRIR': # note down what should be plotted in the end
SOFA_to_plot = Full_Ref_Path

# make a simple copy of the Info.txt
if os.path.isfile(os.path.join(os.path.dirname(SofaFLDR_L), 'Info.txt')):
shutil.copy2(os.path.join(os.path.dirname(SofaFLDR_L), 'Info.txt'), os.path.join(BasePath, 'Info.txt'))

print('\n Done: Merged SOFA files are saved in:\n "' + BasePath + '"\n')

if Diagnostic_plot:

# noinspection PyBroadException
try:
#############################################################################
# plotting part: it is OK if plotting crashes, because the merging is already complete.

# these imports are here so that key functionality works even without "pyfar" and "matplotlib"
import pyfar as pf # https://pypi.org/project/sofar/ pip install pyfar
import matplotlib as mpl
import matplotlib.pyplot as plt

# noinspection PyUnboundLocalVariable
plot_hrir_pair(SOFA_to_plot, -50)

#############################################################################
except Exception:
print('\n WARNING: plotting command crashed. Likely reasons are:')
print(' 1 - missing "pyfar" or missing "matplotlib"')
print(' (to install "pyfar" in command line type "pip install pyfar" ')
print(' 2 - obsolete python packages (especially numpy) - try running: "pip list --outdated"')
print(' (to update a package use for example: "pip install --upgrade numpy"')
print(' 3 - actual bug in this script (debug, or open an issue if you need this plot)\n')

input('Hit Enter to close - Plotting is complete :)')

else:
input('Hit Enter to exit :)')

#
# made for the Mesh2HRTF codebase
#
# mesh2hrtf.sourceforge.net
#
# Mesh2HRTF is licensed under the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version. Mesh2HRTF is distributed in the hope
# that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. You should have received a
# copy of the GNU LesserGeneral Public License along with Mesh2HRTF. If not,
# see <http://www.gnu.org/licenses/lgpl.html>.
#
# If you use Mesh2HRTF:
# - Provide credits:
# "Mesh2HRTF, H. Ziegelwanger, ARI, OEAW (mesh2hrtf.sourceforge.net)"
# - In your publication, cite both articles:
# [1] Ziegelwanger, H., Kreuzer, W., and Majdak, P. (2015). "Mesh2HRTF:
# Open-source software package for the numerical calculation of head-
# related transfer functions," in Proceedings of the 22nd ICSV,
# Florence, IT.
# [2] Ziegelwanger, H., Majdak, P., and Kreuzer, W. (2015). "Numerical
# calculation of listener-specific head-related transfer functions and
# sound localization: Microphone model and mesh discretization," The
# Journal of the Acoustical Society of America, 138, 208-222.

Looks good now. Do you still get index out of range when trying to run script ? Input1 and Input2 is optional parameters as per script and may not even be needed as script captures your default working directory too.

We could start to debug it, but not sure if it's the right route.
 
Mar 26, 2022 at 5:07 AM Post #19 of 93
But I think that is the case here?


There is no .sofa file? The .out files contain the impulse response data?

The error that user is getting is because there is no variable assigned to
AllSofaFiles1[0]

AllSofaFiles1 = [os.path.basename(Path1)] --> Path1 pushes an empty string to it and because of this AllSofaFiles1[0] is unable to query it's member

Input1/Input2 provided by user is assigned to the Path1/Path2 only in certain scenarios and depends on the mode being used. You can leave Input1/Input2 values empty by default as well.

What I know is that you should have Output2HRTF.py available for Left & Right side and script is not detecting your directories. Maybe different Sofa versions uses different folder naming to put output data and this manual is a bit outdated..

You could try to rename your folders sofa_left & sofa_right, but that's more like a blind guess as I see these variables defined in script
 
Mar 26, 2022 at 6:11 AM Post #20 of 93
The error that user is getting is because there is no variable assigned to
AllSofaFiles1[0]

AllSofaFiles1 = [os.path.basename(Path1)] --> Path1 pushes an empty string to it and because of this AllSofaFiles1[0] is unable to query it's member

Input1/Input2 provided by user is assigned to the Path1/Path2 only in certain scenarios and depends on the mode being used. You can leave Input1/Input2 values empty by default as well.

What I know is that you should have Output2HRTF.py available for Left & Right side and script is not detecting your directories. Maybe different Sofa versions uses different folder naming to put output data and this manual is a bit outdated..

You could try to rename your folders sofa_left & sofa_right, but that's more like a blind guess as I see these variables defined in script
I’ll give that a shot too otherwise i might just be missing files not produced when exporting the measurements.

If that fails I might just start from scratch again
 
Mar 26, 2022 at 7:58 AM Post #21 of 93
I got a reply from the one of the people making the tutorials apparently the mesh needs to be air tight which makes sense. Should be like I am meshing in blender to 3d print the solid object. The errors were because some things were not calculated because of the holes.

Guess I’ll need to learn more about blender and how to make a solid mesh.
 
Mar 26, 2022 at 11:20 PM Post #22 of 93
I got a reply from the one of the people making the tutorials apparently the mesh needs to be air tight which makes sense. Should be like I am meshing in blender to 3d print the solid object. The errors were because some things were not calculated because of the holes.

Guess I’ll need to learn more about blender and how to make a solid mesh.
If you have no experience with 3D software, Meshmixer(also free) and youtube might be a little easier/a different approach than Blender if it's only about repair/patch up of mesh. That's the main job of Meshmixer(that, some tools for 3D printing, and for some reason, adding chicken legs under the head of Dwayne Johnson or something:sweat_smile:). While Blender is the Frankenstein monster itself, where motivated people kept adding tools over tools over tools just because they could. Decades of that did not result in something very intuitive IMO.
Powerful, sure. Nice with beginners? Nope.
 
Mar 27, 2022 at 5:48 AM Post #23 of 93
If you have no experience with 3D software, Meshmixer(also free) and youtube might be a little easier/a different approach than Blender if it's only about repair/patch up of mesh. That's the main job of Meshmixer(that, some tools for 3D printing, and for some reason, adding chicken legs under the head of Dwayne Johnson or something:sweat_smile:). While Blender is the Frankenstein monster itself, where motivated people kept adding tools over tools over tools just because they could. Decades of that did not result in something very intuitive IMO.
Powerful, sure. Nice with beginners? Nope.
Thanks I’ve just checked meshmixer looks real good for this. Nice and easy to analyse and just fix all the holes. Awesome recommendation buddy
 
Apr 10, 2022 at 6:14 AM Post #24 of 93
A sofa file can contain someone's HRTF as a collection of impulces (impulce responses) for many different directions, for every direction one impulce to the left ear and one impulce to the right ear.
For example Genelec Aural ID used sofa files containing 836 different directions.
I don't know how many you get from Mesh2hrtf, @morgin do you know? Or can you maybe choose how many and which ones?

I don't know how the file is structered
just got my first batch of .sofa files from mesh2hrtf here is the megaupload link if you want to check it

https://mega.nz/folder/UQky2Yja#PMqQDyVAOGskS4H1zFe5jw

Do you know a way to convert these to .wav so i can try them with hesuvi?
 
Apr 10, 2022 at 10:39 AM Post #25 of 93
Do you know a way to convert these to .wav so i can try them with hesuvi?
No, not at this moment. I hope to figure it out some day.

One thought: If you can not find software that directly does the conversion there is an alternative approach:
If you can find software that can use the sofa file to apply the convolution for any chosen direction to an input signal, then you could just run the Impulcifer sweeps through that (for each of the directions that you want) and proces the output with Impulcifer the normal way.
 
Apr 10, 2022 at 4:51 PM Post #26 of 93
just got my first batch of .sofa files from mesh2hrtf here is the megaupload link if you want to check it

https://mega.nz/folder/UQky2Yja#PMqQDyVAOGskS4H1zFe5jw

Do you know a way to convert these to .wav so i can try them with hesuvi?

There are two sofa files HRIR and HRTF. Which is the correct one?
I tried to load the HRIR in matlab and I can see the data channels but I don't know which are the correct channels for 7.1. I see the source positions with spehric coordinates. I guess it is (phi, theta,r) but trying something like (30°,0°,r) does not sound like the left channel. is the left channel.

In the API for Python I can load and plot the sofa file so it should work to get the correct channels and convert them to wav. But in the momen I don't know what are the correct channels?

Edit: Oh I forgot to transpose the files (from 16:320 to 320:16) before converting to wave. that explains why it sounded strange. here the correct files:
converted wav files
 
Last edited:
Apr 10, 2022 at 5:11 PM Post #27 of 93
The tutorial says the HRIR one is the best one. I’ve asked in other places too some people in binaural discord have apps that read .sofa or convert .sofa to .wav but they didn’t work. One person did say it was working with 3d toolkit

A334B014-9BED-425E-92CA-B430408775F5.png


This is from the tutorial. I had no errors through the process and the image was the same as the good sample they show.


  1. Done. The final "HRTF.sofa" and "HRIR.sofa" files will be saved next to the input project folders (path is visible in the printouts of the "join_sofa_files.py" script). the typical final output from Mesh2HRTF simulation after the "join_sofa_files.py" will be:
    • HRIR_ARI_48000.sofa - the main "HRTF" file to use if sound card is used at 48kHz sampling rate ("the DVD sampling rate" - common in video files).
    • HRIR_ARI_44100.sofa - the main "HRTF" file to use if sound card is used at 44.1kHz sampling rate ("the CD sampling rate" - common among music files).
    • any additional HRIR sampling rates - if the simulation "Max frequency" was higher than 24kHz, then there will be additional HRIR.sofa files. Note, while it is technically possible to simulate 96kHz, 192kHz and other extended frequency range sampling rates, there is almost no practical benefit in doing so.
    • HRTF_ARI_48000.sofa - the frequency domain HRTF file. Not as popular as HRIR files.
    • HRIR_ARI_48000.png - the image file with the plot of the HRIR.sofa data for quick graphical test that SOFA files look reasonable compared to other examples. (the plot is only generated if "pyfar" Python package is installed.)
    • Info.txt - a simple copy of the project Info file generated by Blender. Can be useful for keeping track of the origin of the SOFA files.
example of good simulated HRIR data plotted and saved by "join_sofa_files.py" v1.2 for graphical comparison
HRIR_ARI_50400.png
 
Apr 11, 2022 at 5:33 PM Post #28 of 93
There are two sofa files HRIR and HRTF. Which is the correct one?
I tried to load the HRIR in matlab and I can see the data channels but I don't know which are the correct channels for 7.1. I see the source positions with spehric coordinates. I guess it is (phi, theta,r) but trying something like (30°,0°,r) does not sound like the left channel. is the left channel.

In the API for Python I can load and plot the sofa file so it should work to get the correct channels and convert them to wav. But in the momen I don't know what are the correct channels?

Edit: Oh I forgot to transpose the files (from 16:320 to 320:16) before converting to wave. that explains why it sounded strange. here the correct files:
converted wav files
Man you are such a good person. Can you please link me your PayPal so I can give you a little bit. Private message me if it’s possible on here
 
Apr 12, 2022 at 12:06 AM Post #29 of 93
Wow! ok, so I wasn't expecting that but it's actually very good. And considering this is a not the best detailed ears scan or perfect ears microphone placement. Its way more detailed and crisp than my impulcifer, but I prefer impulcifer because of the wide sound stage it gives, like the whole room is filled with sound. If theres a way to get this to be as wide or sounding like there are speakers around me it's a very very good option. I would recommend you try this out if you have an iphone 10 (newer models have worse true depth camera), and 16gb ram or more. I would love to help if anyone wants to try it. I will be torn between using this and impulcifer now.

@musicreo thankyou again I hope you PM me. was it diffcult to convert it into .wav or will someone like me be able to do it in future. I'm pumped to get better scans now.
 
Last edited:
Apr 12, 2022 at 1:59 AM Post #30 of 93
@morgin: I could guess it doesn't sound like speakers in a room, but is the sound outside your head? And how far? Have you also tried listening to a single channel and how far outside the head does that sound? (The Smyth Realiser has a "solo" function which lets you listen to a single virtual speaker of choice which can be very informative. In your case to do that you could use a test track that only has sound in one channel.)
 

Users who are viewing this thread

Back
Top