Mesh2hrtf
Mar 25, 2022 at 7:32 AM Thread Starter Post #1 of 93

morgin

100+ Head-Fier
Joined
Aug 5, 2021
Posts
341
Likes
76
Location
united kingdom
Has anyone tried this? I’m currently near the last stage where I need to generate the sofa files using "join_sofa_files.py”

But I get these error messages
 

Attachments

  • 18001790-2213-4EE4-B696-2AB9A5795FCE.png
    18001790-2213-4EE4-B696-2AB9A5795FCE.png
    717.5 KB · Views: 0
  • B80070F3-BAED-4D73-B0DC-D59E1B435EDE.png
    B80070F3-BAED-4D73-B0DC-D59E1B435EDE.png
    1.8 MB · Views: 0
Mar 25, 2022 at 10:31 AM Post #2 of 93
Has anyone tried this? I’m currently near the last stage where I need to generate the sofa files using "join_sofa_files.py”

But I get these error messages

What does this sofa do and what are you trying to achieve ? Regards python this error is thrown when you try to access index which is not within the list.
Example"
python_list = [a, b, c] --> a has index 0, b has index 1, c index 2 and etc...

So print(python_list[4]) -> would throw exact same error. On first screen you got misstypo.
Can you share the code of join_sofa_files.py ? though this might be offtopic
 
Last edited:
Mar 25, 2022 at 11:05 AM Post #3 of 93
Thanks for your detailed reply

I’ve virtually measured my head/ears and it has produced some folders that can generate my .sofa file. There are two folders generated for left and right ears and the join_sofa_files.py finds those two folders to convert into .sofa

This is the link to the code if posting it like I have at the bottom isn’t easy to read

https://sourceforge.net/p/mesh2hrtf...ree/SOFA_Merge_Folder/join_sofa_files.py#l590

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
1
import os
import numpy
import sofar as sf # https://pypi.org/project/sofar/ pip install sofar
import subprocess
import shutil

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

Input1 = '' # Empty by default (in mode A1), optional path to the 1st project to merge (for mode A2, B or C)
Input2 = '' # Empty by default (in mode A or B), optional path to the 2nd project to merge (for mode C)
# 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.
 
Last edited:
Mar 25, 2022 at 11:57 AM Post #4 of 93
HRTFsofa_read_1 variable throws an error because there is no value assigned to AllSofaFiles1.

In script you need to provide inputs depending on the mode that you are using.

Input1 = '' # Empty by default (in mode A1), optional path to the 1st project to merge (for mode A2, B or C)
Input2 = '' # Empty by default (in mode A or B), optional path to the 2nd project to merge (for mode C)


It should look similarly to the below:

Input1 = "C:/pathtothefile/youwantTomerge"

Input2 might be needed as well depending on your setup, but I have no idea what sofa is and what these different modes do
 
Last edited:
Mar 25, 2022 at 12:01 PM Post #5 of 93
HRTFsofa_read_1 variable throws an error because there is no value assigned to AllSofaFiles1.

In script you need to provide inputs depending on the mode that you are using.

Input1 = '' # Empty by default (in mode A1), optional path to the 1st project to merge (for mode A2, B or C)
Input2 = '' # Empty by default (in mode A or B), optional path to the 2nd project to merge (for mode C)


It should look similarly to the below:

Input1 = "C:/pathtothefile/youwantTomerge"

Input2 might be needed as well depending on your setup, but I have no idea what sofa is and what these different modes do
Thank you soooooooo much I’ll give this a try I spent hours trying different combinations of the folder locations.
 
Mar 25, 2022 at 12:07 PM Post #6 of 93
Thank you soooooooo much I’ll give this a try I spent hours trying different combinations of the folder locations.

No problem. Script needs to know your file locations and you will have to adjust it if you will work in different directories :thumbsup:
 
Mar 25, 2022 at 12:15 PM Post #7 of 93
I have no idea what sofa is
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
 
Mar 25, 2022 at 12:42 PM Post #8 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
I have no idea, I’m learning as I go but I think it’s many because the whole process took my 16gb ram i7 7700k few hours to process
 
Last edited:
Mar 25, 2022 at 3:08 PM Post #9 of 93
I'm now getting this error. The file should just be double click with the folder next to this .py file, the tutorial does not say to change any locations. But i tried changing input 1 to one of the folder and input 2 to the second folder this is what i'm getting

Microsoft Windows [Version 10.0.22000.588]
(c) Microsoft Corporation. All rights reserved.

C:\mesh2hrtf-tools\SOFA_Merge_Folder>python3 join_sofa_files.py
File "C:\mesh2hrtf-tools\SOFA_Merge_Folder\join_sofa_files.py", line 9
Input1 = r"C:\mesh2hrtf-tools\SOFA_Merge_Folder" Empty by default (in mode A1), optional path to the 1st project to merge (for mode A2, B or C)
^
SyntaxError: invalid decimal literal

C:\mesh2hrtf-tools\SOFA_Merge_Folder>python3 join_sofa_files.py
join_sofa_files started with SavePath = "C:\mesh2hrtf-tools\SOFA_Merge_Folder"
Traceback (most recent call last):
File "C:\mesh2hrtf-tools\SOFA_Merge_Folder\join_sofa_files.py", line 473, in <module>
HRTFsofa_read_1 = sf.read_sofa(os.path.join(SofaFLDR_1, AllSofaFiles1[0]))
IndexError: list index out of range

