7.6 KiB
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
interface BrowserAdapterOptions {
context?: BaseAudioContext
latencyHint?: AudioContextLatencyCategory
sampleRate?: number
lookAhead?: number
ppqn?: number
}
interface WorkletManagerOptions {
allowedUrls?: string[]
}
interface WorkletModuleOptions {
credentials?: RequestCredentials
}
interface AudioGraphSnapshot {
nodes: Record<string, AudioNodeType>
connections: Array<{ source: string; destination: string; outputIndex?: number; inputIndex?: number }>
}
class BrowserTimelineAdapter implements TimelineAdapter {
constructor(options?: BrowserAdapterOptions)
init(options?: { signal?: AbortSignal }): Promise<void>
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<void>
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<void>
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<AudioBuffer>
Error codes
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<Record<BrowserAudioErrorCode, CodeMetadata>>
function registerBrowserAudioErrorCodes(): void
Zod schemas
BrowserAdapterOptionsSchema
WorkletManagerOptionsSchema
WorkletModuleOptionsSchema
AudioGraphSnapshotSchema
File layout
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.