sofa

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 20 Imported by: 0

README

go-sofa

A pure-Go library for reading and writing SOFA files (Spatially Oriented Format for Acoustics, AES69-2015).

SOFA is a file format for storing spatially oriented acoustic data like head-related transfer functions (HRTFs), binaural room impulse responses (BRIRs), and directional room impulse responses (DRIRs). The format is based on HDF5 and follows the netCDF-4 conventions.

Features

  • Pure Go implementation — No C dependencies
  • Full AES69 support — Reads and writes all standard SOFA metadata and data arrays
  • DataTypes — FIR, TF, TF-E (including spherical-harmonics HRTFs) and SOS
  • Interoperable output — Written files are netCDF-4 with named dimensions and open in h5py, netCDF4 and ncdump; FIR and SOS files also load in libmysofa, which does not read TF/TF-E (details)
  • Built on go-hdf5 — Leverages cwbudde/go-hdf5, our maintained fork of scigolib/hdf5, for HDF5 file access
  • Command-line tools — Includes sofainfo, sofa2json and sofaprobe utilities
  • Well-tested — Validated against reference SOFA files from sofaconventions.org

Installation

Library
go get github.com/CWBudde/go-sofa
Command-line tools
go install github.com/CWBudde/go-sofa/cmd/sofainfo@latest
go install github.com/CWBudde/go-sofa/cmd/sofa2json@latest
go install github.com/CWBudde/go-sofa/cmd/sofaprobe@latest

Library Usage

Basic example
package main

import (
    "fmt"
    "log"

    "github.com/CWBudde/go-sofa"
)

func main() {
    // Open a SOFA file
    f, err := sofa.Open("example.sofa")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()

    // Print basic information
    fmt.Printf("Title: %s\n", f.Title)
    fmt.Printf("Measurements: %d\n", f.M)
    fmt.Printf("Receivers: %d\n", f.R)
    fmt.Printf("Samples: %d\n", f.N)
    if sr, err := f.SamplingRateScalar(); err == nil { // not for TF, or varying rates
        fmt.Printf("Sample Rate: %.0f Hz\n", sr)
    }
    if d, err := f.Duration(); err == nil { // FIR only
        fmt.Printf("Duration: %.3f seconds\n", d)
    }
}
Accessing impulse responses
// Get impulse response for measurement 0, receiver 0 (left ear)
ir, err := f.IRAt(0, 0) // ErrUnsupportedDataType for non-FIR files
if err != nil {
    log.Fatal(err)
}
peak, _ := f.IRPeakdB(0, 0)
fmt.Printf("IR samples: %d, peak level: %.1f dB\n", len(ir), peak)

// Access all impulse responses
for m := 0; m < f.M; m++ {
    for r := 0; r < f.R; r++ {
        ir := f.ImpulseResponses[m][r]
        // Process IR data...
    }
}
Streaming large files

Open loads every audio array into memory. For large databases, OpenLazy reads the metadata, dimensions, positions and small per-measurement variables (SamplingRate, Delay, Frequencies) but leaves the audio data in the file, so it can be read one measurement at a time. A lazy File keeps the file open until Close:

f, err := sofa.OpenLazy("large.sofa")
if err != nil {
    log.Fatal(err)
}
defer f.Close() // releases the file handle; later reads fail with fs.ErrClosed

// One measurement: [R][N] impulse responses.
ir, err := f.ReadMeasurement(42)

// Every measurement in order; a non-nil error from the callback stops the
// loop and is returned unchanged.
err = f.RangeMeasurements(func(m int, ir [][]float64) error {
    // process ir[0] (left), ir[1] (right) ...
    return nil
})

The siblings ReadMeasurementTF / RangeMeasurementsTF (TF: real and imaginary parts, [R][N] each), ReadMeasurementTFE (TF-E: [R][E][N]) and ReadMeasurementSOS (SOS: [R][N]) cover the other DataTypes. All of them also work on files read with Open, where they return the loaded slices. On a lazy File the audio fields (ImpulseResponses, TFReal, …) stay nil, so IRAt, IRPeakdB and Save fail with ErrNotLoaded. Indices outside [0, M) fail with ErrIndexOutOfRange, and a method for another DataType with ErrUnsupportedDataType.

Each read is one HDF5 hyperslab covering a whole measurement. For chunked files whose chunks span several measurements (common in files written by the SOFA Toolbox), go-hdf5 keeps recently used chunks decompressed (8 chunks or 16 MiB per variable), so sequential reads decompress each chunk once.

Reading spatial data
// Listener position for first measurement
if len(f.ListenerPositions) > 0 {
    pos := f.ListenerPositions[0]
    fmt.Printf("Listener at (%.2f, %.2f, %.2f) meters\n", pos.X, pos.Y, pos.Z)
}

// Receiver positions (e.g., left and right ear)
for i, recv := range f.ReceiverPositions {
    fmt.Printf("Receiver %d: (%.3f, %.3f, %.3f)\n", i, recv.X, recv.Y, recv.Z)
}

// Source positions for each measurement
for i, src := range f.SourcePositions {
    fmt.Printf("Source %d: (%.2f, %.2f, %.2f)\n", i, src.X, src.Y, src.Z)
}

Position components mean different things in different files, so check the coordinate system before interpreting them. SimpleFreeFieldHRIR stores source positions as spherical (azimuth, elevation, radius), not as (X, Y, Z):

switch strings.ToLower(f.SourcePositionType) {
case sofa.CoordinateSpherical:
    // src.X is azimuth, src.Y elevation, src.Z radius.
    // f.SourcePositionUnits names the angular units, e.g. "degree, degree, metre".
case sofa.CoordinateCartesian:
    // src.X, src.Y, src.Z are metres.
case "":
    // The file omits the Type attribute; fall back to the convention's default.
}

The same …PositionType and …PositionUnits pair exists for the listener, receiver, and emitter datasets. Values are trimmed on read but keep the file's spelling ("Spherical", "meter"), so compare them case-insensitively; Save writes them back unchanged.

Accessing metadata
// AES69 global attributes
fmt.Printf("SOFA Version: %s\n", f.Version)
fmt.Printf("Convention: %s %s\n", f.SOFAConventions, f.SOFAConventionsVersion)
fmt.Printf("Data Type: %s\n", f.DataType)
fmt.Printf("Room Type: %s\n", f.RoomType)
fmt.Printf("Author: %s\n", f.AuthorContact)
fmt.Printf("Organization: %s\n", f.Organization)
fmt.Printf("License: %s\n", f.License)
fmt.Printf("Date Created: %s\n", f.DateCreated)
Transfer-function (TF) files

go-sofa reads and writes both FIR (impulse response, time domain) and TF (transfer function, frequency domain) SOFA files. TF files store a frequency vector and complex transfer functions instead of impulse responses:

f, err := sofa.Open("hrtf.sofa")
if err != nil {
    log.Fatal(err)
}
defer f.Close()

if f.DataType == sofa.DataTypeTF {
    fmt.Printf("Frequencies: %d points (%.1f Hz – %.1f Hz)\n",
        len(f.Frequencies),
        f.Frequencies[0],
        f.Frequencies[len(f.Frequencies)-1])

    // Complex TF for measurement 0, receiver 0
    re := f.TFReal[0][0] // []float64, length N
    im := f.TFImag[0][0] // []float64, length N
    _ = re
    _ = im
}
Writing SOFA files

File.Save(path) writes a *File back out as a netCDF-4/HDF5-based SOFA file. All required AES69 fields and array shapes are validated before any bytes are written, so a failed Save leaves the target path untouched.

Save and WriteTo deflate the audio data at level 4 (sofa.DefaultDeflateLevel), in chunks of one receiver across all measurements, the layout netCDF-C files usually have. MIT KEMAR re-saves in 1.1 MB, against 1.2 MB for the original and 5.9 MB uncompressed. Pass sofa.WithDeflate(level) for another level, or 0 to store the data uncompressed:

