Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -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.