first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
# `spatial`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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
```ts
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`) and`panNorm = 1` (hard right) at `angle = π/2` (gain = `cos π/2 = 0`, sin π/2 = 1). At`panNorm = 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.