Skip to content

Commit 5c64302

Browse files
authored
Merge pull request #995 from MoYingJi/pr/ral
refactor(lyricParser): 修正并移动 alignLocalLyrics
2 parents a94e505 + e3a6f34 commit 5c64302

2 files changed

Lines changed: 87 additions & 48 deletions

File tree

src/core/player/LyricManager.ts

Lines changed: 4 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { isElectron } from "@/utils/env";
99
import { applyBracketReplacement } from "@/utils/lyric/lyricFormat";
1010
import { applyProfanityUncensor } from "@/utils/lyric/lyricProfanity";
1111
import {
12+
alignLyricLines,
1213
alignLyrics,
1314
isWordLevelFormat,
1415
parseQRCLyric,
@@ -110,48 +111,6 @@ class LyricManager {
110111
}
111112
}
112113

113-
/**
114-
* 对齐本地歌词
115-
* @param lyricData 本地歌词数据
116-
* @returns 对齐后的本地歌词数据
117-
*/
118-
private alignLocalLyrics(lyricData: SongLyric): SongLyric {
119-
// 同一时间的两/三行分别作为主句、翻译、音译
120-
const toTime = (line: LyricLine) => Number(line?.startTime ?? line?.words?.[0]?.startTime ?? 0);
121-
// 获取结束时间
122-
const toEndTime = (line: LyricLine) =>
123-
Number(line?.endTime ?? line?.words?.[line?.words?.length - 1]?.endTime ?? 0);
124-
// 取内容
125-
const toText = (line: LyricLine) => String(line?.words?.[0]?.word || "").trim();
126-
const lrc = lyricData.lrcData || [];
127-
if (!lrc.length) return lyricData;
128-
// 按开始时间分组,时间差 < 0.6s 视为同组
129-
const sorted = [...lrc].sort((a, b) => toTime(a) - toTime(b));
130-
const groups: LyricLine[][] = [];
131-
for (const line of sorted) {
132-
const st = toTime(line);
133-
const last = groups[groups.length - 1]?.[0];
134-
if (last && Math.abs(st - toTime(last)) < 0.6) groups[groups.length - 1].push(line);
135-
else groups.push([line]);
136-
}
137-
// 组装:第 1 行主句;第 2 行翻译;第 3 行音译;不调整时长
138-
const aligned = groups.map((group) => {
139-
const base = { ...group[0] } as LyricLine;
140-
const tran = group[1];
141-
const roma = group[2];
142-
if (!base.translatedLyric && tran) {
143-
base.translatedLyric = toText(tran);
144-
base.endTime = Math.max(toEndTime(base), toEndTime(tran));
145-
}
146-
if (!base.romanLyric && roma) {
147-
base.romanLyric = toText(roma);
148-
base.endTime = Math.max(toEndTime(base), toEndTime(roma));
149-
}
150-
return base;
151-
});
152-
return { lrcData: aligned, yrcData: lyricData.yrcData };
153-
}
154-
155114
/**
156115
* 从 QQ 音乐获取歌词(封装方法,供在线和本地歌曲使用)
157116
* @param song 歌曲对象,内部自动判断本地/在线并生成缓存 key
@@ -478,7 +437,7 @@ class LyricManager {
478437
};
479438
}
480439
// 普通格式
481-
let aligned = this.alignLocalLyrics({ lrcData: parsedLines, yrcData: [] });
440+
let aligned: SongLyric = { lrcData: alignLyricLines(parsedLines), yrcData: [] };
482441
let usingQRCLyric = false;
483442
// 如果开启了本地歌曲 QQ 音乐匹配,尝试获取逐字歌词
484443
if (settingStore.localLyricQQMusicMatch && song) {
@@ -806,11 +765,8 @@ class LyricManager {
806765
if (isWordLevelFormat(format)) {
807766
result.yrcData = lines;
808767
} else {
809-
result.lrcData = lines;
810768
// 应用翻译对齐逻辑
811-
const aligned = this.alignLocalLyrics(result);
812-
result.lrcData = aligned.lrcData;
813-
result.yrcData = aligned.yrcData;
769+
result.lrcData = alignLyricLines(lines);
814770
}
815771
}
816772
}
@@ -887,7 +843,7 @@ class LyricManager {
887843
const overrideResult = await this.fetchLocalOverrideLyric(song.id);
888844
if (!isEmpty(overrideResult.data.lrcData) || !isEmpty(overrideResult.data.yrcData)) {
889845
// 对齐
890-
overrideResult.data = this.alignLocalLyrics(overrideResult.data);
846+
overrideResult.data.lrcData = alignLyricLines(overrideResult.data.lrcData);
891847
fetchResult = overrideResult;
892848
} else if (song.path) {
893849
// 本地文件

src/utils/lyric/lyricParser.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,89 @@ export const alignLyrics = (
299299
return result;
300300
};
301301

302+
/**
303+
* 对齐歌词的翻译和音译
304+
* 根据开始时间将同一时间的多行歌词分为一组,第一行作为主句,第二行作为翻译,第三行作为音译
305+
* @param lyrics 未设置翻译和音译的歌词数据
306+
* @param endTime 对齐时如何处理附加行的结束时间(忽略、匹配、设为最大值)
307+
* @param maxTimeDiff 允许匹配的最大时间差(单位:毫秒),超过该时间差的行将不会被视为同一行
308+
* @returns 对齐后的歌词数据
309+
*/
310+
export const alignLyricLines = (
311+
lyrics: LyricLine[],
312+
{
313+
endTime = "set",
314+
maxTimeDiff = 0, // 默认严格匹配
315+
}: Partial<{
316+
endTime: "ignore" | "match" | "set";
317+
maxTimeDiff: number;
318+
}> = {},
319+
): LyricLine[] => {
320+
if (!lyrics.length) return [];
321+
// 获取开始时间
322+
const toStartTime = (line: LyricLine) =>
323+
Number(line?.startTime ?? line?.words?.[0]?.startTime ?? 0);
324+
// 获取结束时间
325+
const toEndTime = (line: LyricLine) =>
326+
Number(line?.endTime ?? line?.words?.[line?.words?.length - 1]?.endTime ?? 0);
327+
// 取内容
328+
const toText = (line: LyricLine) => String(line?.words?.map((w) => w.word).join("") || "").trim();
329+
// 是否匹配
330+
const isTimeMatch = (baseLine: LyricLine | undefined, addLine: LyricLine | undefined) => {
331+
if (!baseLine || !addLine) return false;
332+
const timeDiff = Math.abs(toStartTime(baseLine) - toStartTime(addLine));
333+
if (timeDiff > maxTimeDiff) return false;
334+
if (endTime === "match") {
335+
const endTimeDiff = Math.abs(toEndTime(baseLine) - toEndTime(addLine));
336+
if (endTimeDiff > maxTimeDiff) return false;
337+
}
338+
return true;
339+
};
340+
// 按开始时间分组
341+
const sorted = [...lyrics].sort((a, b) => toStartTime(a) - toStartTime(b));
342+
const groups: LyricLine[][] = [];
343+
for (const line of sorted) {
344+
const last = groups[groups.length - 1]?.[0];
345+
if (isTimeMatch(last, line)) groups[groups.length - 1].push(line);
346+
else groups.push([line]);
347+
}
348+
// 合并附加行
349+
const mergeAddLine = (
350+
baseLine: LyricLine,
351+
addLine: LyricLine | undefined,
352+
key: "translatedLyric" | "romanLyric",
353+
) => {
354+
if (baseLine[key] || !addLine) return;
355+
const addText = toText(addLine);
356+
if (!addText) return;
357+
baseLine[key] = addText;
358+
// 如果需要设置主行的结束时间,则将主行的结束时间设置为主行和附加行结束时间的较大值
359+
if (endTime !== "set") return;
360+
const oldEndTime = toEndTime(baseLine);
361+
const addEndTime = toEndTime(addLine);
362+
if (!Number.isFinite(addEndTime) || addEndTime <= oldEndTime) return;
363+
baseLine.endTime = addEndTime;
364+
// 考虑句中最后一个字的结束时间
365+
if (baseLine.words?.length) {
366+
const lastWord = baseLine.words[baseLine.words.length - 1];
367+
const lastWordEndTime = lastWord.endTime;
368+
if (lastWordEndTime === oldEndTime) {
369+
lastWord.endTime = addEndTime;
370+
}
371+
}
372+
};
373+
// 组装:第 1 行主句;第 2 行翻译;第 3 行音译;其余行舍去
374+
const aligned = groups.map((group) => {
375+
const base = { ...group[0] } as LyricLine;
376+
const tran = group[1];
377+
const roma = group[2];
378+
mergeAddLine(base, tran, "translatedLyric");
379+
mergeAddLine(base, roma, "romanLyric");
380+
return base;
381+
});
382+
return aligned;
383+
};
384+
302385
/**
303386
* 解析 QRC 内容为行数据
304387
*/

0 commit comments

Comments
 (0)