Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/6xingyv/accompanist-lyrics-core/llms.txt

Use this file to discover all available pages before exploring further.

Accompanist Lyrics Core ships with parsers for TTML, Lyricify Syllable, Enhanced LRC, and Kugou KRC. If you need to consume a proprietary or niche format that none of those cover, you can implement the ILyricsParser interface and plug it straight into AutoParser — no forking required.

When to write a custom parser

Proprietary formats

Your backend or content provider delivers lyrics in a format unique to your service that no built-in parser recognises.

Niche community formats

Lesser-known open formats (e.g. ASS subtitles repurposed for music, plain-text with custom delimiters) that are not part of the built-in set.

Implementing ILyricsParser

The ILyricsParser interface has three methods, but you only need to implement canParse and ONE of the two parse overloads — each overload has a default body that delegates to the other:
interface ILyricsParser {
    fun canParse(content: String): Boolean

    // Implement ONE of the two parse overloads.
    // The other is provided as a default that delegates to whichever you implement.
    fun parse(lines: List<String>): SyncedLyrics  // default: joins lines, calls parse(String)
    fun parse(content: String): SyncedLyrics       // default: splits by \n, calls parse(List)
}
The two parse overloads have mutual default implementations — each delegates to the other. Implement whichever suits your parsing logic and the second will work automatically.
1
Implement canParse
2
canParse is called first, before any parsing work begins. It should return true only when the content is definitively your format. Keep it cheap — a header check or a regex on the first few lines is ideal.
3
override fun canParse(content: String): Boolean {
    return content.startsWith("##MY_FORMAT##")
}
4
Be as specific as possible. A loose check (e.g. “contains a pipe character”) risks matching content intended for a different parser.
5
Avoid expensive operations here — canParse can be called multiple times in a row as AutoParser tries each registered parser in order.
6
Implement parse
7
Choose the overload that best fits how your format is structured. If your format is naturally line-oriented, implement parse(lines: List<String>). If you need access to the raw string (e.g. for multi-line tokens or regex over the whole document), implement parse(content: String).
8
The return type is SyncedLyrics, which holds a list of ISyncedLine items. Use SyncedLine for simple timestamped text, or KaraokeLine.MainKaraokeLine for syllable-level karaoke data.
9
override fun parse(content: String): SyncedLyrics {
    val lines = content.lines()
        .filter { it.isNotBlank() && !it.startsWith("##") }
        .mapIndexedNotNull { _, line ->
            val parts = line.split("|")
            if (parts.size < 2) return@mapIndexedNotNull null
            val startMs = parts[0].toLongOrNull()?.toInt() ?: return@mapIndexedNotNull null
            val text = parts[1]
            SyncedLine(
                content = text,
                translation = null,
                start = startMs,
                end = startMs + 3000 // placeholder end time
            )
        }
    return SyncedLyrics(lines = lines)
}
10
SyncedLine requires end >= start. If your format omits end times, derive them from the next line’s start time, or use a fixed offset as a safe placeholder.
11
Register with AutoParser
12
Pass a list of ILyricsParser instances to AutoParser. Parsers are tried in the order they appear in the list — the first one whose canParse returns true wins.
13
Put your parser first if it should take priority over built-in parsers:
14
val autoParser = AutoParser(
    parsers = listOf(
        MyCustomParser(), // checked first
        TTMLParser(),
        LyricifySyllableParser,
        EnhancedLrcParser,
        KugouKrcParser
    )
)
15
Append it at the end if it should only match when all built-in parsers have declined:
16
val autoParser = AutoParser(
    parsers = listOf(
        TTMLParser(),
        LyricifySyllableParser,
        EnhancedLrcParser,
        KugouKrcParser,
        MyCustomParser() // checked last (fallback)
    )
)

Full working example

The example below parses a hypothetical format where the first line is a header tag (##MY_FORMAT##) and each subsequent line is <startMs>|<lyric text>:
##MY_FORMAT##
0|Welcome to the show
3200|The lights go down
6800|And the music starts
class MyCustomParser : ILyricsParser {

    override fun canParse(content: String): Boolean {
        return content.startsWith("##MY_FORMAT##")
    }

    override fun parse(content: String): SyncedLyrics {
        val lines = content.lines()
            .filter { it.isNotBlank() && !it.startsWith("##") }
            .mapIndexedNotNull { _, line ->
                val parts = line.split("|")
                if (parts.size < 2) return@mapIndexedNotNull null
                val startMs = parts[0].toLongOrNull()?.toInt() ?: return@mapIndexedNotNull null
                val text = parts[1]
                SyncedLine(
                    content = text,
                    translation = null,
                    start = startMs,
                    end = startMs + 3000 // placeholder end time
                )
            }
        return SyncedLyrics(lines = lines)
    }
}

// Register with AutoParser — custom parser runs first
val autoParser = AutoParser(
    parsers = listOf(
        MyCustomParser(),
        TTMLParser(),
        LyricifySyllableParser,
        EnhancedLrcParser,
        KugouKrcParser
    )
)

// Use exactly like the default AutoParser
val lyrics = autoParser.parse(rawContent)

Tips for robust parsers

Fast canParse checks

Check only the minimum needed — a magic byte sequence, a header tag, or the first non-empty line. Avoid regex over the full document.

Graceful null handling

Use mapIndexedNotNull or similar to skip malformed lines rather than throwing, so one bad line does not discard the entire file.

Derive missing end times

If your format has no explicit end timestamps, compute each line’s end from the next line’s start, and give the last line a fixed offset (e.g. +3 000 ms).

Return empty SyncedLyrics on failure

If parsing fails entirely, return SyncedLyrics(emptyList()) rather than throwing — consistent with how AutoParser itself handles unrecognised content.

Build docs developers (and LLMs) love