Files
skills/docs/ui/react/react-audio-player/src/karaoke/lrc-parser/lrc-parser.md
2026-05-29 08:20:56 +01:00

3.8 KiB

karaoke/lrc-parser

(no submodules)

Purpose

parseLrc(source, opts?) parses LRC 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

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

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).