EARHORNMUSIC UPnP · Android TV · Phones · Android Auto · GNU/Linux
Release 0.5.0

LrcParser.kt

Each line, with the release that last changed it.

0.5.01package dev.stave.lyrics
2
3/**
4 * Parser for the LRC subtitle format as used by LRCLIB.
5 *
6 * Handles standard `[mm:ss.xx]text` lines, multi-timestamp lines
7 * (`[00:12.34][00:45.67]chorus`) which expand into multiple LrcLines with the same
8 * text, metadata tags (`[ar:…]`, `[ti:…]`) which are discarded, and malformed lines
9 * which are silently skipped.
10 *
11 * Returns lines sorted ascending by timestamp.
12 */
13object LrcParser {
14
15 // `[mm:ss.xx]` or `[mm:ss.xxx]`. mm can be 2+ digits for tracks over 99 minutes.
16 private val TIMESTAMP = Regex("""\[(\d{1,3}):(\d{2})(?:\.(\d{1,3}))?\]""")
17
18 // `[ar:…]`, `[ti:…]`, `[length:…]` — metadata tags we discard.
19 private val META_TAG = Regex("""\[[a-zA-Z]+:[^\]]*\]""")
20
21 fun parse(lrc: String): List<LrcLine> {
22 val out = mutableListOf<LrcLine>()
23 for (raw in lrc.lineSequence()) {
24 val line = raw.trim()
25 if (line.isBlank() || META_TAG.matchEntire(line) != null) continue
26
27 val stamps = TIMESTAMP.findAll(line).toList()
28 if (stamps.isEmpty()) continue
29
30 // Text is whatever comes after the last timestamp on the line.
31 val lastStamp = stamps.last()
32 val text = line.substring(lastStamp.range.last + 1).trim()
33 // Skip lines with empty text — some sources pad with empty timestamp
34 // markers to align timing with instrumental sections.
35 if (text.isBlank()) continue
36
37 for (m in stamps) {
38 val mm = m.groupValues[1].toIntOrNull() ?: continue
39 val ss = m.groupValues[2].toIntOrNull() ?: continue
40 val fracStr = m.groupValues[3]
41 // Normalize fractional to 3 digits. "34" → 340, "345" → 345, "" → 0.
42 val frac = if (fracStr.isBlank()) 0 else {
43 val padded = fracStr.padEnd(3, '0').take(3)
44 padded.toIntOrNull() ?: 0
45 }
46 val millis = (mm * 60_000L) + (ss * 1000L) + frac.toLong()
47 if (millis >= 0) out.add(LrcLine.at(millis, text))
48 }
49 }
50 return out.sortedBy { it.start }
51 }
52}