Private constructorReadonly inputThe 4-channel FOA [W, Y, Z, X] (ACN) entry node. Feed FOA into this.
Readonly outputThe 2-channel binaural stereo node. Route THIS downstream.
Static createBuilds 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.
Optional context: BaseContextStatic Private sliceBuilds 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.
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
CacophonyEffectA
CacophonyEffect(and theBus.addFilterchain) 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 intobuild(): AudioNodeforced 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:
ChannelSplitterNodeyou feed FOA into.GainNodeyou route downstream.Wire it EXPLICITLY (it is NOT a bus filter):
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.connectcalls 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.normalizeis setfalseon 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.