C:\mesh2hrtf-tools\SOFA_Merge_Folder>python3 join_sofa_files.py
join_sofa_files started with SavePath = "C:\mesh2hrtf-tools\SOFA_Merge_Folder"
Traceback (most recent call last):
File "C:\mesh2hrtf-tools\SOFA_Merge_Folder\join_sofa_files.py", line 473, in <module>
HRTFsofa_read_1 = sf.read_sofa(os.path.join(SofaFLDR_1, AllSofaFiles1[0]))
IndexError: list index out of range

C:\mesh2hrtf-tools\SOFA_Merge_Folder>
 
Mar 25, 2022 at 5:23 PM Post #10 of 93
I'm now getting this error. The file should just be double click with the folder next to this .py file, the tutorial does not say to change any locations. But i tried changing input 1 to one of the folder and input 2 to the second folder this is what i'm getting

Microsoft Windows [Version 10.0.22000.588]
(c) Microsoft Corporation. All rights reserved.

C:\mesh2hrtf-tools\SOFA_Merge_Folder>python3 join_sofa_files.py
File "C:\mesh2hrtf-tools\SOFA_Merge_Folder\join_sofa_files.py", line 9
Input1 = r"C:\mesh2hrtf-tools\SOFA_Merge_Folder" Empty by default (in mode A1), optional path to the 1st project to merge (for mode A2, B or C)
^
SyntaxError: invalid decimal literal

C:\mesh2hrtf-tools\SOFA_Merge_Folder>python3 join_sofa_files.py
join_sofa_files started with SavePath = "C:\mesh2hrtf-tools\SOFA_Merge_Folder"
Traceback (most recent call last):
File "C:\mesh2hrtf-tools\SOFA_Merge_Folder\join_sofa_files.py", line 473, in <module>
HRTFsofa_read_1 = sf.read_sofa(os.path.join(SofaFLDR_1, AllSofaFiles1[0]))
IndexError: list index out of range

C:\mesh2hrtf-tools\SOFA_Merge_Folder>python3 join_sofa_files.py
join_sofa_files started with SavePath = "C:\mesh2hrtf-tools\SOFA_Merge_Folder"
Traceback (most recent call last):
File "C:\mesh2hrtf-tools\SOFA_Merge_Folder\join_sofa_files.py", line 473, in <module>
HRTFsofa_read_1 = sf.read_sofa(os.path.join(SofaFLDR_1, AllSofaFiles1[0]))
IndexError: list index out of range

C:\mesh2hrtf-tools\SOFA_Merge_Folder>

You can't leave such line in Input1 field, python syntax doesn't understand anything past double quotes.
It should be like this

Input1 = "C:\mesh2hrtf-tools\SOFA_Merge_Folder"

You can do it that way too if you want to keep description

Input1 = "C:\mesh2hrtf-tools\SOFA_Merge_Folder" #Empty by default (in mode A1), optional path to the 1st project to merge (for mode A2, B or C)
 
Last edited:
Mar 25, 2022 at 6:16 PM Post #11 of 93
Have you tried to put the sofa files directly into the folder with the script instead of putting them into subfolders named left and right?

I understand the error like that the script is not fiding any sofa file.
 
Last edited:
Mar 25, 2022 at 6:53 PM Post #12 of 93
Have you tried to put the sofa files directly into the folder with the script instead of putting them into subfolders named left and right?

I understand the error like that the script is not fiding any sofa file.
Script can use the same location if it’s coded that way, but it won’t be able to guess folder/file naming. In his example he didn’t put parameters correct way and that’s the reason why python throws syntax and index out of range errors.

Script could only guess location if these folders/files are created with default naming
 
Last edited:
Mar 25, 2022 at 7:03 PM Post #13 of 93
Input1 = "C:\mesh2hrtf-tools\SOFA_Merge_Folder"
I tried that with the bottom input in the pic.

Have you tried to put the sofa files directly into the folder with the script instead of putting them into subfolders named left and right?

I understand the error like that the script is not fiding any sofa file.
yeah I've tried all sorts of combinations changing folder names, locations, putting the .py in one folder copying both folders into 1. Nothing I've tried works. I'm thinking maybe I'm missing some .sofa file or it wasn't fully calculated but the command said completed and no errors popped up.

these are the files that were generated in the left ear folder to be converted by the .py into .sofa they are the same in the right ear folder. My question is do they look right or would there be another file that could be missing

Screenshot 2022-03-25 225238.png
 
Mar 25, 2022 at 7:10 PM Post #14 of 93
Send me how user settings code part looks after your path update
 
Mar 25, 2022 at 7:36 PM Post #15 of 93
Script could only guess location if these folders/files are created with default naming
But I think that is the case here?

these are the files that were generated in the left ear folder to be converted by the .py into .sofa they are the same in the right ear folder. My question is do they look right or would there be another file that could be missing
There is no .sofa file? The .out files contain the impulse response data?
 

Users who are viewing this thread

Back
Top