/** One parsed LRC line: a timestamp (seconds) and its text. */ export interface LrcLine { time: number; text: string; } const TAG = /\[(\d{1,2}):(\d{2})(?:[.:](\d{1,3}))?\]/g; /** * Parse an LRC string into time-sorted lines. A single source line may carry * several timestamps (repeated lyric) — each becomes its own entry. Metadata * tags (`[ar:…]`, `[ti:…]`) and untimed lines are skipped; empty-text lines are * kept so the active-line highlight advances through instrumental gaps. */ export function parseLrc(lrc: string): LrcLine[] { const lines: LrcLine[] = []; for (const raw of lrc.split(/\r?\n/)) { const stamps = [...raw.matchAll(TAG)]; if (stamps.length === 0) continue; const text = raw.replace(/\[[^\]]*\]/g, '').trim(); for (const m of stamps) { const min = Number(m[1]); const sec = Number(m[2]); const frac = m[3] ? Number(m[3].padEnd(3, '0')) / 1000 : 0; lines.push({ time: min * 60 + sec + frac, text }); } } return lines.sort((a, b) => a.time - b.time); } /** * Index of the active line for a playback position (seconds) — the last line * whose timestamp has passed. Returns -1 before the first line. Assumes `lines` * is time-sorted (as `parseLrc` returns). */ export function activeLineIndex(lines: LrcLine[], position: number): number { let lo = 0; let hi = lines.length - 1; let result = -1; while (lo <= hi) { const mid = (lo + hi) >> 1; if (lines[mid].time <= position) { result = mid; lo = mid + 1; } else { hi = mid - 1; } } return result; }