From 1f31e652f3e9efcbc66ffeb5677a88871f30e652 Mon Sep 17 00:00:00 2001 From: apoint123 <108002475+apoint123@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:28:21 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A6=84=20refactor:=20=E6=9B=B4?= =?UTF-8?q?=E6=8D=A2=E6=9B=B4=E5=A5=BD=E7=9A=84=20LRC=20=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/player/LyricManager.ts | 9 ++- src/utils/lyric/lyricParser.ts | 3 +- src/utils/parseLrc.ts | 128 ++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 src/utils/parseLrc.ts diff --git a/src/core/player/LyricManager.ts b/src/core/player/LyricManager.ts index abf225255..5ed9b601d 100644 --- a/src/core/player/LyricManager.ts +++ b/src/core/player/LyricManager.ts @@ -3,9 +3,10 @@ import { songLyric, songLyricTTML } from "@/api/song"; import { keywords as defaultKeywords, regexes as defaultRegexes } from "@/assets/data/exclude"; import { useCacheManager } from "@/core/resource/CacheManager"; import { useMusicStore, useSettingStore, useStatusStore, useStreamingStore } from "@/stores"; -import { type SongLyric, type LyricPriority } from "@/types/lyric"; -import { SongType } from "@/types/main"; +import type { LyricPriority, SongLyric } from "@/types/lyric"; +import type { SongType } from "@/types/main"; import { isElectron } from "@/utils/env"; +import { applyBracketReplacement } from "@/utils/lyric/lyricFormat"; import { alignLyrics, isWordLevelFormat, @@ -13,9 +14,9 @@ import { parseSmartLrc, } from "@/utils/lyric/lyricParser"; import { stripLyricMetadata } from "@/utils/lyric/lyricStripper"; -import { applyBracketReplacement } from "@/utils/lyric/lyricFormat"; import { getConverter } from "@/utils/opencc"; -import { type LyricLine, parseLrc, parseTTML, parseYrc } from "@applemusic-like-lyrics/lyric"; +import { parseLrc } from "@/utils/parseLrc"; +import { type LyricLine, parseTTML, parseYrc } from "@applemusic-like-lyrics/lyric"; import { cloneDeep, escapeRegExp, isEmpty } from "lodash-es"; class LyricManager { diff --git a/src/utils/lyric/lyricParser.ts b/src/utils/lyric/lyricParser.ts index 778870732..8a13fdc17 100644 --- a/src/utils/lyric/lyricParser.ts +++ b/src/utils/lyric/lyricParser.ts @@ -1,4 +1,5 @@ -import { type LyricLine, parseLrc } from "@applemusic-like-lyrics/lyric"; +import type { LyricLine } from "@applemusic-like-lyrics/lyric"; +import { parseLrc } from "../parseLrc"; /** * LRC 格式类型 diff --git a/src/utils/parseLrc.ts b/src/utils/parseLrc.ts new file mode 100644 index 000000000..f36687779 --- /dev/null +++ b/src/utils/parseLrc.ts @@ -0,0 +1,128 @@ +import type { LyricLine, LyricWord } from "@applemusic-like-lyrics/lyric"; + +interface ParsedEvent { + time: number; + text: string; + index: number; // 用于保持同时间戳下的原始顺序 +} + +/** + * 一个更好的 LRC 解析器,相比于 AMLL 的 LRC 解析器,支持丢弃空行、trim 歌词和自动检测翻译与音译行 + * @note 如果遇到相同时间戳的歌词行,第一行会作为主歌词行,第二行会作为翻译歌词,第三行会作为音译歌词 + * @param lrcContent LRC 字符串 + * @returns LyricLine 数组 + */ +export function parseLrc(lrcContent: string): LyricLine[] { + const lines = lrcContent.split(/\r?\n/); + const parsedEvents: ParsedEvent[] = []; + + const timeTagRegex = /\[(\d{1,2}):(\d{1,2})(?:[.:](\d{1,3}))?\]/g; + + let globalIndex = 0; + + for (const line of lines) { + const text = line.replace(timeTagRegex, "").trim(); + const matches = line.matchAll(timeTagRegex); + + for (const match of matches) { + const minutes = parseInt(match[1], 10); + const seconds = parseInt(match[2], 10); + const fractionStr = match[3] ? `0.${match[3]}` : "0"; + const fraction = parseFloat(fractionStr); + + const totalSeconds = minutes * 60 + seconds + fraction; + const totalMilliseconds = Math.round(totalSeconds * 1000); + + parsedEvents.push({ + time: totalMilliseconds, + text: text, + index: globalIndex++, + }); + } + } + + parsedEvents.sort((a, b) => a.time - b.time || a.index - b.index); + + const validLyricLines: LyricLine[] = []; + + let i = 0; + while (i < parsedEvents.length) { + const currentTime = parsedEvents[i].time; + + const group: ParsedEvent[] = []; + while (i < parsedEvents.length && parsedEvents[i].time === currentTime) { + group.push(parsedEvents[i]); + i++; + } + + const nextEvent = i < parsedEvents.length ? parsedEvents[i] : null; + const endTime = nextEvent ? nextEvent.time : currentTime + 10000; // 最后一行只加10秒,以便可以在频谱图上调整而不会要拉到后面很远 + + const textEvents = group.filter((e) => e.text.length > 0); + + if (textEvents.length === 0) { + continue; + } + + const mainEvent = textEvents[0]; + const mainLine = newLyricLine(); + const mainWord = newLyricWord(); + + mainWord.word = mainEvent.text; + mainWord.startTime = currentTime; + mainWord.endTime = endTime; + + mainLine.words = [mainWord]; + mainLine.startTime = currentTime; + mainLine.endTime = endTime; + + // 第二行作为翻译 + if (textEvents.length > 1) { + mainLine.translatedLyric = textEvents[1].text; + } + + // 第三行作为音译 + if (textEvents.length > 2) { + mainLine.romanLyric = textEvents[2].text; + } + + validLyricLines.push(mainLine); + + if (textEvents.length > 3) { + for (let k = 3; k < textEvents.length; k++) { + const extraEvent = textEvents[k]; + const extraLine = newLyricLine(); + const extraWord = newLyricWord(); + + extraWord.word = extraEvent.text; + extraWord.startTime = currentTime; + extraWord.endTime = endTime; + + extraLine.words = [extraWord]; + extraLine.startTime = currentTime; + extraLine.endTime = endTime; + + validLyricLines.push(extraLine); + } + } + } + + return validLyricLines; +} + +const newLyricLine = (): LyricLine => ({ + words: [], + translatedLyric: "", + romanLyric: "", + isBG: false, + isDuet: false, + startTime: 0, + endTime: 0, +}); + +const newLyricWord = (): LyricWord => ({ + startTime: 0, + endTime: 0, + word: "", + romanWord: "", +}); From 80641477bac62fea998ac2ec66138e51a3d27353 Mon Sep 17 00:00:00 2001 From: apoint123 <108002475+apoint123@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:06:21 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A6=84=20refactor:=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/parseLrc.ts | 49 +++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/src/utils/parseLrc.ts b/src/utils/parseLrc.ts index f36687779..a660d95e8 100644 --- a/src/utils/parseLrc.ts +++ b/src/utils/parseLrc.ts @@ -27,11 +27,10 @@ export function parseLrc(lrcContent: string): LyricLine[] { for (const match of matches) { const minutes = parseInt(match[1], 10); const seconds = parseInt(match[2], 10); - const fractionStr = match[3] ? `0.${match[3]}` : "0"; - const fraction = parseFloat(fractionStr); + const fractionStr = match[3] || "0"; + const milliseconds = parseInt(fractionStr.padEnd(3, "0"), 10); - const totalSeconds = minutes * 60 + seconds + fraction; - const totalMilliseconds = Math.round(totalSeconds * 1000); + const totalMilliseconds = minutes * 60 * 1000 + seconds * 1000 + milliseconds; parsedEvents.push({ time: totalMilliseconds, @@ -56,7 +55,7 @@ export function parseLrc(lrcContent: string): LyricLine[] { } const nextEvent = i < parsedEvents.length ? parsedEvents[i] : null; - const endTime = nextEvent ? nextEvent.time : currentTime + 10000; // 最后一行只加10秒,以便可以在频谱图上调整而不会要拉到后面很远 + const endTime = nextEvent ? nextEvent.time : currentTime + 10000; const textEvents = group.filter((e) => e.text.length > 0); @@ -64,17 +63,7 @@ export function parseLrc(lrcContent: string): LyricLine[] { continue; } - const mainEvent = textEvents[0]; - const mainLine = newLyricLine(); - const mainWord = newLyricWord(); - - mainWord.word = mainEvent.text; - mainWord.startTime = currentTime; - mainWord.endTime = endTime; - - mainLine.words = [mainWord]; - mainLine.startTime = currentTime; - mainLine.endTime = endTime; + const mainLine = createBasicLyricLine(textEvents[0].text, currentTime, endTime); // 第二行作为翻译 if (textEvents.length > 1) { @@ -90,18 +79,7 @@ export function parseLrc(lrcContent: string): LyricLine[] { if (textEvents.length > 3) { for (let k = 3; k < textEvents.length; k++) { - const extraEvent = textEvents[k]; - const extraLine = newLyricLine(); - const extraWord = newLyricWord(); - - extraWord.word = extraEvent.text; - extraWord.startTime = currentTime; - extraWord.endTime = endTime; - - extraLine.words = [extraWord]; - extraLine.startTime = currentTime; - extraLine.endTime = endTime; - + const extraLine = createBasicLyricLine(textEvents[k].text, currentTime, endTime); validLyricLines.push(extraLine); } } @@ -110,6 +88,21 @@ export function parseLrc(lrcContent: string): LyricLine[] { return validLyricLines; } +function createBasicLyricLine(text: string, startTime: number, endTime: number): LyricLine { + const line = newLyricLine(); + const word = newLyricWord(); + + word.word = text; + word.startTime = startTime; + word.endTime = endTime; + + line.words = [word]; + line.startTime = startTime; + line.endTime = endTime; + + return line; +} + const newLyricLine = (): LyricLine => ({ words: [], translatedLyric: "",