A Swift 6 package for parsing LRC and LRCX lyrics files and querying lyrics by playback position.
This library is generated via AI using GitHub Copilot
- LRC — standard line-level timestamps, metadata tags, ruby/furigana annotations, repeated-chorus multi-timestamps
- LRCX — word-level (karaoke) timestamps,
[<ts]unfill/reverse-karaoke effect - NetEase LRCX extensions —
[tt]progressive character-highlighting,[tr:lang]translations - Binary-search time queries:
activeLine(at:),activeLines(at:),activeWord(at:),highlightedCharacterCount(atOffset:) - Silence detection:
isLyricsSilent(at:)— detects gaps before lyrics start, instrumental breaks, and line end-times - Full
[offset:]support; 2-digit centisecond and 3-digit millisecond timestamp precision - Swift 6,
Sendablethroughout, zero dependencies
Add LyrixLib as a Swift Package Manager dependency:
// Package.swift
dependencies: [
.package(url: "https://github.com/RustyKnight/LyrixLib.git", from: "1.0.0"),
],
targets: [
.target(name: "MyTarget", dependencies: ["LyrixLib"]),
]import LyrixLib
let parser = LyrixParser()
// Parse from a file URL — format (.lrc / .lrcx) is inferred from the extension
let doc = try parser.parse(contentsOf: url)
// Or parse a string directly
let doc = try parser.parse(content, format: .lrcx)
// Query the active line at a playback position (milliseconds)
let line = doc.activeLine(at: 32_566)
print(line?.displayText) // plain text, no annotations
// Also get the next upcoming line for preview
let (current, next) = doc.activeLines(at: 32_566)[ti:Song Title]
[ar:Artist Name]
[al:Album Name]
[offset:0]
[00:32.56]This is the end
[00:37.18]Hold your breath and count to ten
Each line begins with a line timestamp, followed by per-word timestamps:
[01:23.20][01:23.20]Let [01:23.70]the [01:24.07]sky [01:24.57]fall
[01:29.26][01:29.26]We [01:29.51]will [01:29.93]stand [01:30.63]tall
The [<ts] syntax marks a word for the unfill (reverse-karaoke) effect:
[00:05.00][00:05.00]Normal [<00:05.50]reverse [00:06.00]normal
Real-world LRCX files often include translation and character-timing sub-tags at the same timestamp as the main lyric:
[00:32.56]This is the end
[00:32.56][tr:zh-Hans]这便是终结
[00:32.56][tt]<0,0><190,5><498,8><877,12><1565,15><1565>
[tr:lang]— translated lyric stored inLyrixLine.translation[tt]<offset_ms,char_count>…— progressive character-highlight steps stored inLyrixLine.characterTiming
let parser = LyrixParser()
// From a file URL (extension determines format)
let doc = try parser.parse(contentsOf: URL(filePath: "song.lrcx"))
// From a String with an explicit format
let doc = try parser.parse(content, format: .lrc)
let doc = try parser.parse(content, format: .lrcx)doc.metadata.title // String?
doc.metadata.artist // String?
doc.metadata.album // String?
doc.metadata.author // String? — songwriter ([au:])
doc.metadata.lyricist // String? — lyricist ([lr:])
doc.metadata.createdBy // String? — LRC file creator ([by:])
doc.metadata.offset // Int — global ms shift, already applied to all timestamps
doc.metadata.length // String? — e.g. "3:45"// Active line at a given playback position
let line: LyrixLine? = doc.activeLine(at: milliseconds)
// Active line + the immediately following line (useful for preview)
let (current, next): (LyrixLine?, LyrixLine?) = doc.activeLines(at: milliseconds)
// All lines, sorted ascending by timestamp
let lines: [LyrixLine] = doc.lines
// Silence detection — true before the first line, on instrumental breaks,
// and after a line's end time (when determinable from [tt] or LRCX word timing)
let silent: Bool = doc.isLyricsSilent(at: milliseconds)line.timestamp // Int — start time in ms (offset already applied)
line.displayText // String — plain text, no ruby/annotation markup
line.text // String — text preserving {ruby} notation
line.isInstrumentalBreak // Bool — true when the line has no text (silent gap)
line.translation // String? — from [tr:lang] (LRCX)
line.characterTiming // [CharacterTiming] — from [tt] (LRCX)
line.lineDurationMilliseconds // Int? — total line duration from [tt] trailing marker (LRCX)
line.words // [WordSegment] — word-level segments (LRCX karaoke)
line.rubySegments // [RubySegment] — line-level ruby segments (LRC)
line.wordCharBoundaries // [Int] — character end-positions in displayText for each word (LRCX)// Active word segment at a given playback position
let word: WordSegment? = line.activeWord(at: milliseconds)
word.timestamp // Int — word start time in ms
word.displayText // String — plain word text
word.isUnfill // Bool — true for [<ts] reverse-fill words
word.rubySegments // [RubySegment]// Number of characters to highlight, given ms elapsed since the line started
let offset = currentMs - line.timestamp
let count = line.highlightedCharacterCount(atOffset: offset)
// The character-timing steps themselves
for step in line.characterTiming {
print("\(step.offsetMilliseconds) ms → highlight \(step.characterCount) chars")
}// RubySegment appears in both LyrixLine.rubySegments and WordSegment.rubySegments
segment.base // String — displayed base text (e.g. "心")
segment.ruby // String? — annotation text (e.g. "こころ"), nil if absent
segment.displayText // String — alias for .baseIn LRC/LRCX files, ruby is encoded inline as base{ruby} — for example 心{こころ}.
LyrixParser throws LyrixError on failure:
do {
let doc = try LyrixParser().parse(contentsOf: url)
} catch LyrixError.fileReadFailed(let path) {
print("Could not read file: \(path)")
} catch LyrixError.emptyContent {
print("The lyrics content was empty")
}| Case | Thrown when |
|---|---|
emptyContent |
The content string (or file) is empty |
fileReadFailed(String) |
The file at the given path could not be read |
import LyrixLib
func syncToPlayback(url: URL, positionMs: Int) throws {
let doc = try LyrixParser().parse(contentsOf: url)
let (current, next) = doc.activeLines(at: positionMs)
print("Now: \(current?.displayText ?? "(intro)")")
print("Next: \(next?.displayText ?? "")")
}import LyrixLib
func karaokeHighlight(url: URL, positionMs: Int) throws {
let doc = try LyrixParser().parse(contentsOf: url)
guard let line = doc.activeLine(at: positionMs) else { return }
// Highlight the active word
let word = line.activeWord(at: positionMs)
print("Line: \(line.displayText)")
print("Word: \(word?.displayText ?? "")")
// wordCharBoundaries gives character end-positions in displayText for each word,
// accounting for any synthetic inter-word spaces inserted by displayText.
let boundaries = line.wordCharBoundaries
print("Word boundaries: \(boundaries)")
}import LyrixLib
func characterBar(url: URL, positionMs: Int) throws {
let doc = try LyrixParser().parse(contentsOf: url)
guard let line = doc.activeLine(at: positionMs) else { return }
let offset = positionMs - line.timestamp
let count = line.highlightedCharacterCount(atOffset: offset)
let total = line.displayText.count
let filled = String(repeating: "█", count: count)
let empty = String(repeating: "░", count: total - count)
print("\(filled)\(empty) \(line.displayText)")
// Show translation if available
if let translation = line.translation {
print(" ↳ \(translation)")
}
}import LyrixLib
func printWithFurigana(url: URL, positionMs: Int) throws {
let doc = try LyrixParser().parse(contentsOf: url)
guard let line = doc.activeLine(at: positionMs) else { return }
for segment in line.rubySegments {
if let ruby = segment.ruby {
print("\(segment.base)[\(ruby)]", terminator: "")
} else {
print(segment.base, terminator: "")
}
}
print()
}isLyricsSilent(at:) returns true when no lyric content is audible — useful for fading UI elements or pausing animations during intros, outros, and instrumental breaks.
import LyrixLib
func updateLyricsUI(doc: LyrixDocument, positionMs: Int) {
if doc.isLyricsSilent(at: positionMs) {
// Hide or fade lyrics display
return
}
let (current, next) = doc.activeLines(at: positionMs)
print("Now: \(current?.displayText ?? "")")
print("Next: \(next?.displayText ?? "")")
}Silence is detected in three ways:
- The position precedes the first lyric line (intro)
- The active line is an instrumental break (empty line in the file)
- The active line has ended, as determined by the
[tt]duration marker or a trailing empty word segment in LRCX files
Note: Plain LRC files have no line end-time information, so
isLyricsSilentonly returnstruebefore the first line or on instrumental breaks in that format.
MIT