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,106 @@
# `karaoke/lrc-parser`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`parseLrc(source, opts?)` parses [LRC](https://en.wikipedia.org/wiki/LRC_(file_format))
karaoke source into a timestamp-sorted array of `Lyric` entries
suitable for the package's `<LyricsDisplay>` and any consumer-built
karaoke surface. The parser is universal — it has no React, DOM, or
audio-context dependency — so it is safe to call in workers, on the
server during SSR, or inside other Suaveplan tools that need to
preview lyric timing.
The companion `activeLyricIndex(lyrics, currentSec)` performs a
binary search for the active line and is the bridge between the
transport's `seconds` and the `<LyricsDisplay>` highlight state.
Both functions are pure; the parser throws structured
`ReactAudioPlayerError` for the two invalid-input cases.
## Features
- Recognises `[mm:ss]`, `[mm:ss.xx]`, `[mm:ss.xxx]`, and
`[h:mm:ss(.xxx)]` timestamp forms.
- Expands multi-timestamp lines (`[00:10][00:20]hello`) by default;
togglable via `expandMultiTimestamp`.
- Drops the LRC metadata tags `[ar:]`, `[ti:]`, `[al:]`, `[length:]`,
`[by:]`, `[offset:]`, `[re:]`, `[ve:]` — no extraction in v1.
- Skips empty lines, malformed lines, and lines with no leading
timestamp.
- Output is sorted ascending by `timestampSec` and frozen.
- Throws `REACT_AUDIO_PLAYER.LRC_PARSE_FAILED` for non-string input
or input exceeding `maxLength` (default 1 MiB).
- `activeLyricIndex` is O(log n) and allocation-free.
## Usage: Basic
```ts
import { parseLrc } from "@suaveplan/react-audio-player/karaoke/lrc-parser";
const lyrics = parseLrc(`
[ar:Test Artist]
[00:10.50]First line
[00:25.00]Second line
`);
// → [{ timestampSec: 10.5, text: "First line" },
// { timestampSec: 25.0, text: "Second line" }]
```
## Usage: Advanced
```tsx
import {
parseLrc,
activeLyricIndex,
} from "@suaveplan/react-audio-player/karaoke/lrc-parser";
import { useTransportTime } from "@suaveplan/react-audio";
import { useMemo } from "react";
function LyricLine({ source }: { source: string }) {
const lyrics = useMemo(
() => parseLrc(source, { expandMultiTimestamp: false }),
[source]
);
const seconds = useTransportTime();
const idx = activeLyricIndex(lyrics, seconds);
return (
<span aria-live="polite">
{idx >= 0 ? lyrics[idx].text : "…"}
</span>
);
}
```
## API Reference
- `parseLrc(source: unknown, opts?: ParseLrcOptions): readonly Lyric[]`
- `activeLyricIndex(lyrics: readonly Lyric[], currentSec: number): number`
- `Lyric` — `{ timestampSec: number; text: string }`.
- `ParseLrcOptions` — `maxLength?: number` (default 1 MiB),
`expandMultiTimestamp?: boolean` (default `true`).
## Implementation Notes
The timestamp regex `/\[(?:(\d{1,2}):)?(\d{1,3}):(\d{1,2})(?:[.:](\d{1,3}))?\]/g`
captures optional hours, minutes, seconds, and an optional
fractional component. The fractional value is padded right with
zeros to three digits then divided by 1000, so `.5` parses to 0.500
seconds, `.50` to 0.500, and `.500` to 0.500 — all equivalent. Only `.` is accepted as the
fractional separator; the `:` form would conflict with the
`[h:mm:ss]` extended timestamp grammar.
Multi-timestamp expansion walks the regex's global state, but only
honours timestamps that form a contiguous prefix of the line. A
bracketed sequence later in the line is treated as text — many
karaoke services emit `[00:10]hello [00:11]world` to mean two
separate lyric lines and this parser handles both layouts
correctly.
`activeLyricIndex` uses the upper-bound binary search idiom: pick
the greatest index whose timestamp is ≤ current. When `current` is
between two entries, the earlier entry wins; when `current`
precedes the first entry, `-1` is returned (so the consumer can
render an empty active line until the first cue).