err := f.Save("kemar.sofa", sofa.WithDeflate(0)) // uncompressed, contiguous
Creating a file from scratch
package main

import (
    "log"

    "github.com/CWBudde/go-sofa"
)

func main() {
    const M, R, E, N = 1, 2, 1, 64
    f := &sofa.File{
        Conventions:            "SOFA",
        Version:                "1.0",
        SOFAConventions:        "SimpleFreeFieldHRIR",
        SOFAConventionsVersion: "1.0",
        DataType:               sofa.DataTypeFIR,
        Title:                  "Synthetic HRIR",
        M:                      M, R: R, E: E, N: N,
        SamplingRate:         []float64{48000},
        Delay:                []float64{0},
        ListenerPositions:    []sofa.Vector3{{X: 0, Y: 0, Z: 0}},
        ListenerPositionType: sofa.CoordinateCartesian,
        ReceiverPositions: []sofa.Vector3{
            {X: 0, Y: 0.09, Z: 0},  // left ear
            {X: 0, Y: -0.09, Z: 0}, // right ear
        },
        ReceiverPositionType: sofa.CoordinateCartesian,
        // azimuth 0°, elevation 0°, 1 m: straight ahead
        SourcePositions:     []sofa.Vector3{{X: 0, Y: 0, Z: 1}},
        SourcePositionType:  sofa.CoordinateSpherical,
        EmitterPositions:    []sofa.Vector3{{X: 0, Y: 0, Z: 0}},
        EmitterPositionType: sofa.CoordinateCartesian,
    }

    // [M][R][N] impulse responses
    f.ImpulseResponses = make([][][]float64, M)
    for m := range M {
        f.ImpulseResponses[m] = make([][]float64, R)
        for r := range R {
            f.ImpulseResponses[m][r] = make([]float64, N)
            f.ImpulseResponses[m][r][0] = 1.0 // unit impulse at t=0
        }
    }

    if err := f.Save("synthetic_hrir.sofa"); err != nil {
        log.Fatal(err)
    }
}
Round-trip: open, modify, save
f, err := sofa.Open("input.sofa")
if err != nil {
    log.Fatal(err)
}
defer f.Close()

// Update some metadata.
f.Title = "Modified copy"
f.History = f.History + "\nResaved by my-tool"

// Halve every impulse response in place.
for m := range f.M {
    for r := range f.R {
        for n := range f.N {
            f.ImpulseResponses[m][r][n] *= 0.5
        }
    }
}

if err := f.Save("output.sofa"); err != nil {
    log.Fatal(err)
}

A round trip keeps what go-sofa does not interpret. Open collects global attributes without a field of their own in Attributes, variables such as SourceView or RoomCornerA in Variables, and further attributes of the variables Save writes in VariableAttributes; Save writes all three back. They are plain data you can inspect, edit or add to:

for _, a := range f.Attributes {
    fmt.Printf("%s = %v\n", a.Name, a.Value) // e.g. DatabaseName = CIPIC
}
f.Attributes = append(f.Attributes, sofa.Attribute{Name: "ListenerShortName", Value: "subject_003"})
if len(f.Dropped) > 0 {
    log.Printf("not preserved: %v", f.Dropped) // unsupported data or attribute types
}

Command-line Tools

sofainfo

Displays a metadata summary for SOFA files. Similar to PasSofa's SofaReader utility.

Usage:

# One or more files
sofainfo myfile.sofa other.sofa

# All .sofa files in the current directory
sofainfo

With several files each summary is preceded by a ==> file <== header. Errors go to stderr; the exit status is 1 if any file could not be read and 2 on a usage error (sofainfo -h prints the usage).

Example output:

Conventions: SOFA
Version: 0.6
SOFAConventions: SimpleFreeFieldHRIR
SOFAConventionsVersion: 0.4
DataType: FIR
RoomType: free field
DateCreated: 2014-03-20 17:35:22
DateModified: 2014-03-20 17:35:22
APIName: ARI SOFA API for Matlab/Octave
APIVersion: 0.4.0
License: No license provided, ask the author for permission
ApplicationName: Demo of the SOFA API
ApplicationVersion: 0.4.0
History: Converted from the CIPIC file format

Number of Measurements: 1250
Number of Receivers: 2
Number of Emitters: 1
Number of DataSamples: 200
SampleRate: 44100
Delay: 2 values [I,R], min 0, max 0: [0 0]

The Delay line gives the number of values, their netCDF dimensions as stored in the file (File.DelayDimensions: [I], [I,R], [M], [R] or [M,R], so [M] and [R] are told apart even when M == R), the range and, for up to 8 values, the values.

sofa2json

Exports SOFA files to JSON. Enhanced version of PasSofa's SOFA2JSON utility.

Usage:

# Export metadata only (default)
sofa2json myfile.sofa other.sofa

# Include impulse response data (FIR files)
sofa2json --include-ir myfile.sofa

# Include complex transfer-function data (TF / TF-E files)
sofa2json --include-tf myfile.sofa

# Include second-order-section coefficients (SOS files)
sofa2json --include-sos myfile.sofa

# Overwrite existing .json files
sofa2json -f myfile.sofa

# All .sofa files in the current directory
sofa2json --include-ir

Output: Creates <filename>.json next to each input and refuses to overwrite an existing one unless -f is given. Progress and errors go to stderr; the exit status is 1 if any file failed and 2 on a usage error.

JSON keys are the field names of sofa.File: the global attributes (Conventions, Version, SOFAConventions, SOFAConventionsVersion, DataType, Title, …), the dimensions M, R, E, N, SamplingRate, Delay, the positions (ListenerPositions, ReceiverPositions, SourcePositions, EmitterPositions, ListenerView, ListenerUp) as [x, y, z] triples together with their …Type and …Units, and — when requested — ImpulseResponses, TFReal/TFImag (TF-E: TFRealE/TFImagE) or SOSCoefficients. For TF and TF-E files Frequencies is always included (it is small). NaN and ±Inf values are written as null. The output is streamed, so large exports need no in-memory copy of the JSON document.

sofaprobe

Development tool: dumps the HDF5 structure, attributes and dimension scales of each file, then previews Data.IR, Data.Real, Data.Imag or Data.SOS (shape, first and last values) without reading the whole dataset (just the first and last three values). Attribute values go-hdf5 cannot decode are shown inline as (unreadable: …); failures to read the structure or the data go to stderr and make the exit status 1.

sofaprobe myfile.sofa

API Reference

Types
File

Holds the contents of a SOFA file — attributes, positions and audio data — fully loaded by Open (which closes the file before returning) or built in memory for Save; such a File holds no open file handle. A File from OpenLazy leaves the audio arrays in the file and holds it open until Close.

Fields:

  • M, R, E, N int — Dimensions (measurements, receivers, emitters, samples)
  • ImpulseResponses [][][]float64 — The actual IR data [M][R][N]
  • SamplingRate []float64 — Sampling rate in Hz (may be per-measurement)
  • Delay []float64 — Delay in samples per measurement
  • ListenerPositions []Vector3 — Listener positions [M]
  • ReceiverPositions []Vector3 — Receiver positions [R]
  • SourcePositions []Vector3 — Source positions [M]
  • EmitterPositions []Vector3 — Emitter positions [E]
  • SourcePositionType, SourcePositionUnits string — Coordinate system of SourcePositions, from the dataset's Type and Units attributes; empty when the file omits them. Same pair for ListenerPosition…, ReceiverPosition…, and EmitterPosition…
  • ListenerUp, ListenerView Vector3 — Listener orientation vectors
  • Frequencies []float64 — Frequency vector [N] (TF files only)
  • TFReal, TFImag [][][]float64 — Complex transfer functions [M][R][N] (TF files only)
  • Title, DataType, RoomType, License, ... — AES69 metadata attributes
  • Attributes []Attribute — Global attributes without a field of their own (sorted by name)
  • Variables []Variable — Variables go-sofa does not interpret: Name, Dims, Shape, and row-major Values (numeric, as float64) or Chars (char arrays), plus their Attributes
  • VariableAttributes map[string][]Attribute — Further attributes of the variables Save writes, by variable name
  • Dropped []string — What Open could not preserve

