import { IParser } from './IParser'; import { Lyric } from '../bean/Lyric'; import { LyricLine } from '../bean/LyricLine'; import { LyricWord } from '../bean/LyricWord'; /** * The parser to parse the string array of a standard lyric file. */ export class LyricParser implements IParser { /** * Parse the string array to a Lyric. * @param src The lyric source, a string array. * @returns A lyric instance. */ parse(src: Array): Lyric { let lyricLines = new Array() let title = "" let artist = "" let album = "" let by = "" let offset = 0 const ignoredTags = [ 'id', 'hash', 'sign', 'qq', 'total','Outro','Intro']; // 定义需要忽略的标签 // 首先检测是否是纯文本歌词(没有时间标签) let hasValidTimeTag = false; for (let i = 0; i < src.length; i++) { let line = src[i].trim(); if (line.length > 0 && /^\[\d{2}:\d{2}\.\d{2,3}\]/.test(line)) { hasValidTimeTag = true; break; } } // 如果没有时间标签,作为纯文本歌词处理 if (!hasValidTimeTag) { // printD("检测到纯文本歌词(无时间标签),不添加时间标签"); return this.parsePlainTextLyric(src); } for (let i = 0; i < src.length; i++) { let line = src[i] if (line == "" || line == "\n" || line == "\r" || line == "\r\n" || line == "[Verse]" || line == "[Chorus]" || line == "[PreChorus]" || line == "[PreChorus]"||line == "[Bridge]") { // printW("the lyric line is empty, carriage return or line feed, line index= " + i) continue } // 检查是否是需要忽略的标签 // 修改后的标签检测逻辑:只有在[]中的标签才忽略 const shouldIgnore = ignoredTags.some(tag => { const tagPattern = new RegExp(`\\[${tag}[^\\]]*\\]`, 'i'); // 匹配[tag...]格式,忽略大小写 return tagPattern.test(line); }); if (shouldIgnore) { // printW(`the lyric line contains ignored tag, line index= ${i}`); continue; } // 修改后的标签检测逻辑,修复英文歌词的时候,部分歌词没有显示出来。 if (line.startsWith("[ti:")) { title = this.parseIdTag(line); } else if (line.startsWith("[ar:")) { artist = this.parseIdTag(line); } else if (line.startsWith("[al:")) { album = this.parseIdTag(line); } else if (line.startsWith("[by:")) { by = this.parseIdTag(line); } else if (line.startsWith("[offset:")) { offset = Number.parseInt(this.parseIdTag(line)); } else { // 处理双语歌词的特殊情况(英文行+中文行交替) if (this.isBilingualLyric(src, i)) { const englishLine = src[i]; const chineseLine = src[i+1]; // 解析英文逐字歌词 const { timeline, words } = this.parseWordByWordLine(englishLine, offset); // 获取中文文本(去掉时间戳) const chineseText = chineseLine.replace(/^\[\d{2}:\d{2}\.\d{2}\]/, '').trim(); // 创建双语歌词行(将中文作为附加文本) const bilingualLine = new LyricLine('', timeline, -1, words); bilingualLine.translation = chineseText; // 新增translation字段存储翻译 lyricLines.push(bilingualLine); i++; // 跳过已处理的中文行 continue; } // 新增逐字歌词解析逻辑[mm:ss.xx] if (this.isWordByWordLyric(line)) { const { timeline, words } = this.parseWordByWordLine(line, offset); lyricLines.push(new LyricLine('', timeline, -1, words)) continue; } // 新增:逐字歌词[]检测方括号逐字歌词格式 [mm:ss.xxx] 文字 if (this.isSquareBracketWordByWordLyric(line)) { // console.info(`onecold 普通歌词处理中`) const { timeline, words,translation } = this.parseSquareBracketWordLine(line, offset,src[i+1]); // if (words.length > 0) { // lyricLines.push(new LyricLine('', timeline, -1, words)) // } if (words.length > 0) { const lyricLine = new LyricLine('', timeline, -1, words, translation); lyricLines.push(lyricLine); if (translation) i++; // 跳过已处理的中文行 } continue; } else { // 原逻辑处理,但支持逐字歌词 // [00:00.10]画心 - 张靓颖 // [01:05.49][02:08.40]看不穿 是你失落的魂魄 let spr = line.split(']'); if (spr.length <= 1) { continue } // parse text let text = spr[spr.length-1] // ... 原来的文本解析逻辑保持不变 ... for (let i = 0; i < spr.length - 1; i++) { let timeline = spr[i].replace("[", ""); let timeStamp = this.parseTimeline(timeline); lyricLines.push(new LyricLine(text, timeStamp - offset, -1)); } } } } lyricLines.sort((l1, l2) => { return l1.beginTime - l2.beginTime }) for (let i = 0;i < lyricLines.length; i++) { let lyricLine = lyricLines[i] if (i == lyricLines.length - 1) { lyricLine.nextTime = lyricLine.beginTime + 1000 - offset } else { let next = lyricLines[i+1] lyricLine.nextTime = next.beginTime } } // 为逐字歌词填充text(拼接所有歌词词) this.populateTextForWordLyrics(lyricLines); let result = new Lyric(artist, title, album, by, offset, lyricLines) return result } // 新增判断是否为双语歌词的方法 private isBilingualLyric(lines: string[], currentIndex: number): boolean { if (currentIndex + 1 >= lines.length) return false; const currentLine = lines[currentIndex]; const nextLine = lines[currentIndex + 1]; // 检查当前行是否是英文逐字歌词 const isEnglishLine = this.isWordByWordLyric(currentLine) && /[a-zA-Z]/.test(currentLine); // 检查下一行是否是纯中文歌词(带时间戳但不含逐字标记) const isChineseLine = /^\[\d{2}:\d{2}\.\d{2}\]/.test(nextLine) && !this.isWordByWordLyric(nextLine) && /[\u4e00-\u9fa5]/.test(nextLine); return isEnglishLine && isChineseLine; } // 检测方括号格式的逐字歌词 [00:00.000]文[00:01.000]字 private isSquareBracketWordByWordLyric(line: string): boolean { return /\[\d{2}:\d{2}\.\d{2,3}\]\S/.test(line); } private parseSquareBracketWordLine( line: string, offset: number, nextLine?: string ): { timeline: number, words: LyricWord[], translation?: string } { const words: LyricWord[] = []; let firstTimeline = -1; // 解析逐字歌词(原有逻辑) const regex = /\[(\d{2}:\d{2}[.:]\d{2,5})\]([^\[]*)/g; let match; while ((match = regex.exec(line)) !== null) { const timeStr = match[1]; const word = match[2].trim(); if (!word) continue; const timeline = this.parseTimeline(timeStr) - offset; if (firstTimeline < 0) firstTimeline = timeline; words.push(new LyricWord(word, timeline, 0)); } // 计算词持续时间 for (let i = 0; i < words.length - 1; i++) { words[i].duration = words[i + 1].startTime - words[i].startTime; } if (words.length > 0 && words[words.length - 1].duration === 0) { words[words.length - 1].duration = 200; } // 双语支持 if (nextLine && /^\[\d{2}:\d{2}\.\d{2,3}\]/.test(nextLine)) { // 提取两行时间戳(需完全一致) const currentLineTime = line.match(/^\[(\d{2}:\d{2}\.\d{2,3})\]/)?.[1]; const nextLineTime = nextLine.match(/^\[(\d{2}:\d{2}\.\d{2,3})\]/)?.[1]; if (currentLineTime && nextLineTime && currentLineTime === nextLineTime) { const chineseText = nextLine.replace(/^\[\d{2}:\d{2}\.\d{2,3}\]/, '').trim(); if (chineseText) { // 非空文本才作为翻译 return { timeline: firstTimeline, words, translation: chineseText }; } } } return { timeline: firstTimeline, words }; } /******************** 时间解析增强 ********************/ private parseTimeline2(timeString: string): number { // 增强支持毫秒/厘秒解析 const parts = timeString.split(':'); const minutes = parseInt(parts[0], 10); const secondParts = parts[1].split('.'); const seconds = parseInt(secondParts[0], 10); const fraction = parseInt(secondParts[1], 10); // 根据小数位长度判断时间精度 const milliseconds = secondParts[1].length === 2 ? fraction * 10 : // 厘秒转毫秒 (01 -> 10ms) fraction; // 毫秒直接使用 return minutes * 60000 + seconds * 1000 + milliseconds; } // 判断是否是逐字歌词行 private isWordByWordLyric(line: string): boolean { const hasBracketTimestamp = /$$\d{2}:\d{2}\.\d{2,3}$$\S/.test(line); const hasAngleTimestamp = /<\d{2}:\d{2}\.\d{2,3}>/.test(line); return hasBracketTimestamp || hasAngleTimestamp; } // 解析逐字歌词行 private parseWordByWordLine(line: string, offset: number): { timeline: number, words: LyricWord[] } { const words: LyricWord[] = []; let firstTimeline = -1; const regex = /((?:<|$$)(\d{2}:\d{2}\.\d{2,3})(?:>|$$))([^<\[]*)/g; let match; while ((match = regex.exec(line)) !== null) { const [_, tag, timeStr, word] = match; if (word.trim() === '') continue; const timeline = this.parseTimeline(timeStr) - offset; if (firstTimeline < 0) firstTimeline = timeline; words.push(new LyricWord(word.trim(), timeline, 0)); } // 设置每个词持续时间(下一个词开始时间-当前词开始时间) for (let i = 0; i < words.length - 1; i++) { words[i].duration = words[i + 1].startTime - words[i].startTime; } if (words.length > 0 && words[words.length - 1].duration === 0) { // 最后一个词持续200ms words[words.length - 1].duration = 200; } return { timeline: firstTimeline, words }; } // 为逐字歌词拼接整行文本 private populateTextForWordLyrics(lyricLines: LyricLine[]) { lyricLines.forEach(line => { if (line.words.length > 0) { line.text = line.words.map(word => word.word).join(''); } }); } private parseIdTag(line: string): string { let spr = line.split(":") let spr1 = spr[1] let result = spr1.replace("]", "") return result } // // private parseTimeline(timeString: string): number { // // 00:00.50 // let timeStringList = timeString.split(':') // let minuteString = timeStringList[0] //00 // let minute = Number.parseInt(minuteString) // let secondStrings = timeStringList[1] //00.50 // let secondStringList = secondStrings.split(".") // let secondString = secondStringList[0] //00 // let millionSecondString = secondStringList[1] //50 // let seconds = Number.parseInt(secondString) // let millionSecond = Number.parseInt(millionSecondString) // return minute * 60000 + seconds * 1000 + millionSecond // covert to million seconds // } private parseTimeline(timeString: string): number { // 支持多种分隔符:`.` 或 `:` const parts = timeString.split(/[:.]/); if (parts.length < 2) { // printW(`Invalid timeline format: ${timeString}`); return 0; } // 解析分钟、秒、毫秒 const minutes = parseInt(parts[0], 10) || 0; const seconds = parseInt(parts[1], 10) || 0; let milliseconds = 0; // 处理毫秒部分(可能为厘秒或毫秒) if (parts.length > 2) { const fraction = parts[2]; // 根据小数位长度判断精度:2位为厘秒(需*10),3位以上为毫秒 milliseconds = fraction.length <= 2 ? parseInt(fraction, 10) * 10 : // 厘秒转毫秒(如 "54" -> 540ms) parseInt(fraction.substring(0, 3), 10); // 截取前3位(如 "54001" -> 540ms) } return minutes * 60000 + seconds * 1000 + milliseconds; } /** * 解析纯文本歌词(没有时间标签的歌词) * @param src 歌词行数组 * @returns Lyric 对象 */ private parsePlainTextLyric(src: Array): Lyric { let lyricLines = new Array() let title = "" let artist = "" let album = "" let by = "" let offset = 0 for (let i = 0; i < src.length; i++) { let line = src[i].trim() // 跳过空行 if (line.length === 0 || line === "\n" || line === "\r" || line === "\r\n") { continue } // 跳过元数据行(以 [ti:、[ar:、[al:、[by:、[offset: 等开头) if (line.startsWith('[ti:') || line.startsWith('[ar:') || line.startsWith('[al:') || line.startsWith('[by:') || line.startsWith('[offset:') || line.startsWith('[length:') || line.startsWith('[id:') || line.startsWith('[hash:') || line.startsWith('[sign:')) { // 解析元数据 if (line.startsWith("[ti:")) { title = this.parseIdTag(line); } else if (line.startsWith("[ar:")) { artist = this.parseIdTag(line); } else if (line.startsWith("[al:")) { album = this.parseIdTag(line); } else if (line.startsWith("[by:")) { by = this.parseIdTag(line); } else if (line.startsWith("[offset:")) { offset = Number.parseInt(this.parseIdTag(line)); } continue } // 跳过标签行(如 [Verse]、[Chorus] 等) if (line === "[Verse]" || line === "[Chorus]" || line === "[PreChorus]" || line === "[Bridge]" || line === "[Intro]" || line === "[Outro]") { continue } // 跳过其他以 [ 开头但不是时间标签的行 if (line.startsWith('[') && !/^\[\d{2}:\d{2}\.\d{2,3}\]/.test(line)) { continue } // 不添加时间标签,使用 -1 表示纯文本歌词 lyricLines.push(new LyricLine(line, -1, -1)) } // printD(`纯文本歌词解析完成,共 ${lyricLines.length} 行歌词`) // 标记为纯文本歌词 let result = new Lyric(artist, title, album, by, offset, lyricLines, true) return result } }