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