# Capability Spec: react-audio-player **Package**: `@suaveplan/react-audio-player` **Path**: `packages/ui/react/react-audio-player/` **Status**: SHIPPED (karaoke + façade); multi-peer + R3F screensavers deferred > Every requirement carries a `**Verified by:**` line. Where a > requirement is intentionally deferred, the `> Deferred:` marker > records the reason; the requirement remains canonical and the > follow-up change must satisfy the same scenarios when it lands. ## Requirements ### Requirement: Karaoke surface — parser, hook, display The package SHALL export a `parseLrc(source: unknown, opts?: ParseLrcOptions): readonly Lyric[]` function, an `activeLyricIndex(lyrics, currentSec): number` binary search, a `useLyrics(opts)` React hook, and a `` component. The parser SHALL sort entries ascending by `timestampSec`, drop the LRC metadata tags `[ar:]` / `[ti:]` / `[al:]` / `[length:]` / `[by:]` / `[offset:]` / `[re:]` / `[ve:]`, skip lines without a leading timestamp, and expand multi-timestamp lines (`[00:10][00:20]hello`) when `expandMultiTimestamp` is true (default). `parseLrc` SHALL throw `REACT_AUDIO_PLAYER.LRC_PARSE_FAILED` for non-string input and for input exceeding `maxLength` (default 1 MiB). `` SHALL render one element per lyric with `data-active="true|false"` and set `aria-live="polite"` on the active line when `announceLyrics` is true (default). The active line SHALL update within `intervalMs` (default 100) of the consumer-supplied `currentSec` change. #### Scenario: parser sorts and skips invalid lines - **WHEN** `parseLrc("[ar:Foo]\n[00:30.00]Line A\nBroken line\n[00:10.50]Line B")` is called - **THEN** the result is `[{ timestampSec: 10.5, text: "Line B" }, { timestampSec: 30, text: "Line A" }]` #### Scenario: active lyric advances with currentSec - **WHEN** `` is rendered and `t` crosses a lyric's `timestampSec` - **THEN** within 100 ms the active line is the greatest entry whose `timestampSec` is `≤ t`, and the previous line is no longer highlighted **Verified by:** `src/karaoke/lrc-parser/lrc-parser.test.ts::"sorts by timestamp and skips invalid lines"` AND `src/karaoke/lyrics-display/lyrics-display.test.tsx::"updates active line on interval when currentSec rises"` --- ### Requirement: useLyrics is engine-agnostic `useLyrics(opts)` SHALL accept either `lyrics: readonly Lyric[]` or `lrc: string`, plus a required `currentSec: number`, and SHALL NOT call `useTransportTime` or any other engine-bound hook itself. Consumers wire the transport time on the call site. This guarantees the hook is callable outside `` — required for documentation pages, deterministic Playwright clocks, and standalone preview surfaces. #### Scenario: hook renders without a provider - **WHEN** `renderHook(() => useLyrics({ lyrics, currentSec: 5 }))` is called with no surrounding `` - **THEN** the hook returns `{ lyrics, activeIndex, activeLyric }` without throwing **Verified by:** `src/karaoke/use-lyrics/use-lyrics.test.tsx::"derives initial active index from currentSec"` --- ### Requirement: Re-export façade for the existing player surface The root barrel SHALL re-export the player UI from `@suaveplan/react-audio` via two channels: an `export * as ReactAudio from "@suaveplan/react-audio"` namespace export plus flat named-export aliases for the highest-traffic entry points (`AudioProvider`, `AudioPlayer`, `BasicPlayer`, `PremiumPlayer`, `TransportControls`, `Mixer`, `PluginChainView`, `PresetManager`, `TrackList`, `useTransport`, `useTransportTime`, `useAnalyser`, `usePlaylist`). The façade SHALL NOT redefine, wrap, or duplicate the underlying components; consumers reach the same singletons via ESM module identity. The root barrel SHALL NOT use bare `export *` (CLAUDE.md rule 10). #### Scenario: façade exposes player components - **WHEN** a consumer imports `{ AudioPlayer, ReactAudio }` from `@suaveplan/react-audio-player` - **THEN** `AudioPlayer === ReactAudio.AudioPlayer === (require("@suaveplan/react-audio")).AudioPlayer` **Verified by:** `tests/facade.test.tsx::"exposes the namespaced ReactAudio re-export"` AND `tests/facade.test.tsx::"exposes the flat alias façade"` --- ### Requirement: Structured error codes The package SHALL declare its codes via `defineCodes("REACT_AUDIO_PLAYER", [...])` producing `REACT_AUDIO_PLAYER.NO_PROVIDER`, `REACT_AUDIO_PLAYER.PLAYLIST_EMPTY`, `REACT_AUDIO_PLAYER.PERSISTENCE_FAILED`, `REACT_AUDIO_PLAYER.LRC_PARSE_FAILED`, and `REACT_AUDIO_PLAYER.INVALID_OPTIONS`. Export `ReactAudioPlayerErrorCode`, `REACT_AUDIO_PLAYER_ERROR_METADATA`, `registerReactAudioPlayerErrorCodes()`, and a `ReactAudioPlayerError` class extending `SuaveplanError`. Every registered code SHALL be reachable from a real call site. #### Scenario: every registered code is reachable - **WHEN** the test suite runs - **THEN** each code in `REACT_AUDIO_PLAYER_ERROR_METADATA` is asserted reachable **Verified by:** `src/error-codes/error-codes.test.ts::"every code is reachable from a thrown error"` --- ### Requirement: Subpath exports per surface `package.json` SHALL declare subpath exports for `.`, `./karaoke`, `./karaoke/lrc-parser`, `./karaoke/use-lyrics`, `./karaoke/lyrics-display`, and `./error-codes`. `sideEffects` SHALL be `false`. #### Scenario: every surface has a subpath export - **WHEN** the package's `package.json` is read - **THEN** every entry in the required-surfaces allowlist resolves to a defined export **Verified by:** `tests/exports.test.ts::"every public surface has a subpath entry"` --- ### Requirement: Runtime dependency allowlist Runtime `dependencies` SHALL be limited to `@suaveplan/error`, `@suaveplan/react-audio`, `@suaveplan/types`, and `tslib`. `react` and `react-dom` SHALL be peer dependencies. The narrow surface is deliberate: this package is a thin karaoke addition + re-export façade and pulls in zero direct audio-engine, plugin, or DSP runtime deps — those reach the consumer transitively through `@suaveplan/react-audio`. #### Scenario: runtime deps match allowlist - **WHEN** the package's `dependencies` map is enumerated - **THEN** the set equals `{ "@suaveplan/error", "@suaveplan/react-audio", "@suaveplan/types", "tslib" }` **Verified by:** `tests/exports.test.ts::"runtime dependency set matches allowlist"` --- ### Requirement: Multi-peer playback sync > **Deferred** (2026-05-23): the upstream `@suaveplan/react-audio-stream` > package does not yet exist on genesis. This requirement is > retained verbatim; the follow-up change that lands the upstream > package SHALL implement the `./sync/use-multi-peer-sync` subpath > here and satisfy the scenarios below without breaking the > currently-shipped public surface. `useMultiPeerSync` SHALL be exported only via the `./sync/use-multi-peer-sync` subpath and SHALL consume `@suaveplan/react-audio-stream` for WebRTC transport. Transport state synchronisation SHALL use the local `Transport` from the provider as source of truth; on peer disconnect the local transport SHALL continue uninterrupted. A peer reconnect SHALL resync the joining peer to the local transport's current position. #### Scenario: peer disconnect does not interrupt local playback - **WHEN** a peer disconnects mid-playback - **THEN** the local transport continues at the same position and the player UI remains responsive **Verified by:** (deferred — to be filled by the follow-up change) --- ### Requirement: R3F screensavers > **Deferred** (2026-05-23): the optional `./screensavers/*` > subpaths are not in this initial slice. The pre-existing player > components in `@suaveplan/react-audio` do not include R3F > screensavers either, so deferring here introduces no regression. > A focused follow-up SHALL add `` and > `` behind their respective optional > subpath exports and satisfy the scenario below. `` and `` SHALL be exported only via the `./screensavers/glb-viewer` and `./screensavers/shader-background` subpaths. The root barrel (`./`) and any non-screensaver subpath SHALL NOT pull in `@react-three/fiber` or `three`. #### Scenario: importing the root barrel does not bundle three.js - **WHEN** a consumer imports `@suaveplan/react-audio-player` (root) - **THEN** the tree-shaken bundle contains zero bytes from `three` or `@react-three/fiber` **Verified by:** (deferred — to be filled by the follow-up change)