first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
# `karaoke/use-lyrics`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`useLyrics(opts)` is the engine-agnostic karaoke hook. It owns the
active-line state, samples it at a fixed interval (100 ms by
default), and returns `{ lyrics, activeIndex, activeLyric }`. The
consumer hands in either pre-parsed `Lyric[]` or raw LRC source,
plus the current playback time in seconds. The hook deliberately
does not call `useTransportTime` itself so it can be driven from any
clock — `<LyricsDisplay>` wires the transport in for you, but a
preview panel running under a deterministic test clock can call this
hook directly.
The 100 ms re-bind ceiling matches the spec requirement that the
active lyric advance within 100 ms of the transport. Sampling at
that cadence (rather than per-frame) saves a substantial amount of
React commit work on songs with hundreds of lyric lines.
## Features
- Pure: no audio context, no global side-effect.
- Pre-parsed `lyrics` or raw `lrc` source — both accepted; if both
provided, `lyrics` wins (no double work).
- Configurable `intervalMs` (default 100). The hook re-evaluates
the active line on each tick and only commits when the index
actually changes — silent renders cost nothing.
- Returns `activeLyric: undefined` before the first cue or when
`lyrics` is empty.
## Usage: Basic
```tsx
import { useLyrics } from "@suaveplan/react-audio-player/karaoke/use-lyrics";
import { useTransportTime } from "@suaveplan/react-audio";
function Lyrics({ lrc }: { lrc: string }) {
const seconds = useTransportTime();
const { activeLyric } = useLyrics({ lrc, currentSec: seconds });
return <span aria-live="polite">{activeLyric?.text ?? "…"}</span>;
}
```
## Usage: Advanced
```tsx
import {
parseLrc,
useLyrics,
} from "@suaveplan/react-audio-player/karaoke";
import { useMemo, useState } from "react";
function StandaloneLyricPreview({ lrc }: { lrc: string }) {
// Parse once at the call site rather than on every mount.
const lyrics = useMemo(() => parseLrc(lrc), [lrc]);
// Deterministic clock — no <AudioProvider> required.
const [tick, setTick] = useState(0);
const { activeIndex, activeLyric, lyrics: ls } = useLyrics({
lyrics,
currentSec: tick * 0.5,
intervalMs: 50,
});
return (
<div>
<button type="button" onClick={() => setTick((t) => t + 1)}>
+0.5 s
</button>
<ol>
{ls.map((l, i) => (
<li
key={`${l.timestampSec}-${i}`}
data-active={i === activeIndex}
>
{l.text}
</li>
))}
</ol>
<p>Active: {activeLyric?.text ?? "—"}</p>
</div>
);
}
```
## API Reference
- `useLyrics(opts: UseLyricsOptions): UseLyricsResult`
- `UseLyricsOptions`:
- `lyrics?: readonly Lyric[]` — preferred.
- `lrc?: string` — parsed once per identity change.
- `intervalMs?: number` (default 100).
- `currentSec: number` — required.
- `UseLyricsResult`:
- `lyrics: readonly Lyric[]`,
- `activeIndex: number` (`-1` for "before first cue / empty"),
- `activeLyric: Lyric | undefined`.
## Implementation Notes
The hook owns one piece of React state — the active index — and one
`setInterval`. Both `lyrics` and `currentSec` are mirrored through
refs so the interval callback always sees the latest values without
re-binding the timer on every render. The interval only fires
`setActiveIndex` when the recomputed index actually differs, which
is the standard React micro-optimisation for high-frequency external
clocks.
`activeLyricIndex` (from the sibling `lrc-parser` module) is a
binary search; combined with the 100 ms sampling cadence, the hook's
steady-state work per frame is one comparison plus a "same index?"
branch. Long-form lyrics (10k+ lines) are not measurably more
expensive than short ones.
The hook lazily initialises its `activeIndex` state from
`activeLyricIndex(lyrics, currentSec)` so the first render — before
the effect mounts — already reflects the correct active line. This
matters for SSR-rendered previews and snapshot tests that read the
hook's output without ever ticking the interval.