Methods:

  • Open(path string) (*File, error) — Reads a SOFA file completely and closes it again
  • OpenLazy(path string) (*File, error) — Reads everything but the audio arrays and keeps the file open for ReadMeasurement and friends
  • OpenReader(r io.ReaderAt, size int64) (*File, error), OpenLazyReader(r io.ReaderAt, size int64) (*File, error) — Open and OpenLazy for a file in memory or any other io.ReaderAt
  • Close() error — Releases the file handle of a File from OpenLazy (idempotent); does nothing and returns nil for any other File
  • ReadMeasurement(m int) ([][]float64, error) — FIR impulse responses [R][N] of measurement m; ReadMeasurementTF (TF), ReadMeasurementTFE (TF-E, [R][E][N]) and ReadMeasurementSOS (SOS) are the siblings for the other DataTypes
  • RangeMeasurements(fn func(m int, ir [][]float64) error) error — Calls fn for every measurement in order, stopping at the first error; RangeMeasurementsTF for TF
  • Save(path string, opts ...SaveOption) error — Validates the File and writes it to disk as a SOFA file; the audio data is deflated at level 4, WithDeflate(level) sets another level (0: uncompressed)
  • WriteTo(w io.Writer) (int64, error) — Validates the File and writes the bytes Save would write to w (io.WriterTo)
  • SamplingRateScalar() (float64, error) — Returns the single sampling rate; ErrNoSamplingRate when none is stored, ErrVaryingSamplingRate when the per-measurement rates differ
  • SamplingRateAt(m int) (float64, error) — Sampling rate of measurement m ([I] broadcast or [M])
  • SourcePositionAt(m int) (Vector3, error) — Source position of measurement m ([I,C] broadcast or [M,C])
  • DelayAt(m, r int) (float64, error) — Delay in samples of measurement m, receiver r, for Data.Delay stored as [I], [I,R], [R], [M] or [M,R]; 0 when the file has no delay
  • Duration() (float64, error) — Returns IR duration in seconds (FIR only)
  • IRAt(m, r int) ([]float64, error) — Returns impulse response for measurement m, receiver r
  • IRPeakdB(m, r int) (float64, error) — Returns peak level in dB for measurement m, receiver r

The IR accessors fail with ErrUnsupportedDataType on non-FIR files, and all accessors fail with ErrIndexOutOfRange for indices outside the file's dimensions; Open fails with ErrUnsupportedDataType for an empty, unknown, FIR-E or FIRE DataType, and with ErrNotSOFA when the Conventions attribute is not SOFA. Test for these with errors.Is.

Save returns every validation failure as a *ValidationError. Its Field names the File field at fault ("M", "ImpulseResponses", "SourcePositionType", "Variables", …), and its message starts with that field:

var ve *sofa.ValidationError
if errors.As(err, &ve) {
    fmt.Printf("fix %s: %v\n", ve.Field, ve.Err)
}

DataTypeFIR, DataTypeTF, DataTypeTFE and DataTypeSOS are the DataType values the package reads and writes.

Vector3

One coordinate triplet of a position or orientation. Its units are those the variable's Type and Units attributes name: metres for cartesian; azimuth, elevation (degrees; Save rejects other angle units) and radius in metres for spherical and spherical harmonics.

Fields:

  • X, Y, Z float64 — the three coordinates
Functions
Open(path string) (*File, error)

Reads a SOFA file. Checks that it is a SOFA file (Conventions == "SOFA", else ErrNotSOFA), reads all data and metadata into the returned File and closes the file before returning.

Returns:

  • *File — The opened SOFA file
  • error — Error if file cannot be opened or is not a valid SOFA file

Example:

f, err := sofa.Open("myfile.sofa")
if err != nil {
    log.Fatal(err)
}
defer f.Close()
OpenLazy(path string) (*File, error)

Like Open, but leaves the audio arrays in the file and keeps it open until Close. It checks the shape and datatype of the audio arrays, so it rejects the files Open rejects. Read audio data with ReadMeasurement, ReadMeasurementTF, ReadMeasurementTFE, ReadMeasurementSOS, RangeMeasurements or RangeMeasurementsTF; see Streaming large files.

OpenReader(r io.ReaderAt, size int64) (*File, error)

Like Open, for a SOFA file of size bytes read from r, such as a bytes.Reader over a downloaded or embedded file. r is not used after OpenReader returns and is never closed. OpenLazyReader is the OpenLazy counterpart: it reads audio data from r until Close.

f, err := sofa.OpenReader(bytes.NewReader(data), int64(len(data)))
(*File).Save(path string, opts ...SaveOption) error

Validates the File against AES69 requirements (required attributes, positive dimensions, consistent array shapes) and its values (every number finite, sampling rates above zero, frequencies ascending from zero or above, all for the active DataType only; per-measurement ListenerViews/ListenerUps non-zero), and writes it as a new SOFA file at path. An unset ListenerView/ListenerUp is written as the conventions' default, [1 0 0]/[0 0 1] (spherical: (0, 0, 1) / (0, 90, 1)). Only ListenerView carries their Type and Units, as the convention tables define them there alone. ReceiverPositions and SourcePositions are required; an empty ListenerPositions or EmitterPositions is written as the conventions' default, [0 0 0] cartesian in metres. Every position needs a Type; empty Units are written as the default for it, metre for cartesian and degree, degree, metre otherwise. Units may name only metre and degree (also meter, metres, meters, degrees; any case), so radians are rejected. Global attributes a convention makes mandatory beyond the generic ones are written empty when Attributes lacks them:

  • SimpleFreeFieldHRIR/HRTF/HRSOS (and the legacy SimpleFreeFieldSOS) and FreeFieldHRTF: DatabaseName, ListenerShortName
  • FreeFieldDirectivityTF: DatabaseName, SourceType, SourceManufacturer
  • SimpleHeadphoneIR: DatabaseName, ListenerShortName, ReceiverDescription, EmitterDescription
  • SingleRoomSRIR: DatabaseName
  • SingleRoomDRIR: RoomDescription, DatabaseName

SingleRoomSRIR, SingleRoomDRIR and FreeFieldDirectivityTF also make the SourceView and SourceUp variables mandatory. When Variables has none of that name, Save writes the table's default as [I,C]: SourceView [1 0 0] (SingleRoomDRIR: [-1 0 0]), cartesian in metres, and SourceUp [0 0 1] (without Type or Units, which the tables do not define on it), plus any attributes VariableAttributes holds for that name (such as a Reference). FreeFieldDirectivityTF also makes a Reference attribute (a narrative description of the spatial reference) mandatory on SourcePosition, SourceView and SourceUp: Save writes it as "" where neither VariableAttributes nor the variable's own Attributes set one. An empty RoomType is written as the convention's default: reverberant for SingleRoomDRIR, shoebox for SingleRoomSRIR when Variables holds both RoomCornerA and RoomCornerB (sofar requires them for a shoebox room), and free field otherwise. The File itself is not changed.

Like the SOFA Toolbox's SOFAsave, Save records its own provenance: APIName go-sofa, go-sofa's module version as APIVersion, and the save time as DateModified (and as DateCreated when that is empty). When the File names another API or go-sofa version, History gains a line resaved by go-sofa <version> from <APIName> <APIVersion>. Record your own program in ApplicationName/ApplicationVersion. The save time is the current time, or SOURCE_DATE_EPOCH (seconds since the Unix epoch) when that environment variable is set: then saving the same File twice gives byte-identical files. A malformed SOURCE_DATE_EPOCH, the empty string included, is an error.

The destination is created from scratch on each call; an existing file is overwritten only after validation succeeds. Works for every supported DataType (FIR, TF, TF-E, SOS).

Returns:

  • error — a *ValidationError when the File is invalid, else an I/O error from the underlying HDF5 writer.

