Class FoaDecoder

Standalone 4-channel-in / 2-channel-out FOA → binaural FORMAT CONVERTER, built entirely from native Web Audio nodes (NO worklet).

Why this is NOT a CacophonyEffect

A CacophonyEffect (and the Bus.addFilter chain) assumes ONE node that is both the input the chain connects into and the output it connects out of (see the module docstring above). A FOA decoder cannot satisfy that contract: its input is a 4-channel FOA node and its output is a 2-channel binaural node — two distinct nodes with different channel counts. Squeezing it into build(): AudioNode forced the head splitter to masquerade as both, so downstream routing read the 4-channel splitter, not the stereo tail.

Instead this is a standalone construct exposing the two endpoints explicitly:

  • input — the 4-channel ChannelSplitterNode you feed FOA into.
  • output — the 2-channel binaural GainNode you route downstream.

Wire it EXPLICITLY (it is NOT a bus filter):

  const decoder = await cacophony.createFoaDecoder();
foaSourceNode.connect(decoder.input); // 4-ch FOA in
decoder.output.connect(bus.input /* or context.destination */); // stereo out

Math (Ahrens 2022, eq. 31)

Under a real (SN3D/ACN) SH basis the binaural decode collapses to a single per-EAR multiply-accumulate over ALL the SH channels: B^{L,R}(w) = sum_{n,m} S_{n,m}(w) * H_{n,m}^{L,R}(w) (Ahrens, "Binaural Audio Rendering in the Spherical Harmonic Domain", 2022, eq. 31 — the real-basis form where the conjugation and m -> -m degree flip vanish). EACH ear sums the contribution of EVERY FOA channel (W, Y, Z, X) convolved with that ear's HRIR — neither ear may drop a channel. With a single stored SH-HRIR the L/R-symmetric channels (W, Z, X) share the same coefficient for both ears while the L/R-ANTISYMMETRIC channel (Y) is the sign-flip for the right ear, so: B^L = WH_W + YH_Y + ZH_Z + XH_X B^R = WH_W - YH_Y + ZH_Z + XH_X (only the Y term differs, by sign)

Topology (Omnitone WY/ZX packing — GoogleChrome/omnitone foa-convolver.js)

Rather than 8 mono convolvers, the four SH channels are grouped into two STEREO ConvolverNodes — W+Y into one, Z+X into the other — each convolved against a 2-row slice of the 4-row SH-HRIR. A 4-channel ConvolverNode would do unwanted cross-channel convolution per the Web Audio spec; the stereo packing is Omnitone's production graph and is mirrored here VERBATIM (the .connect calls below are line-for-line Omnitone's _buildAudioGraph):

input (ChannelSplitter, 4ch) ch0(W),ch1(Y) -> mergerWY(2ch) -> convolverWY (HRIR rows {W,Y}, stereo) ch2(Z),ch3(X) -> mergerZX(2ch) -> convolverZX (HRIR rows {Z,X}, stereo) convolverWY -> splitterWY(2ch); convolverZX -> splitterZX(2ch) splitterWY.ch0 (W) -> L AND R splitterWY.ch1 (Y) -> L, and (via -1 inverter) -> R splitterZX.ch0 (Z) -> L AND R splitterZX.ch1 (X) -> L AND R mergerBinaural(2ch) -> output (stereo)

The result is exactly eq.31: BOTH ears receive ALL four SH channels; only the Y channel's right-ear contribution is sign-flipped by a GainNode(-1), because Y is the sole left/right-antisymmetric FOA channel. (This is the fix for the prior broken graph, which sent only W+Z to the left ear and only -Y+X to the right — each ear missing two channels.) convolver.normalize is set false on both: the SH-HRIR is already correctly scaled and Web Audio convolver normalization would corrupt it.

Normalization (SN3D end-to-end — NO sqrt(3) rescale)

The decoder, the bundled Omnitone HRIR, and encodeMonoToFoaSN3D are ALL SN3D. The decode is the plain MAC of two SN3D-matched coefficient sets, so NO per-channel sqrt(3) (N3D<->SN3D) rescale is applied — inserting one would double-normalize.

NOTE on the resurrection path: the dormant StereoToFoaUpmixer (createStereoToBFormatNode) emits ACN [W,Y,Z,X] but is a perceptual, frequency-banded, coherence-gated mix with per-channel non-constant gain — it is NEITHER N3D nor SN3D and no single normalization bridge exists. Its 4-ch output plugs straight into input (ACN ordering already lines up), and the resulting binaural is a documented PERCEPTUAL APPROXIMATION, not a physically-correct sound field. The clean, physically-correct path is encodeMonoToFoaSN3D -> this decoder.

Constructors

Properties

Methods

Constructors

Properties

input: AudioNode

The 4-channel FOA [W, Y, Z, X] (ACN) entry node. Feed FOA into this.

output: AudioNode

The 2-channel binaural stereo node. Route THIS downstream.

Methods

  • Builds the live decode graph against context, resolving the SH-HRIR (caller-supplied or bundled Omnitone) and wiring the Omnitone WY/ZX graph. Async because the bundled HRIR is fetched + decodeAudioData-d.

    Parameters

    Returns Promise<FoaDecoder>

  • Builds a 2-channel AudioBuffer from rows rowA/rowB of the 4-row SH-HRIR — the stereo buffer a WY or ZX ConvolverNode convolves against. Falls back to the source buffer unchanged when the runtime cannot allocate a buffer or the source lacks the rows (e.g. a stubbed mock buffer in tests with fewer channels): the graph wiring is unaffected.

    Parameters

    Returns AudioBuffer