Initial Commit
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
# Design — enterprise-react-audio-player
|
||||
|
||||
`@suaveplan/react-audio-player` is a **UI suite** layered on `@suaveplan/react-audio`. Its job is to give a consumer a one-component drop-in player (`<BasicPlayer>` / `<AudioPlayer>` / `<PremiumPlayer>`) that takes a track URL or `Playlist` prop and renders a complete, accessible, beautiful audio player UI, with everything from the source plugin down handled by the unified architecture.
|
||||
|
||||
Cross-cutting architecture: see `openspec/changes/enterprise-audio-fx-plugin-base/design.md` and `openspec/changes/enterprise-react-audio/design.md`. This document covers only the player-specific concerns.
|
||||
|
||||
---
|
||||
|
||||
## 1. Player variants
|
||||
|
||||
Three player components share state via the `audio-player-provider` and a Zustand store, differing only in layout density and feature set:
|
||||
|
||||
- **`<BasicPlayer>`** — single-track playback, play/pause/seek/volume, no chain, no visualiser. ~5 KB tree-shaken.
|
||||
- **`<AudioPlayer>`** — adds playlist, 3-band EQ, waveform visualiser, repeat/shuffle. Re-enabled from upstream "commented-out" state via proper subpath exports.
|
||||
- **`<PremiumPlayer>`** — adds spectrum analyser, R3F screensavers, karaoke (LRC lyrics), multi-peer playback sync via `react-audio-stream`.
|
||||
|
||||
The three sit on the same store and the same `AudioPlayerPlugin`. Switching components without unmounting the provider preserves playback state. Each variant is a separate subpath export; importing one does not pull in the others.
|
||||
|
||||
---
|
||||
|
||||
## 2. Per-player chain layout
|
||||
|
||||
The player constructs a fixed track on the timeline (`player-track`) with a fixed chain:
|
||||
|
||||
```
|
||||
chain [
|
||||
AudioPlayerPlugin ← source plugin (index 0)
|
||||
EQPlugin (3-band, in BasicPlayer this slot is absent)
|
||||
...consumer-added FX slots...
|
||||
]
|
||||
```
|
||||
|
||||
`<AudioPlayer>` and `<PremiumPlayer>` expose a `<PluginChainView chain>` slot so consumers can add reverb, delay, etc. The chain owns the FX rack; the player owns the source.
|
||||
|
||||
The source plugin's `'ended'` event drives playlist advance via `usePlaylist`; the playlist hook calls `audioPlayerPlugin.load(nextSource)` and `audioPlayerPlugin.play()`.
|
||||
|
||||
---
|
||||
|
||||
## 3. LRC karaoke
|
||||
|
||||
LRC is parsed once into `{ timestampSec, text }[]` and stored in `useLyricsStore`. `<LyricsDisplay>` reads `useTransportTime()` at frame rate and binary-searches the lyric list to find the active line. Active line highlight uses `framer-motion` with `prefers-reduced-motion` honoured.
|
||||
|
||||
```ts
|
||||
const activeIndex = useMemo(
|
||||
() => binarySearchActive(lyrics, currentTimeSec),
|
||||
[lyrics, Math.floor(currentTimeSec * 10)] // re-bind every 100 ms
|
||||
)
|
||||
```
|
||||
|
||||
The binary search is O(log n); a 1000-line LRC re-evaluates in ~10 comparisons per 100 ms — well below 1 ms total.
|
||||
|
||||
---
|
||||
|
||||
## 4. Visualisers
|
||||
|
||||
`<WaveformVisualizer>` and `<SpectrumAnalyzer>` both consume `useAnalyser(trackId | 'master')` from `react-audio`. The hook samples the analyser at frame rate; the visualisers render to `<canvas>` (waveform) or `<canvas>` with FFT-bin-to-pixel mapping (spectrum). No new analyser instance is constructed; both share the chain's master analyser node.
|
||||
|
||||
---
|
||||
|
||||
## 5. R3F screensavers (optional)
|
||||
|
||||
`<GLBViewer src animation?>` and `<ShaderBackground shader uniforms>` are R3F components gated to the `./screensavers/*` subpath. They are not pulled in unless explicitly imported. The screensaver wraps the player UI as a backdrop; opacity and visibility are React-controlled.
|
||||
|
||||
Three.js is a peer dep — non-screensaver consumers pay nothing.
|
||||
|
||||
---
|
||||
|
||||
## 6. Multi-peer sync (optional)
|
||||
|
||||
`<PremiumPlayer multiPeer={{ roomId, peers }}>` consumes `useMultiPeerSync` from this package, which in turn consumes hooks from `@suaveplan/react-audio-stream` to negotiate WebRTC peers, share transport `play/pause/seek` events across peers via a control channel, and stream the player's audio output to peers via the engine's tap-to-bus surface. The control protocol uses `@suaveplan/audio-transport` as the source of truth for transport state, so a peer disconnect does not desync the local player.
|
||||
|
||||
`react-audio-stream` is a peer dep of this subpath — basic/audio players don't pay for it.
|
||||
|
||||
---
|
||||
|
||||
## 7. Persistence
|
||||
|
||||
A `PlayerPersistenceStore` interface backs:
|
||||
|
||||
- the playlist (`{ tracks: Track[], position: number, repeat: 'off'|'one'|'all', shuffle: boolean }`),
|
||||
- the EQ preset (`{ schemaVersion, bands: [...] }` validated against `EQPlugin.schema`),
|
||||
- the user's volume + mute state.
|
||||
|
||||
Default implementation: `localStorage` keyed on the provider's `id` prop. Consumers may inject a different store (cloud-sync, IndexedDB, SSR cookie). Reads return defaults; writes are debounced (300 ms).
|
||||
|
||||
---
|
||||
|
||||
## 8. Accessibility
|
||||
|
||||
Inherits everything from `react-audio`: keyboard, ARIA, reduced-motion. Player-specific additions:
|
||||
|
||||
- `<ProgressBar>` is a `role="slider"` keyboard-seekable with arrow keys for 5 s, page-up/down for 30 s.
|
||||
- `<Queue>` is `role="listbox"` with up/down navigation and `Enter` to play.
|
||||
- `<LyricsDisplay>` exposes the active lyric as `aria-live="polite"` so screen readers announce lyric changes (configurable off via `announceLyrics={false}`).
|
||||
|
||||
---
|
||||
|
||||
## 9. Decisions and trade-offs
|
||||
|
||||
- **Three variants, one source plugin, one chain.** Sharing the source + chain across variants means switching variants doesn't restart playback.
|
||||
- **`<AudioPlayer>` and `<PremiumPlayer>` expose the chain via `<PluginChainView>`.** Consumers extend FX without breaking the player UI.
|
||||
- **LRC binary search re-binds every 100 ms.** Frame-rate re-bind (every 16 ms) is unnecessary; lyrics don't change that fast.
|
||||
- **Screensavers and multi-peer sync are opt-in subpaths.** R3F and WebRTC peer deps are paid only by consumers that actually use them.
|
||||
- **Persistence is injectable.** Cloud sync, SSR session, IndexedDB — all the same interface; localStorage is just the default.
|
||||
- **No new analyser node.** Reuse the engine's master analyser; one instance per chain, not per visualiser.
|
||||
Reference in New Issue
Block a user