Files
suaveplan/openspec/archive/2026-05-19-enterprise-react-audio-foundation/proposal.md
2026-05-27 23:29:53 +01:00

201 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OpenSpec Change: enterprise-react-audio
- **Change ID**: enterprise-react-audio
- **Package**: `@suaveplan/react-audio`
- **Category**: react
- **Target path**: `packages/react/react-audio`
- **Status**: PROPOSED
- **Date**: 2026-05-15
- **Design**: `./design.md`
- **Spec delta**: `./specs/react-audio/spec.md`
> Replaces the prior 2026-04-24 draft that was flagged `NEEDS-REWRITE` because it targeted the obsolete monolithic `@suaveplan/audio-engine` and predated the universal `audio-core` + `audio-timeline` + `audio-transport` split. See `../REACT_AUDIO_WAVE_REWRITE_COMPLETE.md`.
---
## Summary
Deliver `@suaveplan/react-audio` on genesis from scratch at enterprise quality. The package is the React surface for the unified audio architecture: it composes `@suaveplan/audio-engine-browser` (the `TimelineAdapter` rendering backend), `@suaveplan/audio-fx-plugin-base` (the `AudioFxPlugin` contract + `PluginChain` primitive + `AmbientState`), `@suaveplan/audio-timeline` (`TimelineEngine`), `@suaveplan/audio-transport` (musical clock), and `@suaveplan/audio-controllers` (MIDI binding) into:
1. **A single React context provider** (`AudioProvider`) that owns one engine adapter, one timeline engine, one transport, one project-level bus registry, and one plugin registry for the React subtree below it. Strict-mode safe (double-render does not produce double-engine). Disposable; unmounting the provider tears down the engine cleanly.
2. **A typed hook layer** that exposes the unified architecture without leaking engine types: `useAudioEngine`, `useTimeline`, `useTransport`, `useTrack(id)`, `usePluginChain(trackId)`, `useChainPlugin(trackId, slotIndex)`, `useParameter(plugin, paramId)` (subscribes to the parameter's per-block value with sample-bounded updates), `usePreset(plugin)`, `useBus(name)`, `useAnalyser(trackId | "master")`, `useTransportTime()`, `useEngineState()`, `useMidiController(controllerId)`. Hooks subscribe via the plugin / chain / transport event surfaces; updates are React-batched.
3. **A component library** that maps the contract to UI: `<TrackList>`, `<TrackRow>`, `<PluginChainView>` (drag-to-reorder, drop-to-insert), `<PluginSlot>` (bypass toggle, dry/wet for plugins that expose it, parameter knobs/sliders rendered from `parameterIndex`, preset selector), `<MasterMeter>`, `<TransportControls>` (play/pause/stop/seek/loop/tempo), `<Mixer>` (track strips with gain/pan/mute/solo/sends), `<MicrophoneControls>`, `<GLaDOSControls>`, `<MidiControllerSetup>` (learn-mode panel), `<PresetManager>` (chain snapshot save/restore via Zod-validated JSON).
4. **Zustand stores** for cross-component UI state that does not belong on the engine (panel open/closed, theme, visualization mode, drag state).
5. **First-class accessibility** — every parameter control is keyboard-driven (arrow keys for fine, page-up/down for coarse, shift for ×10, alt for ×0.1), every knob/slider has a labelled `aria-valuetext` with the unit, every chain UI is screen-reader navigable.
6. **Storybook stories** for every component with default + interactive + edge-case states (empty chain, bypassed plugin, mid-load reverb IR, sidechain bound).
7. **100% test coverage** with happy-dom + injected `OfflineAudioContext` per `audio-engine-browser`'s test seam.
The package contains **no DSP, no plugin instance state, no transport scheduler, no parameter automation logic** — every one of those lives below and is consumed by reference. React's job is presentation and event wiring.
---
## Current-state audit (genesis)
The package does not exist on genesis yet — this is a fresh build replacing the obsolete pre-tier 2026-04-24 draft. The upstream `react/media/react-audio/` is built on the monolithic `@suaveplan/audio-engine`; its public surface (12 hooks, 14 components, 2 stores) is captured here and re-derived against the unified architecture so prior consumer expectations carry over where they remain meaningful.
---
## Parity Checklist (vs upstream `../suaveplan/packages/react/media/react-audio/`)
| Capability | Upstream | Migrated | Notes |
|---|:---:|:---:|---|
| AudioProvider context | ✅ | ✅ | Now wraps `BrowserTimelineAdapter` + `PluginRegistry` + `BusRegistry` instead of the old monolithic engine. |
| useAudioEngine hook | ✅ | ⬆️ | Now exposes the `TimelineEngine` + `Transport` + chain APIs, not the obsolete `AudioEngine` class. |
| useAudioPlayer | ✅ | ⬆️ | Built on `AudioPlayerPlugin` from `@suaveplan/audio-fx-plugin-audio-player`; reads transport time from `useTransportTime`. |
| useAudioRecorder | ✅ | ✅ | Wraps `MediaRecorder` + the engine's tap-to-bus surface for in-app capture. |
| useAudioVisualizer | ✅ | ✅ | Reads `useAnalyser(trackId)` rather than reaching into the engine. |
| useDragDropQueue | ✅ | ✅ | Reuses the existing drag/drop utilities; unchanged surface. |
| useKeyboardShortcuts | ✅ | ✅ | Unchanged. |
| useMobileGestures | ✅ | ✅ | Unchanged. |
| usePlaylist | ✅ | ✅ | Built on `AudioPlayerPlugin`'s loaded-source queue. |
| usePlugin (single plugin) | ✅ | ✅ | Now over `AudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`. |
| usePluginChain | ✅ | ⬆️ | Now reflects the universal `PluginChain` primitive (insert / remove / move / replace / bypass / snapshot / restore / events). |
| usePresetManager | ✅ | ⬆️ | Plugin-level via `usePreset(plugin)` AND chain-level via `useChainSnapshot(chain)` for FX-rack-style "track presets". |
| useQueue | ✅ | ✅ | Unchanged. |
| audio-store (Zustand) | ✅ | ⬆️ | Slimmer; no playback state (that lives on transport). UI state only. |
| ui-store (Zustand) | ✅ | ✅ | Unchanged. |
| audio-dashboard component | ✅ | ✅ | Re-enabled; tree-shaking proven via subpath export bundle test. |
| audio-player component | ✅ | ✅ | Re-enabled; consumes `AudioPlayerPlugin`. |
| audio-provider component | ✅ | ⬆️ | Now also owns `PluginRegistry` and `BusRegistry`. |
| audio-visualizer | ✅ | ✅ | Same surface; reads `useAnalyser`. |
| basic-player | ✅ | ✅ | Slim variant. |
| premium-player | ✅ | ⬆️ | Adds chain UI for inserting FX plugins from the registry. |
| plugin-chain UI | ✅ | ⬆️ | Drag-to-reorder, drop-to-insert, bypass toggle, parameter rendering from `parameterIndex`. |
| plugin-controls (EQ/reverb/delay/compressor) | ✅ | ✅ | Replaced with generic `<PluginSlot>` that renders any `AudioFxPlugin` from its `parameters` declaration. EQ/reverb/delay/compressor get optional specialised UIs that opt in. |
| plugin-loader UI | ✅ | ⬆️ | Now reads from `PluginRegistry.list()`; supports dynamic registration. |
| mixer | ✅ | ✅ | Built on `MixerPlugin` from `@suaveplan/audio-fx-plugin-mixer`. |
| preset-manager | ✅ | ✅ | Chain-snapshot save/restore. |
| microphone-controls | ✅ | ✅ | Built on `MicrophonePlugin`. |
| vocoder-controls (GLaDOS) | ✅ | ✅ | Built on `GLaDOSPlugin`. |
| MIDI controller setup (NEW) | – | ⬆️ | Genesis addition; learn-mode panel for `MidiControllerPlugin` from `audio-controllers`. |
| Storybook coverage | ⚠ partial | ✅ | Every component gets default / interactive / edge-case stories. |
| Coverage | ~70 % | ✅ | 100/100/100/100 per genesis policy. |
No 🚫 rows.
---
## Available workspace deps on genesis
| Package | Role |
|---|---|
| `@suaveplan/audio-engine-browser` (`workspace:^`) | `BrowserTimelineAdapter` + `DefaultBusRegistry` |
| `@suaveplan/audio-timeline` (`workspace:^`) | `TimelineEngine`, `Track`, `Clip` |
| `@suaveplan/audio-transport` (`workspace:^`) | `Transport` musical clock |
| `@suaveplan/audio-fx-plugin-base` (`workspace:^`) | `AudioFxPlugin`, `PluginChain`, `PluginRegistry`, every plugin |
| `@suaveplan/audio-controllers` (`workspace:^`) | `MidiControllerPlugin`, `bindParameter` |
| `@suaveplan/events` (`workspace:^`) | Typed event subscriptions for hook updates |
| `@suaveplan/error` (`workspace:^`) | `defineCodes` + `AudioError` |
| `@suaveplan/types` (`workspace:^`) | `LoggerLike` |
| `@suaveplan/opentelemetry` (`workspace:^`) | OTel spans on lifecycle events |
| `react` (`peerDependency`) | React 19 peer |
| `zustand` (`catalog:state`) | UI store |
| `framer-motion` (`catalog:animation`) | Knob / slider animation |
| `lucide-react` (`catalog:ui`) | Icons |
| `zod` (`catalog:forms`) | Provider-options validation |
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `happy-dom`, `@storybook/react`, `@testing-library/react`, `@testing-library/user-event`, `playwright` (all `catalog:testing`).
---
## File layout
```text
packages/react/react-audio/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts
error-codes.ts (+ .test.ts .md)
provider/
audio-provider.tsx (+ .md .types.ts .test.tsx .stories.tsx)
provider-options.ts (+ .schemas.ts .md .test.ts)
index.ts
hooks/
use-audio-engine.ts (+ .md .test.tsx)
use-timeline.ts (+ .md .test.tsx)
use-transport.ts (+ .md .test.tsx)
use-transport-time.ts (+ .md .test.tsx)
use-engine-state.ts (+ .md .test.tsx)
use-track.ts (+ .md .test.tsx)
use-plugin-chain.ts (+ .md .test.tsx)
use-chain-plugin.ts (+ .md .test.tsx)
use-audio-fx-plugin.ts (+ .md .test.tsx)
use-parameter.ts (+ .md .test.tsx)
use-preset.ts (+ .md .test.tsx)
use-chain-snapshot.ts (+ .md .test.tsx)
use-bus.ts (+ .md .test.tsx)
use-analyser.ts (+ .md .test.tsx)
use-audio-recorder.ts (+ .md .test.tsx)
use-audio-visualizer.ts (+ .md .test.tsx)
use-drag-drop-queue.ts (+ .md .test.tsx)
use-keyboard-shortcuts.ts (+ .md .test.tsx)
use-mobile-gestures.ts (+ .md .test.tsx)
use-playlist.ts (+ .md .test.tsx)
use-queue.ts (+ .md .test.tsx)
use-midi-controller.ts (+ .md .test.tsx)
index.ts
stores/
audio-store.ts (+ .md .test.ts)
ui-store.ts (+ .md .test.ts)
index.ts
components/
transport-controls/ (.tsx .md .test.tsx .stories.tsx)
track-list/ ...
track-row/ ...
plugin-chain-view/ ...
plugin-slot/ ...
plugin-parameter-knob/ ...
plugin-parameter-slider/ ...
preset-selector/ ...
preset-manager/ ...
master-meter/ ...
mixer/ ...
audio-provider-component/ ...
audio-visualizer/ ...
audio-dashboard/ ...
basic-player/ ...
premium-player/ ...
audio-player/ ...
microphone-controls/ ...
vocoder-controls/ ...
plugin-loader/ ...
midi-controller-setup/ ...
a11y/
keyboard-bindings.ts (+ .md .test.ts)
aria-value-text.ts (+ .md .test.ts)
index.ts
__benchmarks__/
hook-rerender.bench.ts
chain-ui.bench.ts
tests/
integration.test.tsx
purity.test.ts
exports.test.ts
bundle-size/
hooks-only.size.test.ts
provider-only.size.test.ts
e2e/
audio.spec.ts
plugin-chain.spec.ts
midi-learn.spec.ts
```
---
## Impact
**Breaking changes**: none — fresh build.
**Prerequisites**: `audio-engine-browser`, `audio-fx-plugin-base` + every `audio-fx-plugin-*` package, `audio-controllers`, `audio-timeline`, `audio-transport`, `events`, `error`, `types`, `opentelemetry` shipped to Verdaccio.
### Affected dependents
- `@suaveplan/react-audio-player` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: separate change `enterprise-react-audio-player` consumes this package's hooks + provider; created against this spec, not retrofitted)
- `@suaveplan/react-audio-stream` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: separate change `enterprise-react-audio-stream` builds on this package's transport hooks; created against this spec)
- `@suaveplan/react-audio-fingerprint` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: separate change `enterprise-react-audio-fingerprint` consumes `useAnalyser`; created against this spec)
- `@suaveplan/r3f-audio` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: separate change `enterprise-gaming-r3f-audio` consumes `useTransport` + the game-audio building blocks from the `@suaveplan/audio-fx-plugin-*` family; created against this spec)
Once landed, any subsequent change to this package's public surface MUST run `bunx turbo test --filter=...@suaveplan/react-audio` per `openspec/AGENTS.md` §13 step 10.