# OpenSpec Change: enterprise-audio-engine-browser - **Change ID**: enterprise-audio-engine-browser - **Package**: `@suaveplan/audio-engine-browser` - **Category**: browser - **Target path**: `packages/audio/audio-engine-browser` - **Status**: PROPOSED - **Date**: 2026-05-14 - **Design**: `./design.md` - **Spec delta**: `./specs/audio-engine-browser/spec.md` --- ## Summary Deliver `@suaveplan/audio-engine-browser` on the genesis branch from scratch at enterprise quality. The package is a Web Audio API rendering backend that implements `TimelineAdapter` to connect the universal `TimelineEngine` to the browser's audio output. It manages `AudioContext` lifecycle (via `standardized-audio-context`), `AudioWorklet` loading, and dual-mode FX plugin hosting. An `Html5AudioFallback` is provided for environments without Web Audio API. This is the only browser-only package in the audio core; it bridges universal data to browser-specific rendering and replaces old browser-only openspecs with a new adapter-based architecture. --- ## Current-state audit (genesis) This is a new architecture replacing old browser-only openspecs. No prior source on genesis for this specific package; fresh build with an adapter-based design that delegates timeline logic to `@suaveplan/audio-timeline`. --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/audio-core` (`workspace:^`) | Shared audio primitives and FX plugin interfaces | | `@suaveplan/audio-timeline` (`workspace:^`) | `TimelineEngine`, `TimelineAdapter`, `Track`, `Clip` | | `@suaveplan/audio-transport` (`workspace:^`) | `Transport` for musical clock integration | | `@suaveplan/audio-fx-plugin-base` (`workspace:^`) | `AudioFxPlugin` interface for FX hosting | | `@suaveplan/error` (`workspace:^`) | Base class for `BrowserAudioError`; `AUDIO_ENGINE_BROWSER.*` codes | | `@suaveplan/events` (`workspace:^`) | Typed event emitter for adapter events | | `standardized-audio-context` | Cross-browser `AudioContext` / `OfflineAudioContext` shim | Dev deps: `vitest`, `@vitest/coverage-istanbul`, `happy-dom` (all `catalog:testing`). --- ## Public API ### Core types ```ts interface BrowserAdapterOptions { context?: BaseAudioContext latencyHint?: AudioContextLatencyCategory sampleRate?: number lookAhead?: number ppqn?: number } interface WorkletManagerOptions { allowedUrls?: string[] } interface WorkletModuleOptions { credentials?: RequestCredentials } interface AudioGraphSnapshot { nodes: Record connections: Array<{ source: string; destination: string; outputIndex?: number; inputIndex?: number }> } class BrowserTimelineAdapter implements TimelineAdapter { constructor(options?: BrowserAdapterOptions) init(options?: { signal?: AbortSignal }): Promise setTracks(tracks: Track[]): void addTrack(track: Track): void removeTrack(trackId: string): void updateTrack(trackId: string, track: Track): void play(time?: number): void pause(time?: number): void stop(time?: number): void seek(time: number): void getCurrentTime(): number setLoop(start: number, end: number, enabled: boolean): void setMasterVolume(volume: number): void setTrackVolume(trackId: string, volume: number): void setTrackMute(trackId: string, muted: boolean): void setTrackSolo(trackId: string, soloed: boolean): void setTrackPan(trackId: string, pan: number): void setTempo(bpm: number, atTick?: number): void ticksToSeconds(ticks: number): number secondsToTicks(seconds: number): number readonly lookAhead: number readonly ppqn: number dispose(): void } class WebAudioGraph { constructor(context: BaseAudioContext) 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 snapshot(): AudioGraphSnapshot restore(snapshot: AudioGraphSnapshot): void } class AudioWorkletManager { constructor(context: BaseAudioContext, options?: WorkletManagerOptions) 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 } class Html5AudioFallback implements TimelineAdapter { constructor(options?: { volume?: number; loop?: boolean }) init(options?: { signal?: AbortSignal }): Promise setTracks(tracks: Track[]): void addTrack(track: Track): void removeTrack(trackId: string): void updateTrack(trackId: string, track: Track): void play(time?: number): void pause(time?: number): void stop(time?: number): void seek(time: number): void getCurrentTime(): number setLoop(start: number, end: number, enabled: boolean): void setMasterVolume(volume: number): void setTrackVolume(trackId: string, volume: number): void setTrackMute(trackId: string, muted: boolean): void setTrackSolo(trackId: string, soloed: boolean): void setTrackPan(trackId: string, pan: number): void setTempo(bpm: number, atTick?: number): void readonly lookAhead: number readonly ppqn: number dispose(): void } async function renderOffline( timeline: TimelineEngine, duration: number, sampleRate: number ): Promise ``` ### Error codes ```ts type BrowserAudioErrorCode = | 'AUDIO_ENGINE_BROWSER.CONTEXT_FAILED' | 'AUDIO_ENGINE_BROWSER.WORKLET_LOAD_FAILED' | 'AUDIO_ENGINE_BROWSER.TRACK_NOT_FOUND' | 'AUDIO_ENGINE_BROWSER.NODE_NOT_FOUND' | 'AUDIO_ENGINE_BROWSER.OFFLINE_RENDER_FAILED' | 'AUDIO_ENGINE_BROWSER.ALREADY_DISPOSED' const AUDIO_ENGINE_BROWSER_ERROR_METADATA: Readonly> function registerBrowserAudioErrorCodes(): void ``` ### Zod schemas ```ts BrowserAdapterOptionsSchema WorkletManagerOptionsSchema WorkletModuleOptionsSchema AudioGraphSnapshotSchema ``` --- ## File layout ```text packages/audio/audio-engine-browser/ package.json tsconfig.json vitest.config.ts src/ index.ts error-codes.ts adapter/ browser-adapter.ts browser-adapter.md browser-adapter.types.ts browser-adapter.test.ts index.ts graph/ web-audio-graph.ts web-audio-graph.md web-audio-graph.types.ts web-audio-graph.test.ts index.ts worklet/ worklet-manager.ts worklet-manager.md worklet-manager.types.ts worklet-manager.test.ts index.ts fallback/ html5-fallback.ts html5-fallback.md html5-fallback.test.ts index.ts offline/ offline-render.ts offline-render.md offline-render.test.ts index.ts __benchmarks__/ graph.bench.ts worklet.bench.ts tests/ integration.test.ts ``` --- ## Impact **Breaking changes**: none — fresh build replacing legacy browser-only openspecs; the package does not yet exist on genesis. **Prerequisites**: `@suaveplan/audio-core`, `@suaveplan/audio-timeline`, `@suaveplan/audio-transport`, `@suaveplan/audio-fx-plugin-base`, `@suaveplan/error`, `@suaveplan/events` published to Verdaccio. ### Affected dependents No dependents in this batch. Consumed by `@suaveplan/react-audio*` and `@suaveplan/r3f-audio`, which land in a later wave (verified by `bunx turbo ls --filter=...@suaveplan/audio-engine-browser`). Once landed, any subsequent change to this package's public surface MUST run `bunx turbo test --filter=...@suaveplan/audio-engine-browser` per `openspec/AGENTS.md` §13 step 10.