# 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 (`` / `` / ``) 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: - **``** — single-track playback, play/pause/seek/volume, no chain, no visualiser. ~5 KB tree-shaken. - **``** — adds playlist, 3-band EQ, waveform visualiser, repeat/shuffle. Re-enabled from upstream "commented-out" state via proper subpath exports. - **``** — 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... ] ``` `` and `` expose a `` 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`. `` 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 `` and `` both consume `useAnalyser(trackId | 'master')` from `react-audio`. The hook samples the analyser at frame rate; the visualisers render to `` (waveform) or `` 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) `` and `` 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) `` 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: - `` is a `role="slider"` keyboard-seekable with arrow keys for 5 s, page-up/down for 30 s. - `` is `role="listbox"` with up/down navigation and `Enter` to play. - `` 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. - **`` and `` expose the chain via ``.** 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.