native-lyrics-tools
Version:
A JavaScript library for parsing and generating various lyric formats.
149 lines (139 loc) • 3.96 kB
JavaScript
// Data structures
class LyricWord {
constructor(word, start_time, end_time) {
this.word = word;
this.start_time = start_time;
this.end_time = end_time;
}
}
class LyricLine {
constructor(words = []) {
this.words = words;
}
}
// Parse [start,duration] -> {start, duration}
function parseTimeTag(str) {
const match = str.match(/^\[(\d+),(\d+)]/);
if (!match) throw new Error(`Invalid time tag: ${str}`);
return {
start: Number(match[1]),
duration: Number(match[2]),
consumed: match[0].length
};
}
// Parse (start,duration) -> {start, duration}
function parseWordTimeTag(str) {
const match = str.match(/^\((\d+),(\d+)\)/);
if (!match) throw new Error(`Invalid word time tag: ${str}`);
return {
start: Number(match[1]),
duration: Number(match[2]),
consumed: match[0].length
};
}
// Parse a word with an optional (start,duration) at the end
function parseQrcWord(str) {
let idx = str.search(/\(\d+,\d+\)/);
if (idx === -1) {
// No time tag, treat full string as word
return {
word: str,
start_time: 0,
end_time: 0,
consumed: str.length
};
}
const word = str.slice(0, idx);
const tag = parseWordTimeTag(str.slice(idx));
return {
word: word,
start_time: tag.start,
end_time: tag.start + tag.duration,
consumed: idx + tag.consumed
};
}
// Parse all words in a line after time tag
function parseQrcWords(str) {
const words = [];
let i = 0;
while (i < str.length) {
while (str[i] === ' ') i++;
if (i >= str.length) break;
let nextParen = str.slice(i).search(/\(\d+,\d+\)/);
if (nextParen === -1) {
words.push(new LyricWord(str.slice(i), 0, 0));
break;
}
let beforeParen = str.slice(i, i + nextParen);
let tag = parseWordTimeTag(str.slice(i + nextParen));
words.push(new LyricWord(beforeParen, tag.start, tag.start + tag.duration));
i += nextParen + tag.consumed;
}
return words;
}
// Parse one line of QRC
function parseQrcLine(line) {
let match = line.match(/^\[(\d+),(\d+)]/);
if (!match) return null;
const tag = parseTimeTag(line);
const rest = line.slice(tag.consumed);
const words = parseQrcWords(rest);
return new LyricLine(words);
}
// Parse full QRC
function parseQRC(src) {
const lines = src.split(/\r?\n/);
const result = [];
for (const line of lines) {
if (!line.trim()) continue;
try {
const lyricLine = parseQrcLine(line);
if (lyricLine && lyricLine.words && lyricLine.words.length > 0) {
result.push(lyricLine);
}
} catch (e) {
continue;
}
}
processQrcLyrics(result);
return result;
}
// In-place adjust end_time for lines and words
function processQrcLyrics(lines) {
let nextLineStart = Number.MAX_SAFE_INTEGER;
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i];
if (!line.words.length) continue;
line.end_time = nextLineStart;
for (let j = line.words.length - 1; j >= 0; j--) {
let word = line.words[j];
if (j === line.words.length - 1) word.end_time = nextLineStart;
}
nextLineStart = line.words[0].start_time;
}
}
// Stringify QRC
function stringifyQRC(lines) {
let result = '';
for (const line of lines) {
if (!line.words.length) continue;
const start_time = line.words[0].start_time;
const duration = line.words.reduce((sum, w) => sum + (w.end_time - w.start_time), 0);
result += `[${start_time},${duration}]`;
for (const word of line.words) {
result += `${word.word}(${word.start_time},${word.end_time - word.start_time})`;
}
result += '\n';
}
return result;
}
// ES module API
export {
LyricWord as QrcLyricWord,
LyricLine as QrcLyricLine,
parseQRC,
stringifyQRC,
parseQrcWord,
parseQrcWords,
processQrcLyrics
};