Files
QZMusic_PC-pre/amll-local/packages/lyric/src/formats/yrc.ts
lqtmcstudio 72f4510dc8 fork(fix): Clone AMLL 并修复 BUG
- 将AMLL Clone到本以地进行修复和优化(emm虽然这很不优雅但是暂时无时间做子模块和Fork)
- 修复在当前播放歌词行不可见的视口Seek会出现滚动偏移的问题
2026-06-07 00:02:14 +08:00

141 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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");
}