Example:

if err := f.Save("output.sofa"); err != nil {
    log.Fatal(err)
}
(*File).WriteTo(w io.Writer) (int64, error)

Validates the File like Save and writes the same bytes to w, returning how many w accepted. The file is assembled in memory and written in one call once complete, so a validation or encoding error writes nothing to w. Save stays the way to write a file on disk: it replaces the destination atomically.

var buf bytes.Buffer
if _, err := f.WriteTo(&buf); err != nil {
    log.Fatal(err)
}

File Format Support

This library supports SOFA files (AES69-2015) based on HDF5 with netCDF-4 conventions:

  • Conventions: SimpleFreeFieldHRIR, SimpleFreeFieldHRTF, SimpleFreeFieldHRSH, SimpleFreeFieldSOS, GeneralTF, GeneralTF-E, SingleRoomDRIR, etc. (free-form — any AES69 convention name is accepted)
  • DataTypes: FIR, TF, TF-E, SOS
  • Storage formats: Contiguous and chunked datasets
  • Compression: Deflate-compressed datasets
  • Dimensions: Standard M, R, E, N dimensions and dimension scales
  • Attributes: Dense (fractal heap) and compact attribute storage
Conventions

Any AES69 convention name is accepted and written unchanged. The official conventions get extra behaviour: Save enforces their DataType, layout and required metadata, and (*File).ConventionWarnings() []string reports advisory findings that never block Save (sofainfo prints them). Besides the per-convention warnings in the table below, it reports a SOFAConventionsVersion that the SOFA Toolbox and pyfar tables do not list for an official convention (custom conventions are not checked), and, for any convention, SOFA 2.x features in a file whose Version is below 2.0: the FreeFieldHRTF convention, DataType TF-E and a position Type of "spherical harmonics".

Convention Accessors Checks
BRIR: SingleRoomDRIR IsBRIR() Save requires DataType FIR and errors with a zero ListenerView/ListenerUp; an empty RoomType is written as reverberant
SRIR: SingleRoomSRIR IsSRIR(), AmbisonicsOrder() (int, bool) Save requires DataType FIR. Warns when RoomVolume or RoomTemperature is missing, or when R is not (order+1)²
GeneralFIR/GeneralTF/GeneralTF-E — Save requires DataType FIR/TF/TF-E
SimpleFreeFieldHRIR/HRTF/HRSOS — Save requires DataType FIR/TF/SOS and E = 1; the legacy SimpleFreeFieldSOS is checked as SimpleFreeFieldHRSOS. Warns when R is not 2 (libmysofa rejects it) or RoomType is not free field (sofar rejects it)
FreeFieldHRTF SHOrder() for SH-encoded files Save requires DataType TF-E. Warns when RoomType is not free field (sofar rejects it)
SimpleHeadphoneIR — Save requires DataType FIR
Directivity: e.g. FreeFieldDirectivityTF IsDirectivity() Save requires DataType TF and writes the mandatory Reference attributes; more needs an example file. M indexes source orientation

RoomVolume (cubic metres) and RoomTemperature (kelvin) are read from their variables, or from root attributes of the same name, and written as variables when non-zero.

Spherical-harmonic (SH) HRTFs

