first commit
This commit is contained in:
31
docs/media/audio-fx/audio-fx-plugin-spatial/README.md
Normal file
31
docs/media/audio-fx/audio-fx-plugin-spatial/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# @suaveplan/audio-fx-plugin-spatial - spatial plugin for the @suaveplan/audio-fx-plugin-* family.
|
||||
|
||||
spatial plugin for the @suaveplan/audio-fx-plugin-* family.
|
||||
|
||||
Member of the `@suaveplan/audio-fx-plugin-*` family. Universal (browser / server / offline / worklet identical). Depends on `@suaveplan/audio-fx-plugin-base` for the `AudioFxPlugin` contract; declares no other `@suaveplan/audio-fx-plugin-*` deps.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options and preset payload of `SpatialAudioPlugin`. These run on the publi…
|
||||
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options and preset payload of `SpatialAudioPlugin`. These run on the publi…
|
||||
- [spatial](./src/spatial/spatial.md) — `SpatialAudioPlugin` is the universal positional spatial plugin. It computes a per-block distance attenuation from the…
|
||||
- [spatial](./src/spatial/spatial.md) — `SpatialAudioPlugin` is the universal positional spatial plugin. It computes a per-block distance attenuation from the…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Composition
|
||||
|
||||
```ts
|
||||
import { PluginChain, DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain";
|
||||
import { /* Plugin */ } from "@suaveplan/audio-fx-plugin-spatial";
|
||||
|
||||
const plugin = new /* Plugin */();
|
||||
const chain = new PluginChain({
|
||||
buses: new DefaultBusRegistry(),
|
||||
plugins: [plugin],
|
||||
});
|
||||
```
|
||||
|
||||
## Status
|
||||
|
||||
Replaces the `./spatial` subpath of the bundled `@suaveplan/audio-plugins-fx`.
|
||||
@@ -0,0 +1,66 @@
|
||||
# `spatial/schemas`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Zod schemas validating the construction-time options and preset payload of `SpatialAudioPlugin`. These run on the public boundary between user code and the plugin: any malformed object hitting the constructor or `applyPreset` is rejected with `AUDIO_FX.INVALID_OPTIONS` / `AUDIO_FX.INVALID_PRESET` before it can corrupt runtime state.
|
||||
|
||||
Separating schemas from the plugin module lets `index.ts` re-export them so callers can introspect the option surface (for forms, RPC validation, or storybook controls) without importing the audio implementation.
|
||||
|
||||
## Features
|
||||
|
||||
- `Vec3Schema` — validates `{ x, y, z }` all `finite()` so `NaN` / `Infinity` cannot sneak through any vector field.
|
||||
- `SpatialAudioPluginOptionsSchema` — validates all five optional construction fields with their documented ranges (`refDistance` 0.01..1 000, `maxDistance` 0.01..100 000, `rolloffFactor` 0..10) plus the two `Vec3` slots.
|
||||
- `SpatialPresetDataSchema` — `.strict()` object carrying only `orientation: Vec3`; extra fields are rejected.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { SpatialAudioPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-spatial";
|
||||
|
||||
const parsed = SpatialAudioPluginOptionsSchema.parse({
|
||||
initialPosition: { x: 5, y: 0, z: 0 },
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
SpatialAudioPluginOptionsSchema,
|
||||
SpatialPresetDataSchema,
|
||||
Vec3Schema,
|
||||
} from "@suaveplan/audio-fx-plugin-spatial";
|
||||
|
||||
// Reuse the shared Vec3 validator on a network message before
|
||||
// forwarding it to the plugin.
|
||||
function parseSpatialMessage(raw: unknown) {
|
||||
const message = z
|
||||
.object({
|
||||
position: Vec3Schema,
|
||||
orientation: Vec3Schema,
|
||||
})
|
||||
.parse(raw);
|
||||
return message;
|
||||
}
|
||||
|
||||
// Strict preset validation at the IPC boundary.
|
||||
function isSpatialPreset(blob: unknown): boolean {
|
||||
return SpatialPresetDataSchema.safeParse(blob).success;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `Vec3Schema: z.ZodObject<{ x: number; y: number; z: number }>`
|
||||
- `SpatialAudioPluginOptionsSchema: z.ZodObject<{ initialPosition?, initialOrientation?, initialRefDistance?, initialMaxDistance?, initialRolloffFactor? }>`
|
||||
- `SpatialPresetDataSchema: z.ZodObject<{ orientation: Vec3 }, "strict">`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`Vec3Schema` is exported as a named schema (not inlined) so the same value is reused by both `SpatialAudioPluginOptionsSchema` (for `initialPosition` and `initialOrientation`) and `SpatialPresetDataSchema` (for `orientation`). That guarantees all three vector fields agree byte-for-byte on what a legal `Vec3` looks like — and downstream code that wants to validate a `Vec3` independently (e.g., a network message carrying position updates) can pull the same schema.
|
||||
|
||||
`SpatialPresetDataSchema` uses `.strict()` so unknown fields are rejected, matching the sibling-plugin policy. Position and the rolloff knobs are universal parameters and travel through the parameter snapshot built by `AbstractAudioFxPlugin`. Storing them again in the preset payload would double-store the same data and risk drift; encoding only the non-parameter `orientation` makes the preset contract minimal and unambiguous.
|
||||
@@ -0,0 +1,74 @@
|
||||
# `spatial.types`
|
||||
|
||||
## Purpose
|
||||
|
||||
TypeScript-only type definitions for `SpatialAudioPlugin`'s construction options, preset payload, and the shared `Vec3` 3-vector. Co-located with the implementation so the public type surface stays aligned with the Zod validator in `schemas/schemas.ts`.
|
||||
|
||||
`Vec3` is exported here (rather than from a shared package) so the spatial plugin's public API does not force callers to import an unrelated math library — a `{ x, y, z }` interface is small enough to live with its consumer.
|
||||
|
||||
## Features
|
||||
|
||||
- `Vec3` — a finite 3-vector with `x`, `y`, `z` number members.
|
||||
- `SpatialAudioPluginOptions` — all fields optional so the default constructor (`new SpatialAudioPlugin()`) is valid.
|
||||
- `SpatialPresetData` — preset payload carrying the non-parameter `orientation` vector so it survives round-trip.
|
||||
- `readonly` fields prevent accidental mutation of saved options.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
SpatialAudioPluginOptions,
|
||||
Vec3,
|
||||
} from "@suaveplan/audio-fx-plugin-spatial";
|
||||
|
||||
const opts: SpatialAudioPluginOptions = {
|
||||
initialPosition: { x: 5, y: 0, z: 0 },
|
||||
initialRefDistance: 2,
|
||||
};
|
||||
|
||||
const forward: Vec3 = { x: 1, y: 0, z: 0 };
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
SpatialAudioPluginOptions,
|
||||
Vec3,
|
||||
} from "@suaveplan/audio-fx-plugin-spatial";
|
||||
|
||||
function lerp(a: Vec3, b: Vec3, t: number): Vec3 {
|
||||
return {
|
||||
x: a.x + (b.x - a.x) * t,
|
||||
y: a.y + (b.y - a.y) * t,
|
||||
z: a.z + (b.z - a.z) * t,
|
||||
};
|
||||
}
|
||||
|
||||
const startPos: Vec3 = { x: 0, y: 0, z: 0 };
|
||||
const endPos: Vec3 = { x: 30, y: 0, z: 0 };
|
||||
const halfway: SpatialAudioPluginOptions = {
|
||||
initialPosition: lerp(startPos, endPos, 0.5),
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `Vec3`
|
||||
- `readonly x: number`
|
||||
- `readonly y: number`
|
||||
- `readonly z: number`
|
||||
- `SpatialAudioPluginOptions`
|
||||
- `readonly initialPosition?: Vec3`
|
||||
- `readonly initialOrientation?: Vec3`
|
||||
- `readonly initialRefDistance?: number`
|
||||
- `readonly initialMaxDistance?: number`
|
||||
- `readonly initialRolloffFactor?: number`
|
||||
- `SpatialPresetData`
|
||||
- `readonly orientation: Vec3`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`Vec3` is intentionally a plain interface (not a class). Callers can construct one with an object literal at any seam, and TypeScript's structural typing means values produced by a math library can satisfy the shape without explicit conversion. The Zod schema in `schemas.ts` validates that all three fields are finite numbers, blocking `NaN` and `Infinity` at the construction boundary.
|
||||
|
||||
`SpatialPresetData` carries only `orientation`. Position and the rolloff knobs are universal parameters and travel through the parameter snapshot built by `AbstractAudioFxPlugin`. Storing them again in the preset payload would double-store the same data and risk drift; storing only the non-parameter `orientation` makes the preset contract minimal and unambiguous.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user