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.
- 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.
- Two direct download links for files of format .LRC:
- https://gist.githubusercontent.com/gb103/8289eef243d3fec155795d4e757603c5/raw
- https://raw.githubusercontent.com/OlafurOrnJosephsson/vogor/main/sodkaffi.lrc
- Ghost blog embedded HTML JavaScript for drag and drop .LRC file to dump properties:
- 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')
- 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");
// }
}
- 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');
- 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;
// }