Files
skills/docs/media/audio-fx/audio-fx-plugin-spatial/src/spatial/spatial.md
2026-05-29 08:20:56 +01:00

5.1 KiB
Raw Blame History

spatial

(no submodules)

Purpose

SpatialAudioPlugin is the universal positional spatial plugin. It computes a per-block distance attenuation from the listener-relative position vector using the Web Audio "inverse-distance" rolloff model, then applies equal-power stereo panning based on the source's position along the X axis. The model matches the standard PannerNode semantics so a browser-side build using createAudioNode() (mounted by @suaveplan/audio-engine-browser) renders identically to the universal block path.

The plugin extends AbstractAudioFxPlugin and is stateless on the processing path. reset() is a no-op by design.

Features

  • Six parameters: positionX/Y/Z, refDistance, maxDistance, rolloffFactor — all automatable, mirroring the Web Audio PannerNode surface.
  • orientation: Vec3 carried via setOrientation / getOrientation and survives preset round-trip via SpatialPresetData. Not used by the universal distance/pan path, but available so a future HRTF renderer can read it without API breakage.
  • Inverse-distance rolloff: gain = refDistance / (refDistance + rolloffFactor · (clamp(d, refDistance, maxDistance) − refDistance)). At d ≤ refDistance, gain = 1 (unity); for d > refDistance, gain decreases monotonically as d increases — the spec's "distance attenuation decreases monotonically per the rolloff model" contract.
  • Equal-power stereo panning: panNorm = x / d, angle = (panNorm + 1) · π / 4, outL = in · cos(angle) · distanceGain, outR = in · sin(angle) · distanceGain. Mono input gets distance gain only; surround channels (third+) get distance gain only.

Usage: Basic Example

import { SpatialAudioPlugin } from "@suaveplan/audio-fx-plugin-spatial";

const spatial = new SpatialAudioPlugin({
    initialPosition: { x: 0, y: 0, z: 0 },
    initialRefDistance: 1,
    initialMaxDistance: 100,
    initialRolloffFactor: 1,
});
chain.add(spatial);

Usage: Advanced Example

import { SpatialAudioPlugin } from "@suaveplan/audio-fx-plugin-spatial";

// Game footstep emitter: position is updated every frame from the
// listener-relative player transform.
const footstep = new SpatialAudioPlugin({
    initialRefDistance: 1,
    initialMaxDistance: 50,
    initialRolloffFactor: 2,
});
await footstep.setup(
    { sampleRate: 48_000, channels: 2, blockSize: 512 },
    ctx
);

function onFrame(playerToSource: { x: number; y: number; z: number }) {
    footstep.setParameter("positionX", playerToSource.x);
    footstep.setParameter("positionY", playerToSource.y);
    footstep.setParameter("positionZ", playerToSource.z);
}

API Reference

  • SpatialAudioPlugin (class extends AbstractAudioFxPlugin)
    • new SpatialAudioPlugin(options?: SpatialAudioPluginOptions) — throws AUDIO_FX.INVALID_OPTIONS on malformed options.
    • setOrientation(orientation: Vec3): void — replace the stored orientation vector.
    • getOrientation(): Vec3 — read the current orientation vector.
    • Inherited parameter accessors for positionX, positionY, positionZ, refDistance, maxDistance, rolloffFactor.
  • Types: Vec3 = { x: number; y: number; z: number }.

Implementation Notes

The distance gain uses the Web Audio inverse-distance model rather than the linear or exponential alternatives because it has the most predictable monotonic-decrease property: every d > refDistance strictly lowers the gain, with no inflection points or asymptote at maxDistance. The clamp clamp(d, refDistance, maxDistance) handles the boundary cases — d < refDistance collapses to unity gain (no boost for sources past the listener's head), and d > maxDistance freezes the attenuation at the maximum-distance value (a typical game-audio "kill zone" that prevents distant sources from being inaudible-but-still-rendered).

Panning is equal-power (cos/sin) rather than linear (1-panNorm/panNorm) because equal-power preserves perceived loudness as a source moves across the stereo field. The mapping angle = (panNorm + 1) · π / 4 puts panNorm = -1 (hard left) at angle = 0 (gain = cos 0 = 1, sin 0 = 0) andpanNorm = 1(hard right) atangle = π/2(gain =cos π/2 = 0, sin π/2 = 1). AtpanNorm = 0(centre), both channels get√½` so the summed energy matches a mono source unchanged.

orientation is deliberately not a parameter. Parameters in the universal scheme are scalar-valued and automatable; a 3-vector orientation would need three separate parameters that are typically updated together. Exposing setOrientation / getOrientation keeps the API ergonomic for game-audio callers while preserving the vector across preset round-trip via SpatialPresetData. A future revision could add automatable orientationX/Y/Z parameters if a real use case emerges.

Mono input is distance-attenuated only — no pan is applied because there is no L/R to mix between. This matches typical 3D-audio engine behaviour: if a sound is recorded mono, it gets routed through the spatial pipeline but emerges mono unless the engine explicitly upmixes. The io.outputs[0].count === "preserve" declaration carries that intent into the chain composition rules.