# `karaoke/lrc-parser` _(no submodules)_ ## 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 `` 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 `` 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 ( {idx >= 0 ? lyrics[idx].text : "…"} ); } ``` ## 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).