|
|
@@ -2,6 +2,7 @@ import { IParser } from './IParser';
|
|
|
import { Lyric } from '../bean/Lyric';
|
|
|
import { LyricLine } from '../bean/LyricLine';
|
|
|
import { printD, printW } from '../extensions/Extension';
|
|
|
+import { LyricWord } from '../bean/LyricWord';
|
|
|
|
|
|
/**
|
|
|
* The parser to parse the string array of a standard lyric file.
|
|
|
@@ -28,7 +29,7 @@ export class LyricParser implements IParser {
|
|
|
let album = ""
|
|
|
let by = ""
|
|
|
let offset = 0
|
|
|
- const ignoredTags = [ 'hash', 'sign', 'qq', 'total']; // 定义需要忽略的标签
|
|
|
+ const ignoredTags = [ 'hash', 'sign', 'qq', 'total','Outro']; // 定义需要忽略的标签
|
|
|
for (let i = 0; i < src.length; i++) {
|
|
|
let line = src[i]
|
|
|
console.info(`content The line of file:line ${line}`);
|
|
|
@@ -37,6 +38,7 @@ export class LyricParser implements IParser {
|
|
|
printW("the lyric line is empty, carriage return or line feed, line index= " + i)
|
|
|
continue
|
|
|
}
|
|
|
+
|
|
|
// 检查是否是需要忽略的标签
|
|
|
const shouldIgnore = ignoredTags.some(tag => line.indexOf(tag) > 0);
|
|
|
if (shouldIgnore) {
|
|
|
@@ -54,23 +56,40 @@ export class LyricParser implements IParser {
|
|
|
} else if (line.indexOf("offset") > 0) {
|
|
|
offset = Number.parseInt(this.parseIdTag(line))
|
|
|
} else {
|
|
|
- // [00:00.10]画心 - 张靓颖
|
|
|
- // [01:05.49][02:08.40]看不穿 是你失落的魂魄
|
|
|
- let spr = line.split(']')
|
|
|
- if (spr.length <= 1) {
|
|
|
- printW("the lyric line is no timestamp, line index= " + i)
|
|
|
- continue
|
|
|
+
|
|
|
+ // 新增逐字歌词解析逻辑[mm:ss.xx] <mm:ss.xx>
|
|
|
+ if (this.isWordByWordLyric(line)) {
|
|
|
+ const { timeline, words } = this.parseWordByWordLine(line, offset);
|
|
|
+ lyricLines.push(new LyricLine('', timeline, -1, words))
|
|
|
+ continue;
|
|
|
}
|
|
|
- // parse text
|
|
|
- let text = spr[spr.length-1]
|
|
|
- // printD("text= " + text)
|
|
|
- // parse timeline
|
|
|
- for (let i = 0;i < spr.length - 1; i++) {
|
|
|
- let timeline = spr[i].replace("[", "")
|
|
|
- let timeStamp = this.parseTimeline(timeline)
|
|
|
- // printD("timestamp= " + timeStamp)
|
|
|
- lyricLines.push(new LyricLine(text, timeStamp - offset, -1))
|
|
|
+
|
|
|
+ // 新增:逐字歌词[]检测方括号逐字歌词格式 [mm:ss.xxx] 文字
|
|
|
+ if (this.isSquareBracketWordByWordLyric(line)) {
|
|
|
+ const { timeline, words } = this.parseSquareBracketWordLine(line, offset);
|
|
|
+ if (words.length > 0) {
|
|
|
+ lyricLines.push(new LyricLine('', timeline, -1, words))
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 原逻辑处理,但支持逐字歌词
|
|
|
+ // [00:00.10]画心 - 张靓颖
|
|
|
+ // [01:05.49][02:08.40]看不穿 是你失落的魂魄
|
|
|
+ let spr = line.split(']');
|
|
|
+ if (spr.length <= 1) {
|
|
|
+ printW("the lyric line is no timestamp, line index= " + i)
|
|
|
+ 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) => {
|
|
|
@@ -85,10 +104,115 @@ export class LyricParser implements IParser {
|
|
|
lyricLine.nextTime = next.beginTime
|
|
|
}
|
|
|
}
|
|
|
+ // 为逐字歌词填充text(拼接所有歌词词)
|
|
|
+ this.populateTextForWordLyrics(lyricLines);
|
|
|
let result = new Lyric(artist, title, album, by, offset, lyricLines)
|
|
|
return result
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+ // 检测方括号格式的逐字歌词 [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):
|
|
|
+ { timeline: number, words: LyricWord[] } {
|
|
|
+
|
|
|
+ const words: LyricWord[] = [];
|
|
|
+ let firstTimeline = -1;
|
|
|
+
|
|
|
+ // 正则匹配:[00:00.000]中文字
|
|
|
+ const regex = /\[(\d{2}:\d{2}\.\d{2,3})\]([^\[]*)/g;
|
|
|
+ let match;
|
|
|
+
|
|
|
+ while ((match = regex.exec(line)) !== null) {
|
|
|
+ const timeStr = match[1]; // 时间部分 00:00.000
|
|
|
+ const word = match[2].trim(); // 歌词文本
|
|
|
+
|
|
|
+ if (!word) continue; // 跳过空词
|
|
|
+
|
|
|
+ const timeline = this.parseTimeline2(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; // 默认200ms
|
|
|
+ }
|
|
|
+
|
|
|
+ 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]
|