forked from miao-moe/QZMusic_PC
141 lines
4.0 KiB
TypeScript
141 lines
4.0 KiB
TypeScript
/**
|
||
* @fileoverview YRC(网易云音乐逐词歌词)格式解析与生成。
|
||
* 行开头为 [startTime,duration],每个词为 (startTime,duration,0)word
|
||
*
|
||
* 格式示例:
|
||
* [190871,1984](190871,361,0)For (191232,172,0)the (191404,376,0)first (191780,1075,0)time
|
||
* [193459,4198](193459,412,0)What's (193871,574,0)past (194445,506,0)is (194951,2706,0)past
|
||
*/
|
||
import type { LyricLine, LyricWord } from "../types";
|
||
import {
|
||
createLine,
|
||
createWord,
|
||
normalizeDuration,
|
||
normalizeTimestamp,
|
||
} from "../utils";
|
||
|
||
const beginParenPattern = /^[((]/;
|
||
const endParenPattern = /[))]$/;
|
||
function checkIsBG(words: LyricWord[]): boolean {
|
||
return (
|
||
words.length > 0 &&
|
||
beginParenPattern.test(words[0].word) &&
|
||
endParenPattern.test(words[words.length - 1].word)
|
||
);
|
||
}
|
||
function trimBGParentheses(words: LyricWord[]): void {
|
||
words[0].word = words[0].word.slice(1);
|
||
words[words.length - 1].word = words[words.length - 1].word.slice(0, -1);
|
||
}
|
||
|
||
/**
|
||
* 解析 YRC 格式的歌词字符串
|
||
* @param yrc 歌词字符串
|
||
* @returns 成功解析出来的歌词
|
||
*/
|
||
export function parseYrc(yrc: string): LyricLine[] {
|
||
const wordPattern = /^(.*?)\((\d+),(\d+),0\)/;
|
||
const linePattern = /^\[(\d+),(\d+)\]/;
|
||
|
||
const lines = yrc
|
||
.split(/\r?\n/)
|
||
.map((l) => l.trim())
|
||
.filter((l) => l.length > 0);
|
||
|
||
return lines
|
||
.map((lineStr) => {
|
||
const lineMatch = lineStr.match(linePattern);
|
||
if (!lineMatch) return null;
|
||
const [linePrefix, lineStartStr, lineDurStr] = lineMatch;
|
||
const lineStart = Number(lineStartStr);
|
||
const lineDuration = Number(lineDurStr);
|
||
|
||
const words: LyricWord[] = [];
|
||
let lineContent = lineStr.slice(linePrefix.length).trim();
|
||
if (!lineContent) return null;
|
||
|
||
let lastStart = -1;
|
||
let lastEnd = -1;
|
||
while (true) {
|
||
const wordMatch = lineContent.match(wordPattern);
|
||
if (!wordMatch) break;
|
||
const [fullMatch, lastText, wordStartStr, wordDurStr] = wordMatch;
|
||
if (lastText && lastStart !== -1)
|
||
words.push(
|
||
createWord({
|
||
word: lastText,
|
||
startTime: lastStart,
|
||
endTime: lastEnd,
|
||
}),
|
||
);
|
||
const wordStart = Number(wordStartStr);
|
||
const wordDur = Number(wordDurStr);
|
||
const wordEnd = wordStart + wordDur;
|
||
[lastStart, lastEnd] = [wordStart, wordEnd];
|
||
lineContent = lineContent.slice(fullMatch.length);
|
||
}
|
||
if (lastStart !== -1 && lineContent)
|
||
words.push(
|
||
createWord({
|
||
word: lineContent,
|
||
startTime: lastStart,
|
||
endTime: lastEnd,
|
||
}),
|
||
);
|
||
|
||
const isBG = checkIsBG(words);
|
||
if (isBG) trimBGParentheses(words);
|
||
return createLine({
|
||
startTime: lineStart,
|
||
endTime: lineStart + lineDuration,
|
||
words,
|
||
isBG,
|
||
});
|
||
})
|
||
.filter((line): line is LyricLine => line !== null);
|
||
}
|
||
|
||
function makeParenthesesFull(text: string): string {
|
||
return text.replace(/\(/g, "(").replace(/\)/g, ")");
|
||
}
|
||
|
||
/**
|
||
* 将歌词数组转换为 YRC 格式的字符串
|
||
* @param lines 歌词数组
|
||
* @returns YRC 格式的字符串
|
||
*/
|
||
export function stringifyYrc(lines: LyricLine[]): string {
|
||
return lines
|
||
.map((line) => {
|
||
const lineStart = normalizeTimestamp(line.startTime);
|
||
const lineEnd = normalizeTimestamp(line.endTime);
|
||
const lineDuration = normalizeDuration(lineEnd - lineStart);
|
||
|
||
const lineWords: string[] = [];
|
||
for (const [
|
||
index,
|
||
{ word, startTime, endTime },
|
||
] of line.words.entries()) {
|
||
if (!word.trim() && lineWords.length) {
|
||
lineWords[lineWords.length - 1] += word;
|
||
continue;
|
||
}
|
||
let printedWord = makeParenthesesFull(word);
|
||
if (line.isBG) {
|
||
if (index === 0) printedWord = `(${printedWord}`;
|
||
if (index === line.words.length - 1) printedWord += ")";
|
||
}
|
||
const normalizedWordStart = normalizeTimestamp(startTime);
|
||
const normalizedWordEnd = normalizeTimestamp(endTime);
|
||
const wordDuration = normalizeDuration(
|
||
normalizedWordEnd - normalizedWordStart,
|
||
);
|
||
lineWords.push(
|
||
`(${normalizedWordStart},${wordDuration},0)${printedWord}`,
|
||
);
|
||
}
|
||
return `[${lineStart},${lineDuration}]${lineWords.join("")}`;
|
||
})
|
||
.join("\n");
|
||
}
|