Quick tour of SOFA and sofar¶
If you are new to SOFA and/or sofar, this is a good place to start. SOFA is short for Spatially Oriented Format for Acoustics and is an open file format for saving acoustic data, as for example head-related impulse responses (HRIRs). A good places to get more information about SOFA are
The SOFA paper
The SOFA standard AES69-2022
Creating SOFA objects¶
To cover a variety of data, SOFA offers different conventions. A convention defines, what data can be saved and how it is saved. You should always find the most specific convention for your data. This will help you to identify relevant data and meta data that you should provide along the actual acoustic data. Using sofar, a list of possible conventions can be obtained with
import sofar as sf sf.list_conventions()
Let us assume, that you want to store head-related impulse responses (HRIRs). In this case the most specific convention is SimpleFreeFieldHRIR. To create a SOFA object use
sofa = sf.Sofa("SimpleFreeFieldHRIR")
The return value sofa is a
sofar.Sofa object filled with the default
values of the SimpleFreeFieldHRIR convention. Note that
also return a sofa object that has only the mandatory attributes. However, it
is recommended to start with all attributes and discard empty optional
attributes before saving the data.
Getting information about SOFA objects¶
To get an overview of the convention, go to the documentation of the SOFA conventions.
You might have noted from the documentation that three different kinds of data types can be stored in SOFA files:
Attributes are meta data stored as strings. There are two kinds of attributes. Global attributes give information about the entire data stored in a SOFA file. All entires starting with GLOBAL are such attributes. Specific attributes hold meta data for a certain variable. These attributes thus start with the name of the variable followed by an underscore, e.g., ListenerPosition_Units. An exception to this rule are the data variables, e.g, Data_IR is not an attribute but a double variable.
- Double Variables:
Variables of type double store numeric data and can be entered as numbers, lists, or numpy arrays.
- String Variables:
Variables of type string store strings and can be entered as strings, lists of string, or numpy string arrays.
The data can be mandatory, optional, and read only and must have a shape (dimension in SOFA language) according to the underlying convention. Read on for more information.
To get a quick insight into SOFA objects use
sofa.inspectprints the data stored in a SOFA object or at least gives the shape in case of large arrays that would clutter the output. This is helpful when reading data from an existing SOFA object.
sofa.list_dimensionsprints the dimensions of the data inside the SOFA object.
sofa.get_dimensionreturns the size of a specific dimension.
For the SimpleFreeFieldHRIR SOFA object we have the following dimensions
sofa.list_dimensions >>> R = 2 receiver (set by ReceiverPosition of dimension RCI, RCM) >>> E = 1 emitter (set by EmitterPosition of dimension ECI, ECM) >>> M = 1 measurements (set by Data_IR of dimension MRN) >>> N = 1 samples (set by Data_IR of dimension MRN) >>> C = 3 coordinate dimensions, fixed >>> I = 1 single dimension, fixed >>> S = 0 maximum string length
In this case, M denotes the number of source positions for which HRIRs are available, R is the number of ears - which is two - and N gives the lengths of the HRIRs in samples. S is zero, because the convention does not have any string variables. C is always three, because coordinates are either given by x, y, and z values or by their azimuth, elevation and radius in degree.
It is important to be aware of the dimensions and enter data as determined by
the convention. SOFA sets the dimensions
implicitly. This means the dimensions are derived from the data itself, as
indicated by the output of
sofa.list_dimensions above (set by…). In
some cases, variables can have different shapes. An example for this is the
ReceiverPosition which can be of shape RCI or RCM. To get a dimension as a
sofa.get_dimension("N) >>> N = 1
Let’s assume you downloaded a SOFA file from the FABIAN database and want to quickly inspect it. You could use
sofa = sf.read_sofa("FABIAN_HRIR_measured_HATO_0.sofa") sofa.inspect() >>> GLOBAL_License : Creative Commons (CC-BY). Visit http://creativecommons.org/licenses/by/4.0/ for licence details. >>> GLOBAL_Organization : Audio Communication Group, TU Berlin, Germany (www.ak.tu-berlin.de) >>> ReceiverPosition : (R=2, C=3, I=1) >>> [[ 0. 0.0662 0. ] >>> [ 0. -0.0662 0. ]] >>> Data_IR : (M=11950, R=2, N=256) >>> Data_SamplingRate : 44100.0 >>> Data_SamplingRate_Units : hertz
Note that the above does not show the entire information for the sake of
brevity. This will most likely give you a better idea of the data then
looking at the definition of the convention or calling
Adding data to SOFA objects¶
Data can simply be obtained and entered
sofa.Data_IR # prints [0, 0] sofa.Data_IR = [1, 1] sofa.SourcePosition = [90, 0, 1.5]
Now, the SOFA object contains a single HRIR - which is
1 for the left
1 for the right ear - for a source at
0 degree azimuth,
degree elevation and a radius of
1.5 meter. Note that you just entered a
list for Data_IR although it has to be a three-dimensional double variable.
Sofar handles this in two steps.
When entering data as lists it is converted to a numpy array with at least two dimensions.
Missing dimensions are appended when writing the SOFA object to disk.
You should now fill all mandatory entries of the SOFA object if you were for real. For this example we’ll cut it here for the sake of brevity. Let us, however, delete an optional entry that we do not need at this point
In some cases you might want to add custom data - although third party applications most likely won’t make use of non-standardized data. Try this to add a temperature value and unit
sofa.add_variable("Temperature", 25.1, "double", "MI") sofa.add_attribute("Temperature_Units", "degree Celsius")
After entering the data, the SOFA object should be verified to make sure that your data can (most likely) be read by other applications.
This will check the following
Are all mandatory data contained?
Are the names of variables and attributes in accordance with the SOFA standard?
Are the data types in accordance with the SOFA standard?
Are the dimensions of the variables consistent and in accordance to the SOFA standard?
Are the values of attributes consistent and in accordance to the SOFA standard?
If any violations are detected, an error is raised.
Reading and writing SOFA objects¶
Note that you usually do not need to call
sofa.verify() separately because
it is by default called if you create write or read a SOFA object. To write
your SOFA object to disk type
It is good to know that SOFA files are essentially netCDF4 files which is based on HDF5. They can thus be viewed with HDF View.
To read your sofa file you can use
sofa_read = sf.read_sofa("your/path/to/SingleHRIR.sofa")
And to see that the written and read files contain the same data you can check
sf.equals(sofa, sofa_read) >>> True
Upgrading SOFA files¶
SOFA conventions might get updates to fix bugs in the conventions, in case new conventions are introduced, or in case conventions get deprecated. To find out if SOFA data from a file is up to data load it and call
which will list upgrade choices or let you know that the convention is already up to date.