fork(fix): Clone AMLL 并修复 BUG

- 将AMLL Clone到本以地进行修复和优化(emm虽然这很不优雅但是暂时无时间做子模块和Fork)
- 修复在当前播放歌词行不可见的视口Seek会出现滚动偏移的问题
This commit is contained in:
lqtmcstudio
2026-06-07 00:02:14 +08:00
parent 783d2c3dee
commit 72f4510dc8
458 changed files with 86075 additions and 1665 deletions

View File

@@ -0,0 +1,140 @@
/**
* @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");
}