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

110 lines
2.7 KiB
TypeScript

/**
* @fileoverview ESLyric 逐词歌词格式解析与生成。
* 每行以行首时间戳开头,后续每个单词后都跟一个结束时间戳。
*
* 格式示例:
* [00:10.82]Test[00:10.97] Word[00:12.62]
* [00:12.62]Next[00:13.20] line[00:14.10]
*/
import type { LyricLine, LyricWord } from "../types";
import {
clampTimestamp,
createLine,
createWord,
formatTime,
parseTime,
} from "../utils";
const TIME_REGEX = /^\[((?:\d+:)*\d+(?:\.\d+)?)\]/;
function parseTimestampPrefix(
src: string,
): { time: number; length: number } | null {
const match = src.match(TIME_REGEX);
if (!match) return null;
const [raw, timeStr] = match;
return { time: parseTime(timeStr), length: raw.length };
}
function parseEslrcLine(rawLine: string): LyricLine | null {
let src = rawLine.trim();
const first = parseTimestampPrefix(src);
if (!first) return null;
src = src.slice(first.length);
let startTime = first.time;
if (!src.trim()) return null;
const words: LyricWord[] = [];
while (src.trim().length > 0) {
const nextTimePos = src.indexOf("[");
if (nextTimePos <= 0) return null;
const word = src.slice(0, nextTimePos);
const nextTime = parseTimestampPrefix(src.slice(nextTimePos));
if (!nextTime) return null;
words.push(
createWord({
word,
startTime,
endTime: nextTime.time,
}),
);
src = src.slice(nextTimePos + nextTime.length);
startTime = nextTime.time;
}
return createLine({ words });
}
/**
* 解析 ESLyric 逐词歌词格式字符串
* @param eslrc 歌词字符串
* @returns 成功解析出来的歌词
*/
export function parseEslrc(eslrc: string): LyricLine[] {
const result: LyricLine[] = [];
for (const rawLine of eslrc.split(/\r?\n/)) {
const line = parseEslrcLine(rawLine);
if (line) result.push(line);
}
result.sort(
(a, b) =>
(a.words[0]?.startTime ?? Number.MAX_SAFE_INTEGER) -
(b.words[0]?.startTime ?? Number.MAX_SAFE_INTEGER),
);
for (const line of result) {
for (const word of line.words) {
word.startTime = clampTimestamp(word.startTime);
word.endTime = clampTimestamp(word.endTime);
}
line.startTime = clampTimestamp(line.words[0]?.startTime ?? 0);
line.endTime = clampTimestamp(
line.words[line.words.length - 1]?.endTime ?? 0,
);
}
return result;
}
/**
* 将歌词数组转换为 ESLyric 逐词歌词格式字符串
* @param lines 歌词数组
* @returns ESLyric 逐词歌词格式字符串
*/
export function stringifyEslrc(lines: LyricLine[]): string {
return lines
.map((line) => {
if (!line.words.length) return "";
return `[${formatTime(clampTimestamp(line.words[0].startTime))}]${line.words
.map(
(word) => `${word.word}[${formatTime(clampTimestamp(word.endTime))}]`,
)
.join("")}`;
})
.filter(Boolean)
.join("\n");
}