AES69-2022 introduced spherical-harmonic representations such as SimpleFreeFieldHRSH. These are stored using the existing TF-E DataType with the emitter dimension E repurposed as the SH coefficient index (E = (Lmax+1)²). go-sofa reads and writes such files via the standard TF-E path; use the helpers below to detect and inspect SH encoding:

  • (*File).IsSHEncoded() bool — true when DataType is TF-E, EmitterPositionType is "spherical harmonics" (AES69's marker; only when it is empty do a convention name containing "SH" or a History mentioning spherical harmonics count instead) and E is (L+1)² for some L ≥ 0
  • (*File).SHOrder() (lmax int, ok bool) — returns Lmax
  • (*File).SHCoefficientCount() int — returns E for SH files, 0 otherwise
  • (*File).SHWarnings() []string — advisory diagnostics for ambiguous or malformed SH metadata

To write an SH-encoded file, populate a File with DataType:"TF-E", EmitterPositionType: sofa.CoordinateSphericalHarmonics, a convention such as FreeFieldHRTF, and E = (Lmax+1)² SH coefficients per (measurement, receiver, frequency) tuple, then call Save.

Limitations
  • FIR-E is not supported. Open and Save fail with ErrUnsupportedDataType for DataType FIR-E (per-emitter impulse responses, GeneralFIR-E) and the legacy FIRE. Files of the conventions built on it, such as MultiSpeakerBRIR and SingleRoomMIMOSRIR, can therefore be neither read nor written, and IsBRIR/IsSRIR do not match them.

Interoperability

Files written by Save are netCDF-4/HDF5 and open in h5py, netCDF4 (netCDF-C), ncdump and the SOFA Toolbox. libmysofa, the C reader used by ffmpeg's sofalizer filter, loads the FIR and SOS files go-sofa writes but no transfer-function (TF, TF-E) files, and its check passes only SimpleFreeFieldHRIR FIR files. Results with libmysofa 3f4cb66, checked in CI for every DataType go-sofa writes and for re-saved reference files:

Written file mysofa_load mysofa_check
SimpleFreeFieldHRIR, DataType FIR OK OK
Other FIR and SOS files (e.g. SRIR, DRIR, SimpleFreeFieldHRSOS) OK MYSOFA_INVALID_ATTRIBUTES: it accepts only SimpleFreeFieldHRIR with FIR
TF and TF-E files MYSOFA_INVALID_FORMAT: it has no transfer-function reader —

The last two rows are not go-sofa limitations: libmysofa gives the same result for the originals written by netCDF-C, and CI requires every re-saved file to match its original's result. See Cross-validation for how the check runs.

Development

Building
# Install dependencies
go mod download

# Build library
go build

# Build CLI tools
go build ./cmd/sofainfo
go build ./cmd/sofa2json
Testing
# Run tests
go test -v ./...

# Run tests with coverage
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
Using just

This project includes a justfile for common development tasks:

# Run all checks (format, lint, test, tidy)
just check

# Format code
just fmt

# Run linter
just lint

# Run tests
just test

# Run tests with coverage
just test-coverage

# Build CLI tools
just build
Cross-validation

go-sofa's output is checked against independent SOFA implementations:

  • h5py and netCDF4 (netCDF-C): just interop (also run in CI) writes one file per DataType with Save and compares what h5py and netCDF4 read with the expected values. It also re-saves every file in testdata/sofar/ with go-sofa and checks that h5py and netCDF4 read the same attributes and values as in the original.

  • libmysofa: just interop then loads all these files, plus a re-save of testdata/MIT_KEMAR_normal_pinna.sofa, with libmysofa's mysofa_load and mysofa_check (internal/interop/mysofa). Generated SimpleFreeFieldHRIR files must pass both, TF and TF-E files must get exactly MYSOFA_INVALID_FORMAT, everything else must load, and every re-saved file must get the same result as its original. just libmysofa builds the loader (scripts/libmysofa) at a pinned commit and caches it in ~/.cache/go-sofa; it needs git, a C compiler and zlib, and just fetch-testdata for the KEMAR file.

  • sofar (pyfar): testdata/sofar/ holds small synthetic files written by sofar through netCDF-C, one per convention that CI cannot otherwise fetch (GeneralTF 2.0, GeneralTF-E, FreeFieldHRTF with and without spherical harmonics, SimpleFreeFieldHRSOS, SingleRoomSRIR, SingleRoomDRIR). sofa_sofar_fixtures_test.go pins their values and round-trips them through Save. Regenerate them with pip install sofar==1.3.0 && python3 scripts/make_sofar_fixtures.py.

  • SOFA Toolbox (MATLAB / GNU Octave): scripts/matlab/roundtrip.m loads a go-sofa-written file with SOFAload, writes it back with SOFAsave, and writes a second file from scratch with the toolbox; internal/interop/toolbox then checks that Data.IR, SourcePosition and ListenerPosition are bit-for-bit identical in both directions:

    # GNU Octave (Ubuntu): apt-get install octave octave-netcdf
    git clone --depth 1 https://github.com/sofacoustics/SOFAtoolbox /tmp/SOFAtoolbox
    export SOFATOOLBOX=/tmp/SOFAtoolbox/SOFAtoolbox
    d=$(mktemp -d)
    
    go run ./internal/interop/toolbox write "$d/gosofa.sofa"
    octave --no-gui --quiet --path scripts/matlab \
      --eval "roundtrip('$d/gosofa.sofa', '$d/toolbox.sofa', '$d/created.sofa')"
    
    # go-sofa -> toolbox -> go-sofa, and toolbox -> go-sofa
    go run ./internal/interop/toolbox compare "$d/gosofa.sofa" "$d/toolbox.sofa"
    go run ./internal/interop/toolbox check-created "$d/created.sofa"
    

    In MATLAB, run roundtrip(...) from scripts/matlab with the toolbox on the path (or SOFATOOLBOX set) instead of the octave line. Last run 2026-09-26 with GNU Octave 8.4.0 and SOFA Toolbox 2.6.0 (d2a83b3) on the go-hdf5#5 writer (scalar string attributes): both comparisons bit-exact.

License

See LICENSE file for details.

Contributing

Contributions are welcome! Please ensure that:

  1. All tests pass (just test)
  2. Code is formatted (just fmt)
  3. Linter is clean (just lint)
  4. go.mod is tidy (just check-tidy)

References

Documentation

Overview

Package sofa provides reading and writing support for SOFA files (Spatially Oriented Format for Acoustics, AES69).

SOFA is a file format for storing spatially oriented acoustic data like head-related transfer functions (HRTFs), binaural room impulse responses (BRIRs), and directional room impulse responses (DRIRs). The format is based on HDF5 and follows the netCDF-4 conventions.

See https://www.sofaconventions.org/ for specifications and documentation.

Index

Constants

View Source
const (
	DataTypeFIR = "FIR"  // time-domain impulse responses
	DataTypeTF  = "TF"   // complex frequency-domain transfer functions
	DataTypeTFE = "TF-E" // TF with active emitter dimension ([M][R][E][N]); also carries SH-encoded HRTFs with E as SH coefficient index
	DataTypeSOS = "SOS"  // second-order section (biquad) filter coefficients
)

DataType values defined by the AES69 specification: the values of File.DataType this package reads and writes.

View Source
const (

	// Coordinate systems a position dataset's Type attribute may name.
	CoordinateCartesian = "cartesian"
	CoordinateSpherical = "spherical"
	// CoordinateSphericalHarmonics marks EmitterPosition data of SH-encoded
	// files, where each emitter is one SH coefficient (see SHOrder).
	CoordinateSphericalHarmonics = "spherical harmonics"

	// UnitsSphericalDegrees is the conventional Units value for spherical
	// positions measured in degrees.
	UnitsSphericalDegrees = "degree, degree, metre"

	// UnitsCartesianMetres is the conventional Units value for cartesian
	// positions, as the SOFA convention tables give it. Files may also say
	// "metre, metre, metre"; Open reads the Units attribute as it is.
	UnitsCartesianMetres = "metre"
)

SOFA file format constants.

View Source
const DefaultDeflateLevel = 4

DefaultDeflateLevel is the deflate level Save and WriteTo compress the audio data with unless WithDeflate sets another.

Variables

View Source
var ErrIndexOutOfRange = errors.New("index out of range")

ErrIndexOutOfRange reports a measurement or receiver index outside the file's dimensions, or one for which no data is stored. Test for it with errors.Is.

View Source
var ErrNoSamplingRate = errors.New("no sampling rate stored")

ErrNoSamplingRate reports a file without Data.SamplingRate, as TF files have. Test for it with errors.Is.

View Source
var ErrNotLoaded = errors.New("audio data not loaded (file opened with OpenLazy)")

ErrNotLoaded reports an accessor such as IRAt that needs the whole audio array in memory, called on a File returned by OpenLazy. Read such a File with ReadMeasurement and its siblings, or open it with Open. Test for it with errors.Is.

View Source
var ErrNotSOFA = errors.New("not a SOFA file")

ErrNotSOFA reports that Open read an HDF5 file whose Conventions attribute is not "SOFA". Test for it with errors.Is.

View Source
var ErrTooLarge = errors.New("file declares too much data")

ErrTooLarge reports that Open read a file whose variables together declare more data than Open reads for a file of its size; see Open. Test for it with errors.Is.

View Source
var ErrUnsupportedDataType = errors.New("unsupported DataType")

ErrUnsupportedDataType reports a DataType this package cannot read or write: an empty or unknown value, GeneralFIR-E's "FIR-E" and the legacy "FIRE". Test for it with errors.Is.

View Source
var ErrVaryingSamplingRate = errors.New("sampling rate varies across measurements")

ErrVaryingSamplingRate reports that a file stores different sampling rates per measurement where a single rate was asked for. Test for it with errors.Is.

Functions

This section is empty.

Types

type Attribute

type Attribute struct {
	Name  string
	Value any
}

Attribute is a netCDF attribute go-sofa does not map to a File field. Value is a string, a numeric scalar (int8 … uint64, float32, float64) or a non-empty slice of one numeric type. Open reads empty (null) attributes as "".

type File

type File struct {
	// Dimensions (M=measurements, R=receivers, E=emitters, N=samples)
	M int // number of measurements
	R int // number of receivers (e.g., 2 for binaural)
	E int // number of emitters (typically 1; for SH-encoded HRTFs this is the SH coefficient index, with E = (Lmax+1)² — see (*File).SHOrder)
	N int // number of samples per impulse response

	// Spatial data. Save requires ReceiverPositions and SourcePositions and
	// writes an empty ListenerPositions or EmitterPositions as the
	// conventions' default, [0 0 0] cartesian in metres.
	ListenerPositions []Vector3 // [M] listener positions for each measurement
	ListenerUp        Vector3   // listener's up vector
	ListenerView      Vector3   // listener's view direction
	ReceiverPositions []Vector3 // [R] receiver positions (e.g., left/right ear)
	SourcePositions   []Vector3 // [M] source positions for each measurement
	EmitterPositions  []Vector3 // [E] emitter positions

	// Measurement-dependent layouts, filled by Open only when a file stores
	// them: ReceiverPosition [R,C,M] and EmitterPosition [E,C,M] as [M][R]
	// and [M][E], ListenerView and ListenerUp [M,C] as [M]. The fields above
	// then hold measurement 0. When set, Save writes these fields in the
	// same layouts instead of the singular ones.
	ReceiverPositionsM [][]Vector3
	EmitterPositionsM  [][]Vector3
	ListenerViews      []Vector3
	ListenerUps        []Vector3

	// Coordinate system of each position dataset, from its Type and Units
	// attributes. Type is "cartesian" or "spherical"; for spherical data the
	// components are (azimuth, elevation, radius) and Units names their units,
	// conventionally "degree, degree, metre". Both are stored trimmed but in
	// the file's case (compare them with strings.EqualFold), and are empty
	// when the file omits the attribute — absence is
	// distinguishable from a value, because a reader that must know the
	// coordinate system should say so rather than guess.
	//
	// Save requires a Type ("cartesian", "spherical" or "spherical
	// harmonics") on every position it writes, and writes empty Units as the
	// conventions' default for the Type: UnitsCartesianMetres for cartesian,
	// UnitsSphericalDegrees otherwise. Units, here and on ListenerView, may
	// name only metre and degree (also meter, metres, meters, degrees; any
	// case), comma-separated.
	ListenerPositionType  string
	ListenerPositionUnits string
	ReceiverPositionType  string
	ReceiverPositionUnits string
	SourcePositionType    string
	SourcePositionUnits   string
	EmitterPositionType   string
	EmitterPositionUnits  string

	// Coordinate system of ListenerView and ListenerUp, from ListenerView's
	// Type and Units attributes. Save writes "cartesian" and "metre" when
	// they are empty.
	ListenerViewType  string
	ListenerViewUnits string

	// Audio data — FIR (used when DataType == "FIR")
	ImpulseResponses [][][]float64 // [M][R][N] the actual IR data
	SamplingRate     []float64     // [M] sampling rate in Hz (may be scalar)
	Delay            []float64     // delay in samples: 1 (shared), M, R or M×R (row-major [M][R]) values; see DelayAt

	// Audio data — TF (used when DataType == "TF")
	// Frequencies has length N. TFReal and TFImag have shape [M][R][N] and
	// together encode the complex transfer function per measurement/receiver.
	Frequencies []float64     // [N] frequency vector, Hz
	TFReal      [][][]float64 // [M][R][N] real part of complex TF
	TFImag      [][][]float64 // [M][R][N] imaginary part of complex TF

	// Audio data — TF-E (used when DataType == "TF-E")
	// Same Frequencies vector as TF, but with an active emitter dimension.
	TFRealE [][][][]float64 // [M][R][E][N] real part of complex TF
	TFImagE [][][][]float64 // [M][R][E][N] imaginary part of complex TF

	// Audio data — SOS (used when DataType == "SOS")
	// Second-order-section filter coefficients. Storage shape is [M][R][N]
	// where N is 6 × (number of biquad sections); each biquad contributes
	// six coefficients (b0, b1, b2, a0, a1, a2). SamplingRate and Delay
	// (above) carry their FIR-style meaning.
	SOSCoefficients [][][]float64 // [M][R][N]

	// AES69 Metadata (global attributes)
	Conventions            string  // "SOFA" for SOFA files
	Version                string  // SOFA version (e.g., "1.0")
	SOFAConventions        string  // specific convention (e.g., "SimpleFreeFieldHRIR", "SimpleFreeFieldHRSH" for SH-encoded HRTFs)
	SOFAConventionsVersion string  // convention version
	DataType               string  // data type (e.g., "FIR")
	RoomType               string  // room type if applicable
	RoomVolume             float64 // room volume in cubic metres; 0 when absent
	RoomTemperature        float64 // room temperature in kelvin; 0 when absent
	Title                  string  // descriptive title
	DateCreated            string  // ISO 8601 date
	DateModified           string  // ISO 8601 date
	APIName                string  // API used to create the file
	APIVersion             string  // API version
	AuthorContact          string  // author contact information
	Organization           string  // organization
	License                string  // license information
	ApplicationName        string  // application name
	ApplicationVersion     string  // application version
	Comment                string  // additional comments
	History                string  // processing history
	References             string  // references
	Origin                 string  // origin of the data

	// Content go-sofa does not interpret, kept so that Open followed by
	// Save loses nothing. Open fills these fields; callers may edit them,
	// and Save writes them back.
	//
	// Attributes holds the global attributes that have no field above
	// (DatabaseName, ListenerShortName, …). Variables holds the variables
	// Save would not write otherwise (SourceView, RoomCornerA, char arrays
	// such as ReceiverDescriptions, …). Open sorts both by name, since HDF5
	// does not keep the order of attributes; Save writes them in slice
	// order. VariableAttributes holds, per variable name, the attributes of
	// the variables and dimensions Save does write beyond the ones it sets
	// itself (Type/Units, Data.SamplingRate:Units, N:LongName/Units for TF).
	// Dropped lists what Open could not keep (an unsupported data or
	// attribute type), so that a lossy round trip is never silent.
	Attributes         []Attribute
	Variables          []Variable
	VariableAttributes map[string][]Attribute
	Dropped            []string
	// contains filtered or unexported fields
}

File holds the contents of a SOFA file: its AES69 attributes, positions and audio data. Open fills it completely, OpenLazy fills everything but the audio data (read it with ReadMeasurement and its siblings), and Save writes one built or modified in memory.

func Open

func Open(path string) (*File, error)

Open reads a SOFA file. It checks that the file is a SOFA file, reads all data and metadata into the returned File and closes the file again before it returns, so the File holds no open handle. A failure to close the file is returned too, joined with any read error, and yields no File. Use OpenLazy to leave the audio data in the file and read it one measurement at a time, and OpenReader to read a file held in memory.

Before reading any data, Open refuses a file whose variables together declare more than 64 Mi elements or eight elements per byte of the file, whichever is more, or one variable declaring more than 2^30 elements, with an error wrapping ErrTooLarge: a file of a few kilobytes can declare gigabytes of never-written data. OpenLazy does not count the audio variables it leaves in the file.

func OpenLazy

func OpenLazy(path string) (*File, error)

OpenLazy reads a SOFA file like Open, except for the audio variables (Data.IR, Data.Real and Data.Imag, Data.SOS): it checks their shapes but leaves their values in the file, so the returned File's ImpulseResponses, TFReal, TFImag, TFRealE, TFImagE and SOSCoefficients are nil. Dimensions, positions, attributes, SamplingRate, Delay, Frequencies and the extra variables are loaded as by Open.

Read the audio data one measurement at a time with ReadMeasurement, ReadMeasurementTF, ReadMeasurementTFE, ReadMeasurementSOS or the Range methods. Accessors that need the whole array in memory (IRAt, IRPeakdB) fail with ErrNotLoaded, and so does Save while the audio fields are empty.

The File keeps the file open until Close, which the caller must call; after it, reading audio data fails with fs.ErrClosed. Reads on one File are serialised, so it may be shared between goroutines.

How much memory OpenLazy saves depends on the file's storage layout. A chunked, compressed file is decompressed one chunk at a time, and typical HRTF files store one chunk per receiver across all measurements (CIPIC: Data.IR [1250,2,200] in chunks of [1250,1,200]). There the first ReadMeasurement decompresses every receiver's chunk and keeps it cached, so OpenLazy saves memory over Open only for files chunked along M or stored contiguously. When chunks span several measurements, the cache holds the chunks one measurement spans (up to 256 MiB per audio variable), so reading every measurement decompresses each chunk once.

func OpenLazyReader

func OpenLazyReader(r io.ReaderAt, size int64) (*File, error)

OpenLazyReader is OpenLazy for a SOFA file of size bytes read from r, such as a bytes.Reader or an *os.File opened elsewhere. The File reads audio data from r until Close, so r must stay usable until then; Close does not close r.

func OpenReader

func OpenReader(r io.ReaderAt, size int64) (*File, error)

OpenReader reads a SOFA file of size bytes from r, such as a bytes.Reader over a file held in memory or an embedded asset, like Open reads one from disk: it reads all data and metadata into the returned File, and r is not used after OpenReader returns. Reads stay within the first size bytes of r. OpenReader never closes r.

func (*File) AmbisonicsOrder

func (f *File) AmbisonicsOrder() (order int, ok bool)

AmbisonicsOrder returns the Ambisonics order of an SRIR file, detected from its receiver count R = (order+1)². ok is false for files that are not SRIR, and for SRIR files whose R is not such a square, which usually means the receivers are the raw capsules of a microphone array.

func (*File) Close

func (f *File) Close() error

Close releases the file handle of a File returned by OpenLazy; after it, the File's metadata stays usable but reading audio data fails with fs.ErrClosed. Closing again does nothing and returns nil. For any other File, Close does nothing and returns nil: Open already closes the file it reads, and the File holds all data in memory.

func (*File) ConventionWarnings

func (f *File) ConventionWarnings() []string

ConventionWarnings returns advisory messages about the file's conformance: SOFA 2.x features (the FreeFieldHRTF convention, DataType TF-E, spherical-harmonics positions) in a file whose Version is below 2.0, a SOFAConventionsVersion its registered convention does not know (custom conventions are not checked), a SimpleFreeField* file with other than two receivers (libmysofa rejects it), a free-field HRTF file whose RoomType is not free field (sofar rejects it), and the checks of the convention's own rules, such as missing optional room metadata. Unlike validation errors they never stop Save; callers should surface them to users.

func (*File) DelayAt

func (f *File) DelayAt(m, r int) (float64, error)

DelayAt returns the delay in samples of measurement m, receiver r, broadcasting Data.Delay stored as [I], [I,R], [R], [M] or [M,R]. A file without Data.Delay has no delay, so DelayAt returns 0. It fails with ErrIndexOutOfRange when m or r is outside [0,M)×[0,R) or the stored delay does not fit its layout.

func (*File) DelayDimensions

func (f *File) DelayDimensions() []string

DelayDimensions returns the netCDF dimension names of Delay, such as ["I", "R"] or ["M"]: the ones Open read from the file, which tell [M] from [R] when M == R, or, for a File built in memory or a Delay changed since, the layout its length implies ([I], [M,R], [M] or [R], checked in that order). It returns nil for an empty Delay.

func (*File) Duration

func (f *File) Duration() (float64, error)

Duration returns the length of the impulse responses in seconds, N divided by the (first) sampling rate. It fails with ErrUnsupportedDataType for non-FIR files, where N counts frequency bins or filter coefficients, and when no positive sampling rate is stored.

func (*File) IRAt

func (f *File) IRAt(m, r int) ([]float64, error)

IRAt returns the impulse response for measurement m, receiver r. It fails with ErrUnsupportedDataType for non-FIR files and with ErrIndexOutOfRange when m or r is outside [0,M)×[0,R) or no complete response (N samples) is stored there. On a File returned by OpenLazy, whose responses stay in the file, it fails with ErrNotLoaded (use ReadMeasurement there) unless the caller has filled ImpulseResponses.

func (*File) IRPeakdB

func (f *File) IRPeakdB(m, r int) (float64, error)

IRPeakdB returns the peak level in dB (relative to 1.0) for measurement m, receiver r; a silent response yields -Inf. Errors are those of IRAt.

func (*File) IsBRIR

func (f *File) IsBRIR() bool

IsBRIR reports whether the file holds binaural room impulse responses, that is, whether SOFAConventions is SingleRoomDRIR. Save requires such files to hold FIR data and to carry a non-zero ListenerView and ListenerUp; an empty RoomType is written as reverberant. MultiSpeakerBRIR files use DataType FIRE, which is not supported.

func (*File) IsDirectivity

func (f *File) IsDirectivity() bool

IsDirectivity reports whether the file holds source directivities, that is, whether SOFAConventions names a Directivity convention such as FreeFieldDirectivityTF.

In these files the measurement dimension M indexes the orientation of the source being characterised, not a source position around a listener as in HRTF sets. Save only checks that FreeFieldDirectivityTF files hold TF data, and writes the conventions' default SourceView and SourceUp when Variables lacks them and an empty Reference attribute on SourcePosition, SourceView and SourceUp when none is set; further rules wait for an example file to check them against.

func (*File) IsSHEncoded

func (f *File) IsSHEncoded() bool

IsSHEncoded reports whether this file represents spherical-harmonic (SH) encoded HRTF/transfer-function data. SH SOFA files reuse the TF-E DataType and store one SH coefficient per emitter (E = (Lmax+1)²); AES69 marks them with EmitterPosition:Type = "spherical harmonics". See SHOrder for the detection rule.

func (*File) IsSRIR

func (f *File) IsSRIR() bool

IsSRIR reports whether the file holds spatial room impulse responses, that is, whether SOFAConventions is SingleRoomSRIR. SingleRoomMIMOSRIR files use DataType FIR-E, which is not supported.

func (*File) RangeMeasurements

func (f *File) RangeMeasurements(fn func(m int, ir [][]float64) error) error

RangeMeasurements calls fn with the impulse responses ([R][N], as from ReadMeasurement) of each measurement in order. It stops at the first error, from reading or from fn, and returns it; an error of fn is returned unwrapped. For a File returned by OpenLazy only one measurement is held in memory at a time, unless fn keeps it, and the stored shape is checked before fn is first called, so a File whose M was changed fails with ErrIndexOutOfRange even when M is now 0 or less.

func (*File) RangeMeasurementsTF

func (f *File) RangeMeasurementsTF(fn func(m int, re, im [][]float64) error) error

RangeMeasurementsTF calls fn with the real and imaginary parts ([R][N], as from ReadMeasurementTF) of each measurement of a TF file in order. It stops like RangeMeasurements.

func (*File) ReadMeasurement

func (f *File) ReadMeasurement(m int) ([][]float64, error)

ReadMeasurement returns the impulse responses of measurement m, shaped [R][N]. For a File returned by OpenLazy it reads just that measurement from the file into new slices; otherwise it returns ImpulseResponses[m], which shares memory with the File. It fails with ErrUnsupportedDataType for non-FIR files, with ErrIndexOutOfRange when m is outside [0,M) or no data is stored there, and with fs.ErrClosed after Close of a lazy File. A lazy File reads the audio variable OpenLazy found for its DataType; after DataType, M, R, N or E are changed, reads fail with ErrUnsupportedDataType when the file holds no variable for the new DataType and with ErrIndexOutOfRange when its shape no longer matches, or with fs.ErrClosed once the File is closed.

func (*File) ReadMeasurementSOS

func (f *File) ReadMeasurementSOS(m int) ([][]float64, error)

ReadMeasurementSOS returns the second-order-section coefficients of measurement m of an SOS file, shaped [R][N] like SOSCoefficients[m]. It reads and fails like ReadMeasurement, with ErrUnsupportedDataType for files other than SOS.

func (*File) ReadMeasurementTF

func (f *File) ReadMeasurementTF(m int) (re, im [][]float64, err error)

ReadMeasurementTF returns the real and imaginary parts of the transfer functions of measurement m of a TF file, each shaped [R][N]. It reads and fails like ReadMeasurement, with ErrUnsupportedDataType for files other than TF.

func (*File) ReadMeasurementTFE

func (f *File) ReadMeasurementTFE(m int) (re, im [][][]float64, err error)

ReadMeasurementTFE returns the real and imaginary parts of the transfer functions of measurement m of a TF-E file, each shaped [R][E][N] like TFRealE[m]. It reads and fails like ReadMeasurement, with ErrUnsupportedDataType for files other than TF-E.

func (*File) SHCoefficientCount

func (f *File) SHCoefficientCount() int

SHCoefficientCount returns the number of SH coefficients stored per (measurement, receiver, frequency) tuple. Returns 0 when the file is not SH-encoded; otherwise returns E = (Lmax+1)².

func (*File) SHOrder

func (f *File) SHOrder() (lmax int, ok bool)

SHOrder returns the spherical-harmonic order Lmax encoded in this file. ok is false if the file is not SH-encoded. Detection rule: DataType is TF-E, the file declares SH (EmitterPosition:Type is "spherical harmonics", or — only when that Type is empty — the convention name or History says so), and E equals (L+1)² for some integer L ≥ 0, so E = 1 is order 0.

func (*File) SHWarnings

func (f *File) SHWarnings() []string

SHWarnings returns advisory messages about possibly-malformed or possibly-undocumented spherical-harmonic encoding. Empty when the file is unambiguous (either clearly SH or clearly not). Callers should surface these to users without treating them as errors.

func (*File) SamplingRateAt

func (f *File) SamplingRateAt(m int) (float64, error)

SamplingRateAt returns the sampling rate in Hz of measurement m, broadcasting an [I] rate to every measurement. It fails with ErrIndexOutOfRange when m is outside [0,M) and with ErrNoSamplingRate when none is stored.

func (*File) SamplingRateScalar

func (f *File) SamplingRateScalar() (float64, error)

SamplingRateScalar returns the file's single sampling rate in Hz: the [I] value, or the [M] values when they are all equal. It fails with ErrNoSamplingRate when none is stored and with ErrVaryingSamplingRate when the per-measurement rates differ; use SamplingRateAt then.

func (*File) Save

func (f *File) Save(path string, opts ...SaveOption) (err error)

Save writes the SOFA file to the specified path. It validates the File struct before writing and creates a fully compliant SOFA file with netCDF-4/HDF5 dimension scales.

Save is atomic: the file is written to a temporary file in the same directory, flushed and fsynced, and then renamed over path. A failed Save leaves any existing file at path untouched and removes the temporary file. If path already exists its permission bits are kept; otherwise the new file gets mode 0644.

Like the SOFA Toolbox's SOFAsave, Save records its own provenance in the written file, without changing the File: APIName "go-sofa" and go-sofa's module version as APIVersion, the save time as DateModified, and as DateCreated when that is empty. When the File names another API (or another go-sofa version), History gains the line "resaved by go-sofa <version> from <APIName> <APIVersion>". Set ApplicationName and ApplicationVersion to record your program.

The save time is the current time, or the SOURCE_DATE_EPOCH environment variable (seconds since the Unix epoch) when set; a malformed value, the empty string included, is an error. Output is otherwise deterministic: with SOURCE_DATE_EPOCH set, saving the same File twice produces byte-identical files.

All required SOFA attributes and datasets are written, along with optional fields if present in the File struct. ReceiverPositions and SourcePositions are required; an empty ListenerPositions or EmitterPositions is written as the conventions' default, [0 0 0] cartesian in metres, again without changing the File. So are global attributes the file's SOFAConventions makes mandatory beyond the generic ones (DatabaseName and ListenerShortName for SimpleFreeFieldHRIR, …): Save writes the empty default the SOFA convention tables give them when Attributes lacks one. Likewise, the SourceView and SourceUp variables that SingleRoomSRIR, SingleRoomDRIR and FreeFieldDirectivityTF make mandatory are written with the tables' defaults (and the attributes VariableAttributes holds for them) when Variables lacks them, and the Reference attribute FreeFieldDirectivityTF makes mandatory on SourcePosition, SourceView and SourceUp as "" where none is set. An empty RoomType is written as the convention's default: reverberant for SingleRoomDRIR, shoebox for SingleRoomSRIR when Variables holds RoomCornerA and RoomCornerB, free field otherwise.

The audio data is deflated at level DefaultDeflateLevel; WithDeflate sets another level, WithDeflate(0) writes it uncompressed.

Returns an error if:

  • Validation fails (missing required fields, invalid dimensions, etc.)
  • An option is invalid (a deflate level outside 0–9)
  • HDF5 file creation fails
  • Any write, flush, close, sync or rename operation fails
  • f came from OpenLazy and its audio fields are still empty (ErrNotLoaded)

func (*File) SourcePositionAt

func (f *File) SourcePositionAt(m int) (Vector3, error)

SourcePositionAt returns the source position of measurement m, broadcasting an [I,C] position to every measurement. It fails with ErrIndexOutOfRange when m is outside [0,M) or no position is stored.

func (*File) WriteTo

func (f *File) WriteTo(w io.Writer) (n int64, err error)

WriteTo writes the SOFA file to w, as Save writes it to a file without options (audio data deflated at DefaultDeflateLevel): it validates f first and produces the same bytes. It returns the number of bytes written, and implements io.WriterTo.

The file is assembled in memory (HDF5 needs random access while writing) and handed to w in one Write only once it is complete, so a validation or encoding error writes nothing to w. An error from w is returned together with the bytes w accepted.

type SaveOption added in v0.3.0

type SaveOption func(*saveOptions)

SaveOption configures how Save writes a file.

func WithDeflate added in v0.3.0

func WithDeflate(level int) SaveOption

WithDeflate stores the audio data (Data.IR, Data.Real and Data.Imag, or Data.SOS) deflated at level 1 (fastest) to 9 (smallest) instead of DefaultDeflateLevel; 0 stores it uncompressed and contiguous.

Deflated data is byte-shuffled and split into chunks of one receiver across all measurements, the layout of most SOFA files written by netCDF-C. Chunks hold fewer measurements when that exceeds 4 MiB, but there are never more than 64 chunks (one B-tree node, all libmysofa reads): large files store several receivers per chunk, and only very large ones more measurements.

Level 4 shrinks typical HRIR sets to about the size of their netCDF-C originals (MIT KEMAR: 5.9 MB uncompressed, 1.1 MB deflated, 1.2 MB original); higher levels save little more and take much longer.

type ValidationError

type ValidationError struct {
	Field string
	Err   error
}

ValidationError reports a File that Save refuses to write. Field names the File field at fault, such as "M", "ImpulseResponses", "SourcePositionType" or "Variables"; Err says what is wrong with it. Save returns every validation failure as a *ValidationError, which callers can extract with errors.As. The message starts with Field, as in "M: must be > 0, got 0" or "ImpulseResponses[0] length 1 does not match R=2".

func (*ValidationError) Error

func (e *ValidationError) Error() string

func (*ValidationError) Unwrap

func (e *ValidationError) Unwrap() error

Unwrap returns Err, so that errors.Is sees sentinels such as ErrUnsupportedDataType through a ValidationError.

type Variable

type Variable struct {
	Name       string
	Dims       []string
	Shape      []int
	Values     []float64
	Chars      []byte
	Attributes []Attribute
}

Variable is a netCDF variable go-sofa does not interpret, such as SourceView, RoomCornerA or a char array like ReceiverDescriptions.

Shape gives the size of each axis and Dims their netCDF dimension names (nil when the file does not name them; Save then writes the variable without dimensions, which netCDF reads as phony ones). Exactly one of Values and Chars holds the data, row-major: Values for numeric variables, which Open reads and Save writes as float64, and Chars for char arrays, one byte per element (0 for an unset character).

type Vector3

type Vector3 struct {
	X, Y, Z float64
}

Vector3 is one coordinate triplet of a position or orientation variable. Its units are those the variable's Type and Units attributes name: X, Y, Z in metres for "cartesian"; azimuth, elevation (degrees, or radians where a file's Units say so; Save accepts only degrees) and radius in metres for "spherical" and "spherical harmonics" (where each EmitterPosition row is one SH coefficient's emitter).

Directories

Path Synopsis
cmd
sofa2json command
Command sofa2json exports SOFA files to JSON.
Command sofa2json exports SOFA files to JSON.
sofainfo command
Command sofainfo prints a metadata summary of SOFA files: AES69 global attributes, dimensions and basic audio parameters.
Command sofainfo prints a metadata summary of SOFA files: AES69 global attributes, dimensions and basic audio parameters.
sofaprobe command
Command sofaprobe inspects SOFA files and dumps their HDF5 structure, attributes, datasets and dimension scales, followed by a preview of the audio data (Data.IR, Data.Real, Data.Imag or Data.SOS).
Command sofaprobe inspects SOFA files and dumps their HDF5 structure, attributes, datasets and dimension scales, followed by a preview of the audio data (Data.IR, Data.Real, Data.Imag or Data.SOS).
internal
clitest
Package clitest builds small, valid SOFA files for the command tests in cmd/.
Package clitest builds small, valid SOFA files for the command tests in cmd/.
interop/gen command
Command gen writes one SOFA file per supported DataType (FIR, TF, TF-E, SOS) with sofa.Save, filled with known values, plus an expected.json sidecar describing what a reference reader must see.
Command gen writes one SOFA file per supported DataType (FIR, TF, TF-E, SOS) with sofa.Save, filled with known values, plus an expected.json sidecar describing what a reference reader must see.
interop/mysofa command
Command mysofa checks that libmysofa, the reader most SOFA renderers use, loads the files go-sofa writes.
Command mysofa checks that libmysofa, the reader most SOFA renderers use, loads the files go-sofa writes.
interop/toolbox command
Command toolbox is the Go side of the SOFA Toolbox cross-validation (scripts/matlab/roundtrip.m, README "Cross-validation").
Command toolbox is the Go side of the SOFA Toolbox cross-validation (scripts/matlab/roundtrip.m, README "Cross-validation").

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL