Files
JustOneCacophony/docs/binaural.en.md
T
TheM14 528d7f6727
Native builds / linux-x64 (push) Failing after 15s
Native builds / macos-arm64 (push) Has been cancelled
Native builds / macos-x64 (push) Has been cancelled
Native builds / windows-x64 (push) Has been cancelled
Native builds / Publish GitHub Release (push) Has been cancelled
添加双耳渲染功能
2026-09-06 19:13:59 +08:00

12 KiB
Raw Blame History

Binaural rendering

中文 · Back to README

JustOneCacophony's binaural backend supports three HRTF sources: SimpleFreeFieldHRIR SOFA, the Rosella .personalized_headphone model exported by Dolby's official personalization scan (its JSON parsing is implemented by this project and invokes no Dolby software), and the .jochrtf cache compiled from SOFA. SOFA is compiled into an in-memory directional field when the model is loaded. A .jochrtf file is only a disposable, reproducible JOC compiled HRTF cache; it is neither an interchange format nor a prerequisite for using SOFA.

SOFA FIR
  -> CanonicalHrtf
  -> 48 kHz / one radius shell / delay-phase policy
  -> 64-QMF / 77-hybrid projection
  -> fifth-order ACN/N3D real-SH field
  -> per-object direct + early reflections
  -> shared unitary-FDN late room
  -> float64 stereo

Inputs

The CLI has three mutually exclusive HRTF input sources; with none given, a default rule resolves the input:

# 1) SOFA: defaults to HRTF/binaural.sofa, or an explicit path
python main.py input.m4a --binaural
python main.py input.m4a --binaural --sofa-hrtf C:\HRTF\subject.sofa

# 2) Rosella .personalized_headphone: defaults to HRTF/binaural.personalized_headphone
python main.py input.m4a --binaural --personalized-headphone
python main.py input.m4a --binaural --personalized-headphone C:\HRTF\subject.personalized_headphone

# 3) .jochrtf: explicitly load a compiled cache
python main.py input.m4a --binaural `
  --compiled-hrtf-cache C:\HRTF\subject.jochrtf

# Optional: create/reuse a transparent disk cache for SOFA
python main.py input.m4a --binaural --sofa-hrtf C:\HRTF\subject.sofa `
  --hrtf-cache-policy disk

The default order is HRTF/binaural.sofa, then the unique .jochrtf under output/hrtf-cache, then HRTF/binaural.personalized_headphone; if none of the three exist, an error asks for an explicit path. Multiple .jochrtf files under output/hrtf-cache are also an error requiring an explicit choice.

The .personalized_headphone JSON parsing is implemented by this project (src/rosella_model.py) and does not invoke any Dolby software.

--hrtf-cache-policy accepts none, memory, or disk. The default is memory; neither none nor memory creates a file. disk writes to output/hrtf-cache by default, or to --hrtf-cache-dir. --hrtf-radius-m selects the nearest measurement-radius shell.

The Python API also uses explicit factories:

from sofa_binaural_backend import SofaBinauralBackend

renderer = SofaBinauralBackend.from_sofa(
    "subject.sofa",
    source_count=16,
    default_profile="mid",
    cache_policy="memory",
)

cached = SofaBinauralBackend.from_compiled_cache(
    "subject.jochrtf",
    source_count=16,
    default_profile="mid",
)

The factories never guess a format from an unknown suffix: SOFA and .jochrtf always use distinct loaders.

Binaural render mode

--binaural-mode off|near|mid|far (default mid) is a human-specified rendering hint, not original binaural metadata extracted or recovered from the input E-AC-3 JOC bitstream:

  • Direct binaural rendering (--binaural): near/mid/far apply, default mid; off is an error;
  • ADM BWF: the low 3 binaural-render-mode bits of the last 15 JOC object entries in DBMD segment 10 carry off=0/near=1/far=2/mid=3, leaving the first 10 bed entries unchanged; the default is mid, and off explicitly disables the binaural metadata hint.

Canonical SOFA contract

The strict importer currently accepts:

  • Conventions=SOFA;
  • SOFAConventions=SimpleFreeFieldHRIR, version 0.4, 1.0, or 1.1;
  • DataType=FIR and Data.IR[M,2,N];
  • one positive finite Data.SamplingRate in hertz/Hz;
  • spherical or Cartesian SourcePosition;
  • singleton or per-measurement ListenerPosition/View/Up;
  • two receivers whose listener-local lateral geometry uniquely identifies L/R;
  • one zero-offset emitter;
  • causal Data.Delay[I,2] or [M,2];
  • an explicitly free-field/anechoic RoomType.

Receiver order comes from geometry, never from the receiver array index. SOFA listener coordinates are +X front, +Y left, +Z up; ADM coordinates are +X right, +Y front, +Z up:

\bigl(x_{\mathrm{SOFA}},\ y_{\mathrm{SOFA}},\ z_{\mathrm{SOFA}}\bigr) = \bigl(y_{\mathrm{ADM}},\ -x_{\mathrm{ADM}},\ z_{\mathrm{ADM}}\bigr)

CanonicalHrtf keeps Data.IR and Data.Delay separate. Only a time-domain baseline calls materialized_measurement() to apply delay once; the runtime SH path never materializes and then restores the delay. Non-48-kHz HRIRs are normalized with float64 scipy.signal.resample_poly, and delay samples scale by the same ratio.

GeneralFIR, BRIR, TF, multiple emitters, ambiguous receivers, and non-free-field data require convention-specific adapters. They cannot enter the core importer through a reshape.

Exactly-once delay and phase

The compiler recognizes three mutually exclusive representations:

  1. Nonzero Data.Delay is external to Data.IR; the FIR is not de-rotated and runtime applies the delay once.
  2. With Data.Delay=0 and an ordinary positive-onset HRIR, each ear's main peak supplies arrival time. Compilation separates it and runtime restores it once. The current threshold is a peak index greater than two samples.
  3. With Data.Delay=0 and both FIRs at a shared sample-zero origin, no external delay is invented. The authored complex phase stays in the fifth-order field.

No path may add a second ear delay or phase-group delay.

Public filterbank and directional field

The runtime is fixed at:

  • 48 kHz;
  • a 64-sample QMF hop;
  • 64-QMF / 77 hybrid bands;
  • 961 samples of analysis/synthesis latency;
  • fifth order, 36 terms, ACN/N3D real spherical harmonics;
  • float64 PCM, delay, SH, and room state; complex128 band transfers and spectra.

Real and imaginary unit gains for every hybrid band pass through the same analysis/synthesis chain to form a 154-real-parameter impulse dictionary. The compiler does not sample 77 FFT bins. Defaults are 1e-3 projection ridge and 1e-5 SH ridge. Coincident directions are merged before a spherical-Voronoi weighted ridge fit.

The fixed resource is data/rosella_kernels.npz, which implements publicly standardized filter banks, computable from the following formulas.

The hybrid analysis kernels are defined in 3GPP TS 26.405 / ETSI TS 126 405, Section 5.2.2 (Table 1 $Q=8$/Q=4 coefficients, delay 6):

G_q^p[n] = g^p[n]\cdot\exp\!\Bigl(j\,\frac{2\pi}{Q^p}\bigl(q+\tfrac12\bigr)(n-6)\Bigr),\qquad n=0,\dots,12

The QMF analysis table is the MPEG-4 AAC/SBR 64 complex QMF bank of ISO/IEC 14496-3/AMD1:2003, subclause 4.B.18.2, stored as the polyphase reordering of the public 640-tap prototype c_0,\dots,c_{639}:

A_{r,t} = \frac{(-1)^t}{128}\,c_{63-r+64t},\qquad r=0,\dots,63,\ t=0,\dots,9

The QMF synthesis table is the causal left inverse of the analysis polyphase matrix \mathbf{A}, i.e. the solution of \mathbf{A}\,\mathbf{W}=\mathbf{P} (\mathbf{P} is the 577-sample delay permutation; total latency 961 = 577 + 6\times64), stored as a rank-4 factorization:

W_{b,l} = \sum_{r=1}^{4} t_{b,l,r}\,\mathbf{b}_{b,r}^{\top}

The hybrid synthesis table is the 77→64 recombination: identity for the high bands, Y_{3+b}=X_{16+b}, and for the low bands (C_p is the 8+4+4 child partition):

Y_p = \sum_{q\in C_p}\Bigl(\operatorname{Re}X_q + j\,s_q\,\operatorname{Im}X_q\Bigr),\qquad s_q\in\{\pm1\}

The loader verifies the archive and every array by SHA-256; the table version, all array hashes, and the 77 reference band-center values are part of the cache key. Public availability of a standard does not by itself grant permission to practice related patent claims. See data/README.en.md and THIRD_PARTY_NOTICES.md for the sources and the rights boundary.

.jochrtf

A .jochrtf file is a pickle-free compressed NumPy archive with an exact member set:

key dtype / shape
metadata_json NumPy Unicode scalar containing JSON text (dtype.kind == "U")
band_center_frequencies_hz little-endian float64[77]
coefficients little-endian complex128[36,2,77]
delay_coefficients little-endian float64[36,2]
delay_bounds little-endian float64[2,2]

Metadata uses the JOC-HRTF-CACHE magic and records the schema, compiler and phase-policy versions, ACN/N3D convention, filterbank hashes, SOFA content SHA-256, sample rate, radius, order, both ridge values, payload hash, and fit report. Every setting that changes compilation participates in the cache key. Metadata never persists an absolute local source_path; it may keep a display name only.

Before constructing a field, the loader uses allow_pickle=False and validates ZIP members and expanded sizes, shapes, dtypes, byte order, contiguous layout, finite values, delay bounds, band centers, payload hash, and cache key. The writer uses a same-directory temporary file, fsync, a process-held OS file lock, and atomic os.replace. Its hidden .lock sidecar may remain and does not mean that a writer still owns the lock. Outdated, damaged, or mismatched caches cannot hit. SOFA input rebuilds an invalid cache; an explicitly selected cache reports the error.

Deleting a disk cache must not change the field or render produced from the same SOFA and compiler configuration.

A .jochrtf file contains directional-field coefficients and delay data transformed from the source HRIRs. Its reproducibility therefore does not make it licence-free. Creating a cache does not enlarge the rights granted by the source SOFA/HRTF dataset: use, copying, and redistribution remain subject to that dataset's terms. If those terms are unclear, keep .jochrtf as a private local cache and do not ship it with the program or another build artifact. source_sha256 is only a content-integrity identifier, not proof of provenance or permission.

JOC objects and room behavior

The production adapter retains the existing JOC schedule:

  • [1536,16] input per frame;
  • channel 0 is special LFE and channels 1..15 are JOC objects;
  • ID11/OAMD positions use a sample-timed timeline;
  • source parameters update every 512 samples;
  • every object owns independent direct/early history while one late FDN is shared;
  • finish() drains early/late tails; output gain is explicit, with no implicit limiter or programme loudness normalization.

Near/Mid/Far, equal-power direct level, six first-order shoebox image sources, late sends, the unitary FDN, the 120–180 Hz cosine-squared LFE low-pass, and room calibration are JOC project-defined behavior, not constants published by SOFA or Dolby.

The public SOFA binaural renderer defaults to the C++20 native core under --backend auto/native (ejoc_sofa_binaural_* in lib/eac3joc_core.dll): the filterbank, the SH direction-field evaluation, the per-object direct/early histories and the shared FDN all run natively, while Python only compiles the SOFA source and issues the per-512-sample metadata updates. When the native library is unavailable the renderer falls back to the Python/NumPy reference implementation; the two agree to better than 1e-9. --backend python forces the Python backend. --backend still selects native/Python JOC reconstruction and speaker rendering; native acceleration for the public binaural DSP is outside the current API.

Technical references and rights boundary

Public availability of a specification, source file, or patent document does not by itself authorize copying its contents, redistribution of derivatives, or practice of patent claims. These technical references grant no patent licence and make no non-infringement representation. Anyone preparing a release or product integration must assess the applicable data and software licences, patent permissions, and freedom to operate. See THIRD_PARTY_NOTICES.md for the public-standard provenance and rights boundary.