229 lines
7.6 KiB
Markdown
229 lines
7.6 KiB
Markdown
# 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<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
|
|
|
|
```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<Record<BrowserAudioErrorCode, CodeMetadata>>
|
|
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.
|