Playback
LrcDocumentExtensions exposes the playback hot path — the methods a karaoke /
lyric-display consumer typically calls per video frame.
The offset model
Documents store [offset:N] verbatim in
LrcMetadata.Offset. Line timestamps
are not mutated at parse time. This preserves round-trip fidelity — re-writing
the document emits the original [offset:N] tag rather than collapsing it
into mutated timestamps.
When you query the document for "what's playing now", apply the offset on the way out:
TimeSpan effective = doc.GetEffectiveTime(line.Timestamp);
LrcLine? current = doc.FindLineAt(player.Position); // already factors offset
GetEffectiveTime
returns TimeSpan (signed) rather than LrcTimestamp because a large negative
offset can shift past zero — LrcTimestamp cannot represent that.
Find the currently-singing line
// O(log n) binary search; null if Lines is empty or position precedes every line.
LrcLine? current = doc.FindLineAt(player.Position);
string text = current switch
{
LrcPlainLine plain => plain.Text,
LrcEnhancedLine enh => enh.GetText(), // joins all words
null => "",
_ => current.GetText(),
};
FindLineAt returns the
greatest line whose effective timestamp is ≤ the position. So it returns
the line that's currently being sung, even if the position is between two lines.
Highlight the current word in an enhanced line
if (doc.FindLineAt(player.Position) is LrcEnhancedLine enhanced)
{
var posTs = LrcTimestamp.FromTimeSpan(player.Position - doc.Metadata.Offset);
LrcWord? currentWord = null;
foreach (var word in enhanced.Words)
if (word.Timestamp <= posTs) currentWord = word; else break;
// Render currentWord highlighted, others plain.
}
Show a window of upcoming lines
var window = doc.LinesInRange(
start: player.Position,
end: player.Position + TimeSpan.FromSeconds(10));
foreach (var line in window)
Console.WriteLine(line.GetText());
LinesInRange is a
linear scan (yields lazily). It's cheap enough for per-frame use on documents up
to thousands of lines.
Voice state
Each line carries EffectiveVoice
already resolved — the parser propagates the last explicit M: / F: / D:
marker forward across subsequent untagged lines. You don't need to track voice
state yourself.
var line = (LrcPlainLine)doc.FindLineAt(player.Position)!;
display.SetSpeakerColor(line.EffectiveVoice switch
{
LrcVoice.Male => Brushes.SkyBlue,
LrcVoice.Female => Brushes.Pink,
LrcVoice.Duet => Brushes.Gold,
_ => Brushes.White,
});
Performance notes
FindLineAtis O(log n) — call it freely (60 fps × thousands of lines = no problem).LinesInRangeis O(n) but allocation-free per yielded item; the iterator state machine is one allocation per call.GetTexton an enhanced line usesstring.Createto build the result with a single allocation sized to the exact total length.