Files
2026-05-27 23:29:53 +01:00

150 lines
7.1 KiB
Markdown

# Design — @suaveplan/audio-engine
## Architecture overview
The package is a layered audio engine:
```
┌─────────────────────────────────────────────────────────┐
│ AudioQueueEngine (extends AudioEngine + queue logic) │
├─────────────────────────────────────────────────────────┤
│ AudioEngine (context lifecycle, plugin map, metrics) │
├─────────────────────────────────────────────────────────┤
│ AudioGraph (plugin node connections, DAG algorithms) │
│ PluginRegistry (constructor + descriptor registration) │
│ AudioWorkletManager (module load, node lifecycle) │
│ EventBus (typed wrapper over @suaveplan/events) │
├─────────────────────────────────────────────────────────┤
│ AudioPlugin (abstract base: state, metadata, params) │
└─────────────────────────────────────────────────────────┘
```
All layers depend downward. `AudioQueueEngine` is the only class that extends another; everything else uses composition.
## File layout
```
src/
index.ts # explicit named re-exports only
error-codes.ts # defineCodes + AudioEngineError
error-codes.md
types/
common.ts # all public types + Zod schemas
common.md
engine/
engine-state.ts # AudioEngineState enum (~20 lines)
engine-metrics.ts # OTel meter instruments (~40 lines)
engine.ts # AudioEngine class (~280 lines)
engine.test.ts
engine.spec-conformance.test.ts
engine.md
graph/
graph-connect.ts # connect/disconnect + cycle detection (~100 lines)
graph-traverse.ts # topological sort, BFS, source/sink (~100 lines)
graph.ts # AudioGraph class facade (~120 lines)
graph.test.ts
graph.spec-conformance.test.ts
graph.md
plugin/
plugin-base.ts # AudioPlugin abstract class + definePlugin (~180 lines)
plugin-base.test.ts
plugin-base.md
registry/
plugin-registry.ts # PluginRegistry class (~280 lines)
plugin-registry.test.ts
plugin-registry.md
worklet/
worklet-loader.ts # loadModule, URL allow-list (~100 lines)
worklet-messaging.ts # sendMessage, setMessageListener (~80 lines)
worklet-registry.ts # createNode, destroyNode, stats (~120 lines)
worklet-manager.ts # facade re-exporting sub-modules (~60 lines)
worklet-manager.test.ts
worklet-manager.spec-conformance.test.ts
worklet-manager.md
queue-engine/
cue.ts # AudioProcessingTask + task data types (~140 lines)
scheduler.ts # serial/parallel chain, retry (~160 lines)
queue-engine.ts # AudioQueueEngine class (~180 lines)
queue-engine.test.ts
queue-engine.md
event-bus/
event-bus.ts # EventBus class (~120 lines)
event-bus.test.ts
event-bus.md
__benchmarks__/
graph.bench.ts
graph.bench.md
```
## Key design decisions
### AudioContext injection seam
`AudioEngine` accepts an optional `audioContext` in `AudioEngineConfig`. If provided, the engine skips `new AudioContext()` and uses the injected instance. This lets tests supply a mock or `standardized-audio-context` shim without patching globals.
### Error isolation (plugin quarantine)
When `loadPlugin`, `activatePlugin`, or `unloadPlugin` throws, the engine catches the error, emits an `ERROR` event, and marks the plugin state as `PluginState.ERROR`. The plugin is removed from the active set but not deleted from the map, so callers can inspect `getPlugin(id)?.state === 'error'` and decide whether to retry or remove.
### OTel instrumentation strategy
All instruments are created lazily on first access via a private `getMeter()` helper. If no `MeterProvider` is registered, the calls are no-ops (OTel API inertness guarantee). Counters:
- `audio_engine.plugin_loaded_total` — incremented on successful `loadPlugin`
- `audio_engine.plugin_failed_total` — incremented on failed `loadPlugin`
- `audio_engine.worklet_module_loaded_total` — incremented on successful `loadModule`
Histograms:
- `audio_engine.latency_ms` — recorded in `getMetrics()`
- `audio_engine.worklet_roundtrip_ms` — recorded around `sendMessage` + reply
- `audio_engine.graph_connect_ms` — recorded around `AudioGraph.connect`
### Worklet URL allow-list
`AudioWorkletManager` constructor accepts an optional `allowedOrigins: string[]`. `loadModule` rejects URLs whose origin is not in the list. Default allow-list is `['self']` (same-origin only). This prevents dynamic worklet injection attacks.
### Zod boundaries
Three schemas are validated at runtime:
- `AudioEngineConfigSchema` — parsed in `AudioEngine` constructor
- `PluginMetadataSchema` — parsed in `AudioPlugin` constructor
- `AudioPluginDescriptorSchema` — parsed in `PluginRegistry.registerDescriptor`
All other types are TypeScript-only (no runtime validation) because they are internal or consumed only by typed callers.
### No `standardized-audio-context`
The upstream imported `AudioContext` from `standardized-audio-context`. In genesis we use native Web Audio API types directly. The API surface is identical for consumers (both expose `AudioContext` and `AudioNode`). This removes a runtime dependency.
### Typed event map
`AudioEngineEventMap` replaces the upstream `AudioEngineEvents` enum:
```ts
interface AudioEngineEventMap {
stateChanged: { previousState: AudioEngineState; currentState: AudioEngineState };
pluginLoaded: { pluginId: string; metadata: PluginMetadata };
// ...
}
```
`EventBus` is generic over `EventMap extends Record<string, unknown>` so that `subscribe<K>(type: K, listener: (data: EventMap[K]) => void)` is fully typed.
### Test strategy
- **Engine tests**: inject a mock `AudioContext` (minimal object with `createGain`, `destination`, `state`, `resume`, `suspend`, `close`, `sampleRate`, `currentTime`). No real browser required.
- **Graph tests**: pure logic, no audio nodes needed. Use a mock `AudioPlugin` with fake `inputNode`/`outputNode` objects.
- **Worklet tests**: mock `audioWorklet.addModule` and `AudioWorkletNode`.
- **Queue tests**: mock `@suaveplan/queue` types or use the real in-memory queue (it's lightweight).
- **Registry tests**: pure logic, no audio nodes.
- **EventBus tests**: use `@suaveplan/events` `EventEmitter` directly.
## Dependencies
- `@suaveplan/events` ✅
- `@suaveplan/logger` ✅
- `@suaveplan/queue` ✅
- `@suaveplan/error` ✅
- `@suaveplan/types` ✅
- `zod` (catalog:forms)
- `@opentelemetry/api` (catalog:observability)