# OpenSpec Change: enterprise-browser-audio-engine - **Change ID**: enterprise-browser-audio-engine - **Package**: `@suaveplan/audio-engine` - **Category**: browser - **Target path**: `browser/audio/audio-engine` - **Status**: IN PROGRESS - **Date**: 2026-04-24 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/audio-engine.md` --- ## Summary Deliver `@suaveplan/audio-engine` on the genesis branch from scratch at enterprise quality. The package is the core browser-side Web Audio engine consumed by every React and R3F audio surface in the monorepo. It owns the global `AudioContext` lifecycle, a typed plugin system (`AudioPlugin` base class, `PluginRegistry`, `AudioEngine`), an `AudioGraph` node-routing layer, an `AudioWorkletManager` for loading and messaging worklet modules, an `AudioQueueEngine` for cue/sequence playback, and an `EventBus` facade. Zero React dependencies — consumed by `@suaveplan/audio-controllers`, `@suaveplan/audio-plugins-fx`, and React audio packages. --- ## Current-state audit (genesis) The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data. --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/error` (`workspace:^`) | Base class for `AudioEngineError`; `defineCodes` for `AUDIO.*` error codes | | `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `EventBusLike`, `Clock` for injectable clock support | | `@suaveplan/events` (`workspace:^`) | `EventEmitter` for `AudioEngineEventMap`; typed event bus | | `@suaveplan/schema-registry` (`workspace:^`) | Register `AudioPluginDescriptor` Zod schema | | `@suaveplan/api-schemas` (`workspace:^`) | Shared schema registration surface | | `zod` (`catalog:forms`) | Zod schemas for `PluginMetadata`, `AudioPluginDescriptor`, configs | | `@opentelemetry/api` (`catalog:observability`) | Counters + histograms for plugin lifecycle and latency | | `standardized-audio-context` (`catalog:browser`) | Cross-browser `AudioContext` + `OfflineAudioContext` shim | | `tslib` (`catalog:build`) | Runtime TS helpers | Dev deps: `vitest`, `@vitest/coverage-istanbul`, `mitata` (all `catalog:testing`). --- ## Preserved public API surface (from main branch) ### Core ```ts class AudioEngine { constructor(config?: AudioEngineConfig, context?: BaseAudioContext) start(): Promise stop(): Promise getState(): AudioEngineState getStateInfo(): AudioEngineStateInfo getMetrics(): AudioEngineMetrics loadPlugin(id: string, plugin: AudioPlugin): Promise unloadPlugin(id: string): Promise getPlugin(id: string): T | undefined on(event: K, listener: AudioEngineEventMap[K]): void off(event: K, listener: AudioEngineEventMap[K]): void } interface AudioEngineConfig { sampleRate?: number; latencyHint?: AudioContextLatencyCategory; bufferSize?: number; maxPlugins?: number } type AudioEngineState = 'uninitialized' | 'starting' | 'running' | 'suspended' | 'stopping' | 'stopped' | 'error' interface AudioEngineStateInfo { state: AudioEngineState; previousState: AudioEngineState; timestamp: number } interface AudioEngineMetrics { pluginCount: number; activeNodes: number; cpuUsage: number; sampleRate: number; currentTime: number } type AudioEngineEventMap = { 'state:change': (info: AudioEngineStateInfo) => void; 'plugin:loaded': (id: string) => void; 'plugin:unloaded': (id: string) => void; 'plugin:error': (id: string, error: Error) => void; 'error': (error: Error) => void } ``` ### AudioGraph ```ts class AudioGraph { addNode(id: string, node: AudioNode): void removeNode(id: string): void connect(sourceId: string, destinationId: string, outputIndex?: number, inputIndex?: number): void disconnect(sourceId: string, destinationId?: string): void getNode(id: string): AudioNode | undefined traverse(startId: string): string[] snapshot(): AudioGraphSnapshot restore(snapshot: AudioGraphSnapshot): void } ``` ### AudioQueueEngine ```ts class AudioQueueEngine { enqueue(cue: AudioCue): string dequeue(cueId: string): void play(cueId: string): Promise stop(cueId: string): void getQueue(): AudioCue[] getSequences(): AudioSequence[] addSequence(seq: AudioSequence): void removeSequence(seqId: string): void } interface AudioCue { id?: string; buffer: AudioBuffer; startTime?: number; offset?: number; duration?: number; gain?: number; loop?: boolean } interface AudioSequence { id: string; cues: AudioCue[]; loop?: boolean; tempo?: number } ``` ### AudioWorkletManager ```ts class AudioWorkletManager { addModule(url: string, options?: WorkletModuleOptions): Promise createNode(processorName: string, options?: AudioWorkletNodeOptions): AudioWorkletNode postMessage(processorName: string, message: unknown, transfer?: Transferable[]): void onMessage(processorName: string, handler: (event: MessageEvent) => void): void isLoaded(url: string): boolean } interface WorkletModuleOptions { allowedUrls?: string[]; credentials?: RequestCredentials } ``` ### Plugin system ```ts abstract class AudioPlugin { abstract readonly metadata: PluginMetadata state: PluginState abstract initialize(context: BaseAudioContext): Promise abstract connect(graph: AudioGraph): void abstract disconnect(): void abstract destroy(): Promise setParameter(id: string, value: number): void getParameter(id: string): number | undefined getParameters(): PluginParameter[] } type PluginState = 'uninitialized' | 'initializing' | 'initialized' | 'active' | 'inactive' | 'error' | 'destroyed' interface PluginMetadata { id: string; name: string; version: string; category: string; description?: string } interface PluginParameter { id: string; name: string; value: number; min: number; max: number; default: number; automatable?: boolean } class PluginRegistry { register(descriptor: AudioPluginDescriptor): void unregister(id: string): void get(id: string): AudioPluginDescriptor | undefined list(): AudioPluginDescriptor[] instantiate(id: string): AudioPlugin } interface AudioPluginDescriptor { id: string; name: string; version: string; factory: () => AudioPlugin; metadata: PluginMetadata } ``` ### EventBus facade ```ts class EventBus { constructor(bus?: EventBusLike) emit(event: K, ...args: Parameters): void on(event: K, listener: AudioEngineEventMap[K]): void off(event: K, listener: AudioEngineEventMap[K]): void } ``` --- ## New additions for genesis ### 1. Structured errors via `@suaveplan/error` Error codes with stable `AUDIO.*` namespace: | Code | Severity | Retryable | |---|---|---| | `AUDIO_ENGINE_STATE_INVALID` | `error` | `false` | | `AUDIO_PLUGIN_NOT_FOUND` | `error` | `false` | | `AUDIO_PLUGIN_LOAD_FAILED` | `error` | `true` | | `AUDIO_WORKLET_LOAD_FAILED` | `error` | `true` | | `AUDIO_WORKLET_URL_REJECTED` | `error` | `false` | | `AUDIO_GRAPH_CONNECT_FAILED` | `error` | `false` | | `AUDIO_CONTEXT_UNAVAILABLE` | `fatal` | `false` | ### 2. AudioContext injection seam `AudioEngine` constructor accepts an optional pre-built `BaseAudioContext` so tests and hosts can inject a `standardized-audio-context` context or a shared singleton without touching `globalThis`. ### 3. Plugin quarantine / error isolation A plugin that throws during `process` or `initialize` is quarantined: marked `ERROR` state, disconnected from the graph, and a `plugin:error` event is emitted. The engine and other plugins continue operating. ### 4. Typed `AudioEngineEventMap` Replace string-enum `AudioEngineEvents` with `AudioEngineEventMap` so `EventEmitter` provides per-event argument typing. ### 5. Graph snapshot / restore `AudioGraph.snapshot()` serializes the routing topology (node IDs and connections) to a plain JSON object; `restore()` re-wires from that snapshot. ### 6. Worklet URL allow-list `AudioWorkletManager` rejects `addModule` calls for URLs not present in the engine's configured `allowedWorkletUrls` list; throws `AUDIO_WORKLET_URL_REJECTED`. ### 7. OTel metrics - Counter `audio.plugin_loaded_total` - Counter `audio.plugin_failed_total` - Histogram `audio.engine_latency_ms` - Histogram `audio.worklet_roundtrip_ms` - Histogram `audio.graph_connect_ms` ### 8. Render-on-demand offline mode `AudioEngine.renderOffline(durationSeconds, sampleRate)` drives an `OfflineAudioContext` and returns an `AudioBuffer` for export / bounce workflows. ### 9. Parameter automation `AudioPlugin.setParameterCurve(id, curve: ParameterCurve)` schedules sample-accurate linear/exponential/target-at-time automation via the Web Audio API param scheduling methods. ### 10. Benchmarks `src/__benchmarks__/graph.bench.ts`, `queue.bench.ts`, `worklet.bench.ts` with mitata. --- ## File layout ```text browser/audio/audio-engine/ package.json tsconfig.json vitest.config.ts src/ index.ts error-codes.ts constants/ index.ts types/ common.ts common.md engine/ engine/ engine.ts engine.md engine.types.ts engine.test.ts index.ts engine-state/ engine-state.ts engine-state.md engine-state.test.ts index.ts engine-metrics/ engine-metrics.ts engine-metrics.md engine-metrics.test.ts index.ts graph/ graph/ graph.ts graph.md graph.types.ts graph.test.ts index.ts graph-connect/ graph-connect.ts graph-connect.md graph-connect.test.ts index.ts graph-traverse/ graph-traverse.ts graph-traverse.md graph-traverse.test.ts index.ts queue-engine/ queue-engine/ queue-engine.ts queue-engine.md queue-engine.types.ts queue-engine.test.ts index.ts cue/ cue.ts cue.md cue.test.ts index.ts scheduler/ scheduler.ts scheduler.md scheduler.test.ts index.ts worklet/ worklet-loader/ worklet-loader.ts worklet-loader.md worklet-loader.test.ts index.ts worklet-messaging/ worklet-messaging.ts worklet-messaging.md worklet-messaging.test.ts index.ts worklet-registry/ worklet-registry.ts worklet-registry.md worklet-registry.test.ts index.ts plugin/ plugin/ plugin.ts plugin.md plugin.types.ts plugin.test.ts index.ts plugin-registry/ plugin-registry.ts plugin-registry.md plugin-registry.types.ts plugin-registry.test.ts index.ts event-bus/ event-bus/ event-bus.ts event-bus.md event-bus.types.ts event-bus.test.ts index.ts __benchmarks__/ graph.bench.ts queue.bench.ts worklet.bench.ts tests/ integration.test.ts ``` --- ## Impact **Downstream packages that will consume `@suaveplan/audio-engine`**: `@suaveplan/audio-plugins-fx`, `@suaveplan/audio-controllers`, `@suaveplan/react-audio`, `@suaveplan/react-audio-player`, `@suaveplan/r3f-audio`, `@suaveplan/react-audio-stream`. **Breaking changes from main-branch API**: none. All main-branch call sites compile unchanged. New optional fields and methods are additive. **Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events` must be published to Verdaccio before this package can be built or tested.