justlyrics
Version:
A comprehensive TypeScript library for parsing, converting, and processing various lyric formats including LRC, ALRC, YRC, QRC, and more
784 lines (783 loc) • 31.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.LyricFormat = exports.LyricIO = exports.CoreLyric = void 0;
const utils_1 = require("./utils");
var CoreLyric;
(function (CoreLyric) {
class Timestamp {
static max(...timestamps) {
return new Timestamp(Math.max(...timestamps
.filter((t) => t !== null)
.map((t) => t.milliSeconds)));
}
static min(...timestamps) {
return new Timestamp(Math.min(...timestamps
.filter((t) => t !== null)
.map((t) => t.milliSeconds)));
}
constructor(milliSeconds) {
this.milliSeconds = 0;
this.milliSeconds = milliSeconds;
}
seconds() {
return this.milliSeconds / 1000;
}
formatted() {
const seconds = Math.floor(this.seconds());
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
}
}
CoreLyric.Timestamp = Timestamp;
let VoiceAgentType;
(function (VoiceAgentType) {
VoiceAgentType["Vocal"] = "vocal";
VoiceAgentType["BackgroundVocal"] = "background-vocal";
VoiceAgentType["Other"] = "other";
})(VoiceAgentType = CoreLyric.VoiceAgentType || (CoreLyric.VoiceAgentType = {}));
let LineAnnotationRole;
(function (LineAnnotationRole) {
LineAnnotationRole["Prononciation"] = "pron";
LineAnnotationRole["Translation"] = "trans";
})(LineAnnotationRole = CoreLyric.LineAnnotationRole || (CoreLyric.LineAnnotationRole = {}));
class SyllableSyncedLine {
constructor(opt) {
this.syllables = opt.syllables;
this.voiceAgent = opt.voiceAgent;
this.annotations = opt.annotations;
}
toLineSyncedLine() {
const lineText = this.syllables.map((s) => s.text).join('');
const lineStart = Timestamp.min(...this.syllables
.filter((syl) => syl.start?.milliSeconds != 0 ||
syl.end?.milliSeconds != 0 ||
syl.text.trim() != '')
.map((s) => s.start));
const lineEnd = Timestamp.max(...this.syllables
.filter((syl) => syl.start?.milliSeconds != 0 ||
syl.end?.milliSeconds != 0 ||
syl.text.trim() != '')
.map((s) => s.end));
return new LineSyncedLine({
text: lineText,
start: lineStart,
end: lineEnd,
voiceAgent: this.voiceAgent,
annotations: this.annotations,
});
}
extractAnnotations(role) {
return this.toLineSyncedLine().extractAnnotations(role);
}
}
CoreLyric.SyllableSyncedLine = SyllableSyncedLine;
class LineSyncedLine {
static checkOverlap(a, b) {
if (a.start === null ||
a.end === null ||
b.start === null ||
b.end === null) {
return false;
}
const res = a.start.milliSeconds < b.end.milliSeconds &&
b.start.milliSeconds < a.end.milliSeconds;
return res;
}
static fromText(tr) {
return new LineSyncedLine({
text: tr,
});
}
constructor(opt) {
this.text = opt.text;
this.start = opt.start || null;
this.end = opt.end || null;
this.voiceAgent = opt.voiceAgent || null;
this.annotations = opt.annotations || [];
}
extractAnnotations(role) {
return this.annotations
.filter((a) => a.role === role)
.map((a) => {
const l2 = a.line;
l2.start = this.start;
l2.end = this.end;
return l2;
});
}
}
CoreLyric.LineSyncedLine = LineSyncedLine;
let Utils;
(function (Utils) {
function extractLineAnnotations(lines, role) {
return lines.flatMap((line) => {
if (line instanceof SyllableSyncedLine ||
line instanceof LineSyncedLine) {
return line.extractAnnotations(role);
}
else {
return [];
}
});
}
Utils.extractLineAnnotations = extractLineAnnotations;
function splitWordSyllables(syl) {
const res = [];
let buf = '';
let remaining = syl.text;
while (remaining.length > 0) {
const c = remaining[0];
if ((0, utils_1.isCJKChar)(c) &&
buf.length > 0 &&
buf != '(' &&
buf != '(') {
res.push({
text: buf,
start: null,
end: null,
annotations: syl.annotations,
});
buf = '';
}
buf += c;
remaining = remaining.slice(1);
}
if (buf.length > 0) {
if (buf == ')' || buf == ')')
res[res.length - 1].text += buf;
else
res.push({
text: buf,
start: null,
end: null,
annotations: syl.annotations,
});
}
const totalDur = syl.end?.milliSeconds - syl.start?.milliSeconds;
let pos = 0;
// reassign time
for (const i in res) {
const partLength = (totalDur / syl.text.replace(/[\(\)()]/g, '').length) *
res[i].text.replace(/[\(\)()]/g, '').length;
const s = res[i];
s.start = new Timestamp(Math.round(syl.start?.milliSeconds + pos));
s.end = new Timestamp(Math.round(syl.start?.milliSeconds + pos + partLength));
pos += partLength;
}
return res;
}
Utils.splitWordSyllables = splitWordSyllables;
function preprocessLinesForLyricify(lines) {
return lines.map((line) => {
if (line instanceof SyllableSyncedLine) {
const newSyls = line.syllables.flatMap(splitWordSyllables);
return new SyllableSyncedLine({
syllables: newSyls,
voiceAgent: line.voiceAgent,
annotations: line.annotations,
});
}
else {
return line;
}
});
}
Utils.preprocessLinesForLyricify = preprocessLinesForLyricify;
})(Utils = CoreLyric.Utils || (CoreLyric.Utils = {}));
})(CoreLyric || (exports.CoreLyric = CoreLyric = {}));
var LyricIO;
(function (LyricIO) {
let Abstraction;
(function (Abstraction) {
let ALRC;
(function (ALRC) {
let ALRCStylePosition;
(function (ALRCStylePosition) {
ALRCStylePosition[ALRCStylePosition["Undefined"] = 0] = "Undefined";
ALRCStylePosition[ALRCStylePosition["Left"] = 1] = "Left";
ALRCStylePosition[ALRCStylePosition["Center"] = 2] = "Center";
ALRCStylePosition[ALRCStylePosition["Right"] = 3] = "Right";
})(ALRCStylePosition = ALRC.ALRCStylePosition || (ALRC.ALRCStylePosition = {}));
let ALRCStyleAccent;
(function (ALRCStyleAccent) {
ALRCStyleAccent[ALRCStyleAccent["Normal"] = 0] = "Normal";
ALRCStyleAccent[ALRCStyleAccent["Background"] = 1] = "Background";
ALRCStyleAccent[ALRCStyleAccent["Whisper"] = 2] = "Whisper";
ALRCStyleAccent[ALRCStyleAccent["Emphasise"] = 3] = "Emphasise";
})(ALRCStyleAccent = ALRC.ALRCStyleAccent || (ALRC.ALRCStyleAccent = {}));
})(ALRC = Abstraction.ALRC || (Abstraction.ALRC = {}));
})(Abstraction = LyricIO.Abstraction || (LyricIO.Abstraction = {}));
let Dumping;
(function (Dumping) {
let YRC;
(function (YRC) {
function createHeaderLines(data) {
const headerLines = [];
const lyricistsLine = [];
for (let idx = 0; idx < data.lyricists.length; idx++) {
const lyricist = data.lyricists[idx];
if (idx > 0)
lyricistsLine.push({ tx: '/' });
lyricistsLine.push({ tx: lyricist });
}
const musiciansLine = [];
for (let idx = 0; idx < data.musicians.length; idx++) {
const musician = data.musicians[idx];
if (idx > 0)
musiciansLine.push({ tx: '/' });
musiciansLine.push({ tx: musician });
}
if (lyricistsLine.length > 0)
headerLines.push({
t: 0,
c: [{ tx: '作词: ' }, ...lyricistsLine],
});
if (musiciansLine.length > 0)
headerLines.push({
t: 0,
c: [{ tx: '作曲: ' }, ...musiciansLine],
});
return headerLines;
}
YRC.createHeaderLines = createHeaderLines;
})(YRC = Dumping.YRC || (Dumping.YRC = {}));
let LQE;
(function (LQE) {
function createLQEPartHeader(data) {
return Object.entries(data)
.map(([key, value]) => `${key}@${value}`)
.join(', ');
}
LQE.createLQEPartHeader = createLQEPartHeader;
})(LQE = Dumping.LQE || (Dumping.LQE = {}));
function dumpLYS(lines) {
let editorBuffer = '';
let lineStart = null;
// compile to buffer
const linesData = [];
const properties = [];
let i = 0;
for (const line of lines) {
const property = (line.voiceAgent?.type ==
CoreLyric.VoiceAgentType.BackgroundVocal
? 6
: 3) + (line.voiceAgent?.n != 2 ? 1 : 2);
properties.push(property);
const lineData = [];
if (line instanceof CoreLyric.SyllableSyncedLine) {
for (const syllable of line.syllables) {
lineStart = CoreLyric.Timestamp.min(lineStart, syllable.start);
lineData.push({
text: syllable.text,
start: syllable.start?.milliSeconds || 0,
end: syllable.end?.milliSeconds || 0,
});
}
}
else if (line instanceof CoreLyric.LineSyncedLine) {
lineStart = line.start;
lineData.push({
text: line.text,
start: line.start?.milliSeconds || 0,
end: line.end?.milliSeconds || 0,
});
}
linesData.push(lineData);
if (i > 0 && lineStart != null) {
const last = linesData[i - 1];
if (last instanceof CoreLyric.LineSyncedLine) {
last.end = lineStart;
}
}
i += 1;
}
i = 0;
for (const lineData of linesData) {
editorBuffer += `[${properties[i]}]`;
for (const syllableData of lineData) {
editorBuffer += `${syllableData.text}(${Math.round(syllableData.start)},${Math.round(syllableData.end - syllableData.start)})`;
}
editorBuffer += '\n';
i += 1;
}
return editorBuffer.trim();
}
Dumping.dumpLYS = dumpLYS;
function dumpLRC(lines) {
let editorBuffer = '';
for (const line of lines) {
let lineSyncedLine;
if (line instanceof CoreLyric.SyllableSyncedLine) {
lineSyncedLine = line.toLineSyncedLine();
}
else {
lineSyncedLine = line;
}
const start = lineSyncedLine.start;
if (start) {
const minutes = Math.floor(start.milliSeconds / 60000);
const seconds = Math.floor((start.milliSeconds % 60000) / 1000);
const milliseconds = start.milliSeconds % 1000;
editorBuffer += `[${minutes
.toString()
.padStart(2, '0')}:${seconds
.toString()
.padStart(2, '0')}.${milliseconds
.toString()
.padStart(3, '0')}]${lineSyncedLine.text}\n`;
}
}
return editorBuffer.trim();
}
Dumping.dumpLRC = dumpLRC;
function dumpLYL(lines) {
let editorBuffer = '';
for (const line of lines) {
let lineSyncedLine;
if (line instanceof CoreLyric.SyllableSyncedLine) {
lineSyncedLine = line.toLineSyncedLine();
}
else {
lineSyncedLine = line;
}
const start = lineSyncedLine.start;
const end = lineSyncedLine.end;
if (start) {
editorBuffer += `[${Math.round(start.milliSeconds)},${Math.round(end?.milliSeconds || 0)}]${lineSyncedLine.text}\n`;
}
}
return editorBuffer.trim();
}
Dumping.dumpLYL = dumpLYL;
function dumpQRC(lines) {
let editorBuffer = '';
for (const line of lines) {
let lineSyncedLine;
if (line instanceof CoreLyric.SyllableSyncedLine) {
lineSyncedLine = line.toLineSyncedLine();
}
else {
lineSyncedLine = line;
}
const start = lineSyncedLine.start;
const end = lineSyncedLine.end;
const endMilliSeconds = end?.milliSeconds || null;
const durMillis = endMilliSeconds && start
? endMilliSeconds - start.milliSeconds
: null;
if (line instanceof CoreLyric.SyllableSyncedLine) {
if (start) {
editorBuffer += `[${Math.round(start.milliSeconds)},${Math.round(durMillis || 0)}]`;
for (const syllable of line.syllables) {
const sylDurMillis = syllable.end && syllable.start
? syllable.end.milliSeconds -
syllable.start.milliSeconds
: null;
editorBuffer += `${syllable.text}(${Math.round(syllable.start?.milliSeconds || 0)},${Math.round(sylDurMillis || 0)})`;
}
editorBuffer += '\n';
}
}
else if (line instanceof CoreLyric.LineSyncedLine) {
if (start) {
editorBuffer += `[${Math.round(start.milliSeconds)},${Math.round(durMillis || 0)}]${lineSyncedLine.text}(${Math.round(start.milliSeconds)},${Math.round(durMillis || 0)})\n`;
}
}
}
return editorBuffer.trim();
}
Dumping.dumpQRC = dumpQRC;
function dumpYRC(lines) {
let editorBuffer = '';
for (const line of lines) {
let lineSyncedLine;
if (line instanceof CoreLyric.SyllableSyncedLine) {
lineSyncedLine = line.toLineSyncedLine();
}
else {
lineSyncedLine = line;
}
const start = lineSyncedLine.start;
const end = lineSyncedLine.end;
const endMilliSeconds = end?.milliSeconds || null;
const durMillis = endMilliSeconds && start
? endMilliSeconds - start.milliSeconds
: null;
if (line instanceof CoreLyric.SyllableSyncedLine) {
if (start) {
editorBuffer += `[${Math.round(start.milliSeconds)},${Math.round(durMillis || 0)}]`;
for (const syllable of line.syllables) {
const sylDurMillis = syllable.end && syllable.start
? syllable.end.milliSeconds -
syllable.start.milliSeconds
: null;
editorBuffer += `(${Math.round(syllable.start?.milliSeconds || 0)},${Math.round(sylDurMillis || 0)},0)${syllable.text}`;
}
editorBuffer += '\n';
}
}
else if (line instanceof CoreLyric.LineSyncedLine) {
if (start) {
editorBuffer += `[${Math.round(start.milliSeconds)},${Math.round(durMillis || 0)}](${Math.round(start.milliSeconds)},${Math.round(durMillis || 0)},0)${lineSyncedLine.text}\n`;
}
}
}
return editorBuffer.trim();
}
Dumping.dumpYRC = dumpYRC;
function dumpALRC(lines) {
const alrcData = {
li: {},
si: null,
h: {
s: [],
},
l: [],
};
for (const line of lines) {
if (line instanceof CoreLyric.SyllableSyncedLine) {
const alrcLine = {
w: line.syllables.map((syllable) => ({
f: syllable.start?.milliSeconds || 0,
t: syllable.end?.milliSeconds || 0,
w: syllable.text,
})),
f: CoreLyric.Timestamp.min(...line.syllables.map((syllable) => syllable.start)).milliSeconds,
t: CoreLyric.Timestamp.max(...line.syllables.map((syllable) => syllable.end)).milliSeconds,
};
alrcData.l.push(alrcLine);
}
else if (line instanceof CoreLyric.LineSyncedLine) {
const alrcLine = {
tx: line.text,
f: line.start?.milliSeconds || 0,
t: line.end?.milliSeconds || 0,
};
alrcData.l.push(alrcLine);
}
}
return JSON.stringify(alrcData);
}
Dumping.dumpALRC = dumpALRC;
function dumpSRT(lines) {
let editorBuffer = '';
let index = 1;
for (const line of lines) {
let lineSyncedLine;
if (line instanceof CoreLyric.SyllableSyncedLine) {
lineSyncedLine = line.toLineSyncedLine();
}
else {
lineSyncedLine = line;
}
const start = lineSyncedLine.start;
const end = lineSyncedLine.end;
if (start && end) {
const startTime = new Date(start.milliSeconds);
const endTime = new Date(end.milliSeconds);
const startStr = `${startTime
.getUTCHours()
.toString()
.padStart(2, '0')}:${startTime
.getUTCMinutes()
.toString()
.padStart(2, '0')}:${startTime
.getUTCSeconds()
.toString()
.padStart(2, '0')},${(start.milliSeconds % 1000)
.toString()
.padStart(3, '0')}`;
const endStr = `${endTime
.getUTCHours()
.toString()
.padStart(2, '0')}:${endTime
.getUTCMinutes()
.toString()
.padStart(2, '0')}:${endTime
.getUTCSeconds()
.toString()
.padStart(2, '0')},${(end.milliSeconds % 1000)
.toString()
.padStart(3, '0')}`;
let text = lineSyncedLine.text.trim();
if (lineSyncedLine.voiceAgent &&
lineSyncedLine.voiceAgent.type ==
CoreLyric.VoiceAgentType.BackgroundVocal) {
text = `(${text})`;
}
editorBuffer += `${index}\n${startStr} --> ${endStr}\n${text}\n\n`;
index++;
}
}
return editorBuffer.trim();
}
Dumping.dumpSRT = dumpSRT;
function dumpLQE(lines) {
let result = '';
const transLy = CoreLyric.Utils.extractLineAnnotations(lines, CoreLyric.LineAnnotationRole.Translation);
const pronLy = CoreLyric.Utils.extractLineAnnotations(lines, CoreLyric.LineAnnotationRole.Prononciation);
// original lyrics
result += `[lyrics: ${LyricIO.Dumping.LQE.createLQEPartHeader({
format: 'Lyricify Syllable',
// language: 'en-US',
})}]\n`;
result += dumpLYS(lines).trimEnd();
result += '\n\n';
// translation
if (transLy.length > 0) {
result += `\n[translation: ${LyricIO.Dumping.LQE.createLQEPartHeader({
format: 'LRC',
// language: 'zh-CN',
})}]\n`;
result += dumpLRC(transLy).trimEnd();
result += '\n\n';
}
// pronunciation
if (pronLy.length > 0) {
result += `\n[pronunciation: ${LyricIO.Dumping.LQE.createLQEPartHeader({
format: 'LRC',
language: 'romaji',
})}]\n`;
result += dumpLRC(pronLy).trimEnd();
result += '\n\n';
}
return result.trim();
}
Dumping.dumpLQE = dumpLQE;
function dumpSPL(lines) {
function timeMark(ms) {
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
const milliseconds = ms % 1000;
return `[${minutes.toString().padStart(2, '0')}:${seconds
.toString()
.padStart(2, '0')}.${milliseconds
.toString()
.padStart(3, '0')}]`;
}
let editorBuffer = '';
for (const line of lines) {
let lineSyncedLine;
if (line instanceof CoreLyric.SyllableSyncedLine) {
lineSyncedLine = line.toLineSyncedLine();
}
else {
lineSyncedLine = line;
}
const start = lineSyncedLine.start;
const end = lineSyncedLine.end;
if (line instanceof CoreLyric.SyllableSyncedLine) {
if (start && end) {
let lastMarkedTime = null;
for (const syllable of line.syllables) {
if (syllable.start?.milliSeconds != lastMarkedTime) {
editorBuffer += timeMark(syllable.start?.milliSeconds || 0);
}
editorBuffer += syllable.text;
editorBuffer += timeMark(syllable.end?.milliSeconds || 0);
lastMarkedTime = syllable.end?.milliSeconds || 0;
}
editorBuffer += '\n';
}
}
else if (line instanceof CoreLyric.LineSyncedLine) {
if (start && end) {
// editorBuffer += `${timeMark(start.milliSeconds || 0)}${lineSyncedLine.text}${timeMark(end?.milliSeconds || 0)}\n`
editorBuffer += timeMark(start.milliSeconds || 0);
editorBuffer += lineSyncedLine.text;
editorBuffer += timeMark(end?.milliSeconds || 0);
editorBuffer += '\n';
}
}
for (const annotation of line.annotations) {
if (annotation.role ==
CoreLyric.LineAnnotationRole.Translation) {
editorBuffer += annotation.line.text;
editorBuffer += '\n';
break;
}
}
}
return editorBuffer.trim();
}
Dumping.dumpSPL = dumpSPL;
function selectDumpWorker(type) {
switch (type) {
case 'lys':
return dumpLYS;
case 'lyl':
return dumpLYL;
case 'lrc':
return dumpLRC;
case 'qrc':
return dumpQRC;
case 'alrc':
return dumpALRC;
case 'srt':
return dumpSRT;
case 'yrc':
return dumpYRC;
case 'spl':
return dumpSPL;
case 'lqe':
return dumpLQE;
case 'ttml':
return null; // Unsupported
case 'ttml_amll':
return null; // Unsupported
case 'apple_syllable':
return null; // Unsupported
case 'krc':
return null; // Unsupported
// Unsupported
default: {
console.error(`Unsupported format: ${type}`);
return null;
}
}
}
function dump(type, lines) {
const worker = selectDumpWorker(type);
if (!worker) {
return '[ERROR] Unsupported';
}
let preprocessedLines;
switch (type) {
case 'lys':
case 'lqe':
case 'yrc':
case 'qrc':
case 'krc':
preprocessedLines =
CoreLyric.Utils.preprocessLinesForLyricify(lines);
break;
case 'lyl':
case 'lrc':
case 'alrc':
case 'ttml':
case 'ttml_amll':
case 'apple_syllable':
case 'srt':
case 'spl':
preprocessedLines = lines;
break;
default:
throw new Error('should not happen');
}
return worker(preprocessedLines);
}
Dumping.dump = dump;
function supportDump(type) {
return selectDumpWorker(type) != null;
}
Dumping.supportDump = supportDump;
})(Dumping = LyricIO.Dumping || (LyricIO.Dumping = {}));
})(LyricIO || (exports.LyricIO = LyricIO = {}));
var LyricFormat;
(function (LyricFormat) {
LyricFormat.TYPES = {
lys: {
displayName: 'Lyricify Syllable',
extensions: ['.lys'],
},
lyl: {
displayName: 'Lyricify Lines',
extensions: ['.lyl'],
},
lqe: {
displayName: 'Lyricify Quick Export',
extensions: ['.lqe'],
},
lrc: {
displayName: 'LRC',
extensions: ['.lrc'],
},
alrc: {
displayName: 'ALRC',
extensions: ['.alrc'],
},
ttml: {
displayName: 'TTML (Original)',
extensions: ['.ttml'],
},
ttml_amll: {
displayName: 'TTML (AMLL Standards)',
extensions: ['.ttml'],
},
apple_syllable: {
displayName: 'Apple Syllable',
extensions: ['.json', '.as', '.asyl'],
},
yrc: {
displayName: 'YRC',
extensions: ['.yrc'],
},
qrc: {
displayName: 'QRC (Lyricify Standards)',
extensions: ['.qrc'],
},
krc: {
displayName: 'KRC',
extensions: ['.krc'],
},
srt: {
displayName: 'SRT',
extensions: ['.srt'],
},
spl: {
displayName: 'Salt Player Lyrics',
extensions: ['.lrc'],
},
};
LyricFormat.allTypes = Object.keys(LyricFormat.TYPES);
function getLyricFormatDisplayName(formatType) {
return LyricFormat.TYPES[formatType].displayName;
}
LyricFormat.getLyricFormatDisplayName = getLyricFormatDisplayName;
function getLyricFormatFileExtensions(formatType) {
return LyricFormat.TYPES[formatType].extensions;
}
LyricFormat.getLyricFormatFileExtensions = getLyricFormatFileExtensions;
async function requestReadLyricsFile(description, languageExtensions) {
const fileTypes = languageExtensions.map((ext) => `${ext}`);
// @ts-ignore
const fileHandles = await window.showOpenFilePicker({
types: [
{
description,
accept: { 'text/lyricsSpecific': [...fileTypes] },
},
],
});
if (fileHandles.length > 0) {
const fileHandle = fileHandles[0]; // Access the first file handle
const file = await fileHandle.getFile();
const contents = await file.text();
return contents;
}
return null;
}
LyricFormat.requestReadLyricsFile = requestReadLyricsFile;
async function requestWriteLyricsFile(description, languageExtensions, content, options) {
const fileTypes = languageExtensions.map((ext) => `${ext}`);
// @ts-ignore
const fileHandle = await window.showSaveFilePicker({
types: [
{
description,
accept: { 'text/lyricsSpecific': [...fileTypes] },
},
],
suggestedName: options?.fileName || 'lyrics',
});
if (fileHandle) {
const writable = await fileHandle.createWritable();
await writable.write(content);
await writable.close();
return true;
}
return false;
}
LyricFormat.requestWriteLyricsFile = requestWriteLyricsFile;
})(LyricFormat || (exports.LyricFormat = LyricFormat = {}));