Initial Commit
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user