Table of Contents

Getting started

Install

dotnet add package ModernLrc

Requires .NET 10 SDK.

Parse

The canonical entry is LrcParser.Parse(string). The result always carries a Document plus every Diagnostic that was emitted during the scan.

using ModernLrc;
using ModernLrc.Model;

const string source = """
[ti:Demo Song]
[ar:Tester]
[00:01.00]intro line
[00:05.20]F: female voice
[00:08.50]<00:08.50>word <00:09.00>by <00:09.50>word
""";

var result = LrcParser.Parse(source);

Console.WriteLine(result.Document.Metadata.Title);    // "Demo Song"
Console.WriteLine(result.Document.Lines.Count);       // 3
Console.WriteLine(result.HasErrors);                  // false

Inspecting lines

foreach (var line in result.Document.Lines)
{
    var t = line.Timestamp;
    switch (line)
    {
        case LrcPlainLine plain:
            Console.WriteLine($"[{t}] {plain.EffectiveVoice}: {plain.Text}");
            break;

        case LrcEnhancedLine enhanced:
            foreach (var word in enhanced.Words)
                Console.Write($"<{word.Timestamp}>{word.Text}");
            Console.WriteLine();
            break;
    }
}

Inspecting diagnostics

Every recoverable issue is recorded — even on HasErrors == false you can find informational notes (e.g. LRC0030 for non-standard timestamp variants):

foreach (var d in result.Diagnostics)
    Console.WriteLine($"{d.Code} {d.Severity} L{d.Line}:C{d.Column} — {d.Message}");

See the diagnostics reference for the full code catalogue.

Author

var doc = new LrcDocumentBuilder()
    .WithTitle("New Song")
    .WithArtist("Artist")
    .WithOffset(TimeSpan.FromMilliseconds(-200))
    .AddLine("00:01.00", "first line")
    .AddLine("00:05.00", "she sings", LrcVoice.Female)
    .Build();

The builder is mutable and chainable. Build() is idempotent — call it as many times as you want; it doesn't reset the builder's state.

Write

// To a string
string lrc = LrcWriter.Write(doc);

// To a stream (UTF-8 by default; takes a fast path that avoids any intermediate string)
using var fs = File.Create("song.lrc");
LrcWriter.Write(doc, fs);

// Atomic file write (temp + rename)
LrcWriter.WriteFile(doc, "song.lrc");

// Zero-allocation: render straight into your own buffer writer
var bw = new System.Buffers.ArrayBufferWriter<byte>();
LrcWriter.Write(doc, bw);
ReadOnlySpan<byte> bytes = bw.WrittenSpan;

Round-trip

The parser and writer are designed to round-trip losslessly for the supported grammar (modulo whitespace inside the line that wasn't normalized at parse time). Specifically: voice markers, multi-timestamp lines, enhanced word timing, metadata block, and the verbatim [offset:N] tag all survive a parse → write cycle. Literal lyric text that itself contains valid LRC markers is interpreted as LRC syntax when parsed again; escape or filter such display text before authoring if you need it to remain plain.

var doc1 = LrcParser.Parse(source).Document;
var rendered = LrcWriter.Write(doc1);
var doc2 = LrcParser.Parse(rendered).Document;
// doc1.Lines structurally equal doc2.Lines

Next steps