Initial Commit
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
# `WebAudioGraph`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`WebAudioGraph` is the thin id-keyed registry that sits between
|
||||
`BrowserTimelineAdapter` and a `BaseAudioContext`. It exists so the adapter
|
||||
can address `AudioNode`s by stable string id (e.g. `track:42:gain`) rather
|
||||
than juggling raw node references, and so a populated graph can be
|
||||
snapshotted to a JSON-safe object and restored into a fresh context.
|
||||
|
||||
The registry is intentionally narrow: it owns the node table and the edge
|
||||
list, validates ids on every operation, and produces a snapshot whose
|
||||
shape (node-types + edges) is independent of any live `AudioNode`
|
||||
instance. Snapshots round-trip through `JSON.stringify` and survive
|
||||
context replacement — the same snapshot can be restored into a realtime
|
||||
`AudioContext`, an `OfflineAudioContext`, or a deterministic test shim.
|
||||
|
||||
## Features
|
||||
|
||||
- `addNode` / `removeNode` keyed by string id.
|
||||
- `connect` / `disconnect` wrapping `AudioNode.connect` / `disconnect`
|
||||
with full optional `outputIndex`/`inputIndex` support.
|
||||
- `getNode` and `listIds` for read-side inspection.
|
||||
- `snapshot()` returning a `Readonly<AudioGraphSnapshot>` containing
|
||||
node-type tags and the edge list.
|
||||
- `restore(snapshot)` Zod-validates the snapshot and rebuilds the graph
|
||||
against the bound context, raising
|
||||
`AUDIO_ENGINE_BROWSER.NODE_NOT_FOUND` for orphan edges.
|
||||
- `classifyNode(node)` exported helper that maps a live `AudioNode` to
|
||||
its `AudioNodeType` tag using its `constructor.name`.
|
||||
- Defensive: every id mismatch raises a typed
|
||||
`BrowserAudioError(NODE_NOT_FOUND)` carrying the offending id.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { WebAudioGraph } from "@suaveplan/audio-engine-browser/graph/web-audio-graph";
|
||||
|
||||
const graph = new WebAudioGraph(audioContext);
|
||||
graph.addNode("master", audioContext.createGain());
|
||||
graph.addNode("dest", audioContext.destination);
|
||||
graph.connect("master", "dest");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
WebAudioGraph,
|
||||
} from "@suaveplan/audio-engine-browser/graph/web-audio-graph";
|
||||
|
||||
const graph = new WebAudioGraph(audioContext);
|
||||
graph.addNode("src", audioContext.createBufferSource());
|
||||
graph.addNode("gain", audioContext.createGain());
|
||||
graph.addNode("dest", audioContext.destination);
|
||||
graph.connect("src", "gain");
|
||||
graph.connect("gain", "dest");
|
||||
|
||||
// Persist for an undo step
|
||||
const snap = graph.snapshot();
|
||||
localStorage.setItem("graph", JSON.stringify(snap));
|
||||
|
||||
// Later — possibly in a new context — restore
|
||||
const restored = new WebAudioGraph(newContext);
|
||||
restored.restore(JSON.parse(localStorage.getItem("graph")!));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `new WebAudioGraph(context)`
|
||||
|
||||
Constructs a graph bound to a `BaseAudioContext`-shaped value. The
|
||||
context is the factory used by `restore()`; calls to `addNode` accept
|
||||
externally-constructed nodes.
|
||||
|
||||
### `addNode(id, node) → void`
|
||||
|
||||
Registers a node under `id`. Existing entries are overwritten so the
|
||||
adapter can swap nodes during reconfiguration; the edge list is left
|
||||
untouched.
|
||||
|
||||
### `removeNode(id) → void`
|
||||
|
||||
Disconnects the node, removes it, and prunes every edge that referenced
|
||||
it. Throws `NODE_NOT_FOUND` for an unknown id.
|
||||
|
||||
### `connect(sourceId, destinationId, outputIndex?, inputIndex?) → void`
|
||||
|
||||
Wraps `AudioNode.connect`. Throws `NODE_NOT_FOUND` if either id is
|
||||
absent. Records the edge so it appears in `snapshot()`.
|
||||
|
||||
### `disconnect(sourceId, destinationId?) → void`
|
||||
|
||||
When `destinationId` is omitted, disconnects every outgoing edge from
|
||||
`source`. Otherwise disconnects just that edge. Throws `NODE_NOT_FOUND`
|
||||
for missing source.
|
||||
|
||||
### `snapshot() → AudioGraphSnapshot`
|
||||
|
||||
Returns `{ nodes: Record<id, AudioNodeType>, connections: Edge[] }`.
|
||||
|
||||
### `restore(snapshot) → void`
|
||||
|
||||
Zod-validates the snapshot, clears the current registry, rebuilds nodes
|
||||
via the bound context's factories, and replays every edge. Throws
|
||||
`NODE_NOT_FOUND` for invalid or orphan-edge snapshots.
|
||||
|
||||
### `classifyNode(node) → AudioNodeType`
|
||||
|
||||
Static helper inferring the type tag from `constructor.name`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `snapshot()` records **node types**, not node instances; this is what
|
||||
makes it JSON-safe.
|
||||
- `restore()` calls into the context's `create*` factories
|
||||
(`createGain`, `createStereoPanner`, …) so the restored graph uses
|
||||
nodes native to the new context. Worklet and `ScriptProcessor` nodes
|
||||
cannot be reconstructed by tag alone and are rejected at restore.
|
||||
- Per-disconnect graph mutations are best-effort: the underlying
|
||||
`AudioNode.disconnect` may throw when the destination is not actually
|
||||
connected, which is treated as a no-op so a careless caller cannot
|
||||
desynchronise the edge list from the live graph.
|
||||
- The Zod validator rejects unknown node types and any edge referencing
|
||||
an absent node before any factory call is issued — the graph reaches
|
||||
an inconsistent state only when a downstream context factory fails,
|
||||
in which case the partial state is left for the caller to observe.
|
||||
Reference in New Issue
Block a user