UNPKG

native-lyrics-tools

Version:

A JavaScript library for parsing and generating various lyric formats.

149 lines (139 loc) 3.96 kB
// 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 };