Task 362: .LRC File Format

Task 362: .LRC File Format

.LRC File Format Specifications

The .LRC file format is a text-based format used to synchronize song lyrics with audio files. It is not a binary format and has no specific file signature or MIME type beyond being plain text (typically UTF-8 or ASCII encoded). The structure consists of optional metadata tags (ID tags) at the beginning, followed by time-stamped lyric lines. Time stamps are in the format [mm:ss.xx], where mm is minutes, ss is seconds, and xx is hundredths of a second. Multiple time stamps can appear on the same line for repeating lyrics. There is also an enhanced variant for word-level synchronization using mm:ss.xx tags, but the standard format is line-based.

  1. List of all the properties of this file format intrinsic to its file system:
  • Artist (tag: ar)
  • Title (tag: ti)
  • Album (tag: al)
  • Author (tag: au)
  • Lyricist (tag: lr)
  • Length (tag: length)
  • Creator (tag: by)
  • Offset (tag: offset)
  • Editor (tag: re or tool)
  • Version (tag: ve)
  • Comments (tag: #)
  • Synchronized Lyrics (time-stamped lines)

These properties are extracted from the text lines in the file. The format is line-based, with no fixed header or footer. The file extension is .lrc, and it is typically UTF-8 encoded. There are no intrinsic file system properties like magic numbers, as it is plain text.

  1. Two direct download links for files of format .LRC:
  1. Ghost blog embedded HTML JavaScript for drag and drop .LRC file to dump properties:
Drag and drop .LRC file here
  1. Python class for .LRC file:
import re

class LRCFile:
    def __init__(self, filepath):
        self.filepath = filepath
        self.properties = {
            'artist': '',
            'title': '',
            'album': '',
            'author': '',
            'lyricist': '',
            'length': '',
            'creator': '',
            'offset': '',
            'editor': '',
            'version': '',
            'comments': [],
            'lyrics': []
        }

    def read(self):
        with open(self.filepath, 'r', encoding='utf-8') as f:
            lines = f.readlines()
        for line in lines:
            line = line.strip()
            if re.match(r'\[ar:.*\]', line):
                self.properties['artist'] = re.sub(r'\[ar:(.*)\]', r'\1', line)
            elif re.match(r'\[ti:.*\]', line):
                self.properties['title'] = re.sub(r'\[ti:(.*)\]', r'\1', line)
            elif re.match(r'\[al:.*\]', line):
                self.properties['album'] = re.sub(r'\[al:(.*)\]', r'\1', line)
            elif re.match(r'\[au:.*\]', line):
                self.properties['author'] = re.sub(r'\[au:(.*)\]', r'\1', line)
            elif re.match(r'\[lr:.*\]', line):
                self.properties['lyricist'] = re.sub(r'\[lr:(.*)\]', r'\1', line)
            elif re.match(r'\[length:.*\]', line):
                self.properties['length'] = re.sub(r'\[length:(.*)\]', r'\1', line)
            elif re.match(r'\[by:.*\]', line):
                self.properties['creator'] = re.sub(r'\[by:(.*)\]', r'\1', line)
            elif re.match(r'\[offset:.*\]', line):
                self.properties['offset'] = re.sub(r'\[offset:(.*)\]', r'\1', line)
            elif re.match(r'\[re:.*\]', line) or re.match(r'\[tool:.*\]', line):
                self.properties['editor'] = re.sub(r'\[re:|tool:(.*)\]', r'\1', line)
            elif re.match(r'\[ve:.*\]', line):
                self.properties['version'] = re.sub(r'\[ve:(.*)\]', r'\1', line)
            elif re.match(r'\[#.*\]', line):
                self.properties['comments'].append(re.sub(r'\[#(.*)\]', r'\1', line))
            elif re.match(r'\[\d+:\d+\.\d+\]', line):
                self.properties['lyrics'].append(line)

    def print_properties(self):
        print("Artist:", self.properties['artist'])
        print("Title:", self.properties['title'])
        print("Album:", self.properties['album'])
        print("Author:", self.properties['author'])
        print("Lyricist:", self.properties['lyricist'])
        print("Length:", self.properties['length'])
        print("Creator:", self.properties['creator'])
        print("Offset:", self.properties['offset'])
        print("Editor:", self.properties['editor'])
        print("Version:", self.properties['version'])
        print("Comments:", ', '.join(self.properties['comments']))
        print("Lyrics:")
        for lyric in self.properties['lyrics']:
            print(lyric)

    def write(self, new_filepath=None):
        filepath = new_filepath or self.filepath
        with open(filepath, 'w', encoding='utf-8') as f:
            if self.properties['artist']:
                f.write(f"[ar:{self.properties['artist']}]\n")
            if self.properties['title']:
                f.write(f"[ti:{self.properties['title']}]\n")
            if self.properties['album']:
                f.write(f"[al:{self.properties['album']}]\n")
            if self.properties['author']:
                f.write(f"[au:{self.properties['author']}]\n")
            if self.properties['lyricist']:
                f.write(f"[lr:{self.properties['lyricist']}]\n")
            if self.properties['length']:
                f.write(f"[length:{self.properties['length']}]\n")
            if self.properties['creator']:
                f.write(f"[by:{self.properties['creator']}]\n")
            if self.properties['offset']:
                f.write(f"[offset:{self.properties['offset']}]\n")
            if self.properties['editor']:
                f.write(f"[re:{self.properties['editor']}]\n")
            if self.properties['version']:
                f.write(f"[ve:{self.properties['version']}]\n")
            for comment in self.properties['comments']:
                f.write(f"[#{comment}]\n")
            for lyric in self.properties['lyrics']:
                f.write(f"{lyric}\n")

# Example usage:
# lrc = LRCFile('example.lrc')
# lrc.read()
# lrc.print_properties()
# lrc.write('output.lrc')
  1. Java class for .LRC file:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.PrintWriter;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class LRCFile {
    private String filepath;
    private String artist = "";
    private String title = "";
    private String album = "";
    private String author = "";
    private String lyricist = "";
    private String length = "";
    private String creator = "";
    private String offset = "";
    private String editor = "";
    private String version = "";
    private List<String> comments = new ArrayList<>();
    private List<String> lyrics = new ArrayList<>();

    public LRCFile(String filepath) {
        this.filepath = filepath;
    }

    public void read() throws Exception {
        BufferedReader reader = new BufferedReader(new FileReader(filepath));
        String line;
        Pattern tagPattern = Pattern.compile("\\[(\\w+):(.*?)\\]");
        Pattern timePattern = Pattern.compile("\\[\\d+:\\d+\\.\\d+\\]");
        while (line = reader.readLine() != null) {
            line = line.trim();
            Matcher matcher = tagPattern.matcher(line);
            if (matcher.find()) {
                String tag = matcher.group(1);
                String value = matcher.group(2);
                switch (tag) {
                    case "ar": artist = value; break;
                    case "ti": title = value; break;
                    case "al": album = value; break;
                    case "au": author = value; break;
                    case "lr": lyricist = value; break;
                    case "length": length = value; break;
                    case "by": creator = value; break;
                    case "offset": offset = value; break;
                    case "re":
                    case "tool": editor = value; break;
                    case "ve": version = value; break;
                    case "#": comments.add(value); break;
                }
            } else if (timePattern.matcher(line).find()) {
                lyrics.add(line);
            }
        }
        reader.close();
    }

    public void printProperties() {
        System.out.println("Artist: " + artist);
        System.out.println("Title: " + title);
        System.out.println("Album: " + album);
        System.out.println("Author: " + author);
        System.out.println("Lyricist: " + lyricist);
        System.out.println("Length: " + length);
        System.out.println("Creator: " + creator);
        System.out.println("Offset: " + offset);
        System.out.println("Editor: " + editor);
        System.out.println("Version: " + version);
        System.out.println("Comments: " + String.join(", ", comments));
        System.out.println("Lyrics:");
        for (String lyric : lyrics) {
            System.out.println(lyric);
        }
    }

    public void write(String newFilepath) throws Exception {
        if (newFilepath == null) newFilepath = filepath;
        PrintWriter writer = new PrintWriter(new File(newFilepath), "UTF-8");
        if (!artist.isEmpty()) writer.println("[ar:" + artist + "]");
        if (!title.isEmpty()) writer.println("[ti:" + title + "]");
        if (!album.isEmpty()) writer.println("[al:" + album + "]");
        if (!author.isEmpty()) writer.println("[au:" + author + "]");
        if (!lyricist.isEmpty()) writer.println("[lr:" + lyricist + "]");
        if (!length.isEmpty()) writer.println("[length:" + length + "]");
        if (!creator.isEmpty()) writer.println("[by:" + creator + "]");
        if (!offset.isEmpty()) writer.println("[offset:" + offset + "]");
        if (!editor.isEmpty()) writer.println("[re:" + editor + "]");
        if (!version.isEmpty()) writer.println("[ve:" + version + "]");
        for (String comment : comments) {
            writer.println("[#" + comment + "]");
        }
        for (String lyric : lyrics) {
            writer.println(lyric);
        }
        writer.close();
    }

    // Example usage:
    // public static void main(String[] args) throws Exception {
    //     LRCFile lrc = new LRCFile("example.lrc");
    //     lrc.read();
    //     lrc.printProperties();
    //     lrc.write("output.lrc");
    // }
}
  1. JavaScript class for .LRC file (Node.js, using fs for open/read/write):
const fs = require('fs');

class LRCFile {
  constructor(filepath) {
    this.filepath = filepath;
    this.properties = {
      artist: '',
      title: '',
      album: '',
      author: '',
      lyricist: '',
      length: '',
      creator: '',
      offset: '',
      editor: '',
      version: '',
      comments: [],
      lyrics: []
    };
  }

  read() {
    const content = fs.readFileSync(this.filepath, 'utf8');
    const lines = content.split('\n');
    lines.forEach((line) => {
      line = line.trim();
      if (line.startsWith('[ar:')) this.properties.artist = line.slice(5, -1);
      else if (line.startsWith('[ti:')) this.properties.title = line.slice(5, -1);
      else if (line.startsWith('[al:')) this.properties.album = line.slice(5, -1);
      else if (line.startsWith('[au:')) this.properties.author = line.slice(5, -1);
      else if (line.startsWith('[lr:')) this.properties.lyricist = line.slice(5, -1);
      else if (line.startsWith('[length:')) this.properties.length = line.slice(9, -1);
      else if (line.startsWith('[by:')) this.properties.creator = line.slice(5, -1);
      else if (line.startsWith('[offset:')) this.properties.offset = line.slice(9, -1);
      else if (line.startsWith('[re:') || line.startsWith('[tool:')) this.properties.editor = line.slice(line.indexOf(':') + 1, -1);
      else if (line.startsWith('[ve:')) this.properties.version = line.slice(5, -1);
      else if (line.startsWith('[#')) this.properties.comments.push(line.slice(2, -1));
      else if (/^\[\d+:\d+\.\d+\]/.test(line)) this.properties.lyrics.push(line);
    });
  }

  printProperties() {
    console.log('Artist:', this.properties.artist);
    console.log('Title:', this.properties.title);
    console.log('Album:', this.properties.album);
    console.log('Author:', this.properties.author);
    console.log('Lyricist:', this.properties.lyricist);
    console.log('Length:', this.properties.length);
    console.log('Creator:', this.properties.creator);
    console.log('Offset:', this.properties.offset);
    console.log('Editor:', this.properties.editor);
    console.log('Version:', this.properties.version);
    console.log('Comments:', this.properties.comments.join(', '));
    console.log('Lyrics:');
    this.properties.lyrics.forEach((lyric) => console.log(lyric));
  }

  write(newFilepath = this.filepath) {
    let content = '';
    if (this.properties.artist) content += `[ar:${this.properties.artist}]\n`;
    if (this.properties.title) content += `[ti:${this.properties.title}]\n`;
    if (this.properties.album) content += `[al:${this.properties.album}]\n`;
    if (this.properties.author) content += `[au:${this.properties.author}]\n`;
    if (this.properties.lyricist) content += `[lr:${this.properties.lyricist}]\n`;
    if (this.properties.length) content += `[length:${this.properties.length}]\n`;
    if (this.properties.creator) content += `[by:${this.properties.creator}]\n`;
    if (this.properties.offset) content += `[offset:${this.properties.offset}]\n`;
    if (this.properties.editor) content += `[re:${this.properties.editor}]\n`;
    if (this.properties.version) content += `[ve:${this.properties.version}]\n`;
    this.properties.comments.forEach((comment) => content += `[#${comment}]\n`);
    this.properties.lyrics.forEach((lyric) => content += `${lyric}\n`);
    fs.writeFileSync(newFilepath, content, 'utf8');
  }
}

// Example usage:
// const lrc = new LRCFile('example.lrc');
// lrc.read();
// lrc.printProperties();
// lrc.write('output.lrc');
  1. C "class" for .LRC file (using struct and functions):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <regex.h>

typedef struct {
    char *filepath;
    char *artist;
    char *title;
    char *album;
    char *author;
    char *lyricist;
    char *length;
    char *creator;
    char *offset;
    char *editor;
    char *version;
    char **comments;
    int comments_count;
    char **lyrics;
    int lyrics_count;
} LRCFile;

LRCFile* lrc_new(const char *filepath) {
    LRCFile *lrc = malloc(sizeof(LRCFile));
    lrc->filepath = strdup(filepath);
    lrc->artist = NULL;
    lrc->title = NULL;
    lrc->album = NULL;
    lrc->author = NULL;
    lrc->lyricist = NULL;
    lrc->length = NULL;
    lrc->creator = NULL;
    lrc->offset = NULL;
    lrc->editor = NULL;
    lrc->version = NULL;
    lrc->comments = NULL;
    lrc->comments_count = 0;
    lrc->lyrics = NULL;
    lrc->lyrics_count = 0;
    return lrc;
}

void lrc_read(LRCFile *lrc) {
    FILE *file = fopen(lrc->filepath, "r");
    if (!file) return;
    char line[1024];
    regex_t time_regex;
    regcomp(&time_regex, "^\\[\\d+:\\d+\\.\\d+\\]", REG_EXTENDED);
    while (fgets(line, sizeof(line), file)) {
        char *trimmed = strtok(line, "\n");
        if (trimmed) {
            if (strstr(trimmed, "[ar:")) lrc->artist = strdup(trimmed + 5);
            else if (strstr(trimmed, "[ti:")) lrc->title = strdup(trimmed + 5);
            else if (strstr(trimmed, "[al:")) lrc->album = strdup(trimmed + 5);
            else if (strstr(trimmed, "[au:")) lrc->author = strdup(trimmed + 5);
            else if (strstr(trimmed, "[lr:")) lrc->lyricist = strdup(trimmed + 5);
            else if (strstr(trimmed, "[length:")) lrc->length = strdup(trimmed + 9);
            else if (strstr(trimmed, "[by:")) lrc->creator = strdup(trimmed + 5);
            else if (strstr(trimmed, "[offset:")) lrc->offset = strdup(trimmed + 9);
            else if (strstr(trimmed, "[re:") || strstr(trimmed, "[tool:")) lrc->editor = strdup(strchr(trimmed, ':') + 1);
            else if (strstr(trimmed, "[ve:")) lrc->version = strdup(trimmed + 5);
            else if (strstr(trimmed, "[#")) {
                lrc->comments = realloc(lrc->comments, (lrc->comments_count + 1) * sizeof(char*));
                lrc->comments[lrc->comments_count++] = strdup(trimmed + 2);
            } else if (regexec(&time_regex, trimmed, 0, NULL, 0) == 0) {
                lrc->lyrics = realloc(lrc->lyrics, (lrc->lyrics_count + 1) * sizeof(char*));
                lrc->lyrics[lrc->lyrics_count++] = strdup(trimmed);
            }
        }
    }
    regfree(&time_regex);
    fclose(file);
}

void lrc_print_properties(LRCFile *lrc) {
    printf("Artist: %s\n", lrc->artist ? lrc->artist : "");
    printf("Title: %s\n", lrc->title ? lrc->title : "");
    printf("Album: %s\n", lrc->album ? lrc->album : "");
    printf("Author: %s\n", lrc->author ? lrc->author : "");
    printf("Lyricist: %s\n", lrc->lyricist ? lrc->lyricist : "");
    printf("Length: %s\n", lrc->length ? lrc->length : "");
    printf("Creator: %s\n", lrc->creator ? lrc->creator : "");
    printf("Offset: %s\n", lrc->offset ? lrc->offset : "");
    printf("Editor: %s\n", lrc->editor ? lrc->editor : "");
    printf("Version: %s\n", lrc->version ? lrc->version : "");
    printf("Comments: ");
    for (int i = 0; i < lrc->comments_count; i++) {
        printf("%s%s", lrc->comments[i], i < lrc->comments_count - 1 ? ", " : "\n");
    }
    printf("Lyrics:\n");
    for (int i = 0; i < lrc->lyrics_count; i++) {
        printf("%s\n", lrc->lyrics[i]);
    }
}

void lrc_write(LRCFile *lrc, const char *new_filepath) {
    const char *path = new_filepath ? new_filepath : lrc->filepath;
    FILE *file = fopen(path, "w");
    if (!file) return;
    if (lrc->artist) fprintf(file, "[ar:%s\n", lrc->artist);
    if (lrc->title) fprintf(file, "[ti:%s\n", lrc->title);
    if (lrc->album) fprintf(file, "[al:%s\n", lrc->album);
    if (lrc->author) fprintf(file, "[au:%s\n", lrc->author);
    if (lrc->lyricist) fprintf(file, "[lr:%s\n", lrc->lyricist);
    if (lrc->length) fprintf(file, "[length:%s\n", lrc->length);
    if (lrc->creator) fprintf(file, "[by:%s\n", lrc->creator);
    if (lrc->offset) fprintf(file, "[offset:%s\n", lrc->offset);
    if (lrc->editor) fprintf(file, "[re:%s\n", lrc->editor);
    if (lrc->version) fprintf(file, "[ve:%s\n", lrc->version);
    for (int i = 0; i < lrc->comments_count; i++) {
        fprintf(file, "[#%s\n", lrc->comments[i]);
    }
    for (int i = 0; i < lrc->lyrics_count; i++) {
        fprintf(file, "%s\n", lrc->lyrics[i]);
    }
    fclose(file);
}

void lrc_free(LRCFile *lrc) {
    free(lrc->filepath);
    free(lrc->artist);
    free(lrc->title);
    free(lrc->album);
    free(lrc->author);
    free(lrc->lyricist);
    free(lrc->length);
    free(lrc->creator);
    free(lrc->offset);
    free(lrc->editor);
    free(lrc->version);
    for (int i = 0; i < lrc->comments_count; i++) free(lrc->comments[i]);
    free(lrc->comments);
    for (int i = 0; i < lrc->lyrics_count; i++) free(lrc->lyrics[i]);
    free(lrc->lyrics);
    free(lrc);
}

// Example usage:
// int main() {
//     LRCFile *lrc = lrc_new("example.lrc");
//     lrc_read(lrc);
//     lrc_print_properties(lrc);
//     lrc_write(lrc, "output.lrc");
//     lrc_free(lrc);
//     return 0;
// }