LyricParser.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. import { IParser } from './IParser';
  2. import { Lyric } from '../bean/Lyric';
  3. import { LyricLine } from '../bean/LyricLine';
  4. import { LyricWord } from '../bean/LyricWord';
  5. /**
  6. * The parser to parse the string array of a standard lyric file.
  7. */
  8. export class LyricParser implements IParser {
  9. /**
  10. * Parse the string array to a Lyric.
  11. * @param src The lyric source, a string array.
  12. * @returns A lyric instance.
  13. */
  14. parse(src: Array<string>): Lyric {
  15. let lyricLines = new Array<LyricLine>()
  16. let title = ""
  17. let artist = ""
  18. let album = ""
  19. let by = ""
  20. let offset = 0
  21. const ignoredTags = [ 'id', 'hash', 'sign', 'qq', 'total','Outro','Intro']; // 定义需要忽略的标签
  22. // 首先检测是否是纯文本歌词(没有时间标签)
  23. let hasValidTimeTag = false;
  24. for (let i = 0; i < src.length; i++) {
  25. let line = src[i].trim();
  26. if (line.length > 0 && /^\[\d{2}:\d{2}\.\d{2,3}\]/.test(line)) {
  27. hasValidTimeTag = true;
  28. break;
  29. }
  30. }
  31. // 如果没有时间标签,作为纯文本歌词处理
  32. if (!hasValidTimeTag) {
  33. // printD("检测到纯文本歌词(无时间标签),不添加时间标签");
  34. return this.parsePlainTextLyric(src);
  35. }
  36. for (let i = 0; i < src.length; i++) {
  37. let line = src[i]
  38. if (line == "" || line == "\n" || line == "\r" || line == "\r\n" || line == "[Verse]" || line == "[Chorus]"
  39. || line == "[PreChorus]" || line == "[PreChorus]"||line == "[Bridge]") {
  40. // printW("the lyric line is empty, carriage return or line feed, line index= " + i)
  41. continue
  42. }
  43. // 检查是否是需要忽略的标签
  44. // 修改后的标签检测逻辑:只有在[]中的标签才忽略
  45. const shouldIgnore = ignoredTags.some(tag => {
  46. const tagPattern = new RegExp(`\\[${tag}[^\\]]*\\]`, 'i'); // 匹配[tag...]格式,忽略大小写
  47. return tagPattern.test(line);
  48. });
  49. if (shouldIgnore) {
  50. // printW(`the lyric line contains ignored tag, line index= ${i}`);
  51. continue;
  52. }
  53. // 修改后的标签检测逻辑,修复英文歌词的时候,部分歌词没有显示出来。
  54. if (line.startsWith("[ti:")) {
  55. title = this.parseIdTag(line);
  56. }
  57. else if (line.startsWith("[ar:")) {
  58. artist = this.parseIdTag(line);
  59. }
  60. else if (line.startsWith("[al:")) {
  61. album = this.parseIdTag(line);
  62. }
  63. else if (line.startsWith("[by:")) {
  64. by = this.parseIdTag(line);
  65. }
  66. else if (line.startsWith("[offset:")) {
  67. offset = Number.parseInt(this.parseIdTag(line));
  68. }
  69. else {
  70. // 处理双语歌词的特殊情况(英文行+中文行交替)
  71. if (this.isBilingualLyric(src, i)) {
  72. const englishLine = src[i];
  73. const chineseLine = src[i+1];
  74. // 解析英文逐字歌词
  75. const { timeline, words } = this.parseWordByWordLine(englishLine, offset);
  76. // 获取中文文本(去掉时间戳)
  77. const chineseText = chineseLine.replace(/^\[\d{2}:\d{2}\.\d{2}\]/, '').trim();
  78. // 创建双语歌词行(将中文作为附加文本)
  79. const bilingualLine = new LyricLine('', timeline, -1, words);
  80. bilingualLine.translation = chineseText; // 新增translation字段存储翻译
  81. lyricLines.push(bilingualLine);
  82. i++; // 跳过已处理的中文行
  83. continue;
  84. }
  85. // 新增逐字歌词解析逻辑[mm:ss.xx] <mm:ss.xx>
  86. if (this.isWordByWordLyric(line)) {
  87. const { timeline, words } = this.parseWordByWordLine(line, offset);
  88. lyricLines.push(new LyricLine('', timeline, -1, words))
  89. continue;
  90. }
  91. // 新增:逐字歌词[]检测方括号逐字歌词格式 [mm:ss.xxx] 文字
  92. if (this.isSquareBracketWordByWordLyric(line)) {
  93. // console.info(`onecold 普通歌词处理中`)
  94. const { timeline, words,translation } = this.parseSquareBracketWordLine(line, offset,src[i+1]);
  95. // if (words.length > 0) {
  96. // lyricLines.push(new LyricLine('', timeline, -1, words))
  97. // }
  98. if (words.length > 0) {
  99. const lyricLine = new LyricLine('', timeline, -1, words, translation);
  100. lyricLines.push(lyricLine);
  101. if (translation) i++; // 跳过已处理的中文行
  102. }
  103. continue;
  104. } else {
  105. // 原逻辑处理,但支持逐字歌词
  106. // [00:00.10]画心 - 张靓颖
  107. // [01:05.49][02:08.40]看不穿 是你失落的魂魄
  108. let spr = line.split(']');
  109. if (spr.length <= 1) {
  110. continue
  111. }
  112. // parse text
  113. let text = spr[spr.length-1]
  114. // ... 原来的文本解析逻辑保持不变 ...
  115. for (let i = 0; i < spr.length - 1; i++) {
  116. let timeline = spr[i].replace("[", "");
  117. let timeStamp = this.parseTimeline(timeline);
  118. lyricLines.push(new LyricLine(text, timeStamp - offset, -1));
  119. }
  120. }
  121. }
  122. }
  123. lyricLines.sort((l1, l2) => {
  124. return l1.beginTime - l2.beginTime
  125. })
  126. for (let i = 0;i < lyricLines.length; i++) {
  127. let lyricLine = lyricLines[i]
  128. if (i == lyricLines.length - 1) {
  129. lyricLine.nextTime = lyricLine.beginTime + 1000 - offset
  130. } else {
  131. let next = lyricLines[i+1]
  132. lyricLine.nextTime = next.beginTime
  133. }
  134. }
  135. // 为逐字歌词填充text(拼接所有歌词词)
  136. this.populateTextForWordLyrics(lyricLines);
  137. let result = new Lyric(artist, title, album, by, offset, lyricLines)
  138. return result
  139. }
  140. // 新增判断是否为双语歌词的方法
  141. private isBilingualLyric(lines: string[], currentIndex: number): boolean {
  142. if (currentIndex + 1 >= lines.length) return false;
  143. const currentLine = lines[currentIndex];
  144. const nextLine = lines[currentIndex + 1];
  145. // 检查当前行是否是英文逐字歌词
  146. const isEnglishLine = this.isWordByWordLyric(currentLine) &&
  147. /[a-zA-Z]/.test(currentLine);
  148. // 检查下一行是否是纯中文歌词(带时间戳但不含逐字标记)
  149. const isChineseLine = /^\[\d{2}:\d{2}\.\d{2}\]/.test(nextLine) &&
  150. !this.isWordByWordLyric(nextLine) &&
  151. /[\u4e00-\u9fa5]/.test(nextLine);
  152. return isEnglishLine && isChineseLine;
  153. }
  154. // 检测方括号格式的逐字歌词 [00:00.000]文[00:01.000]字
  155. private isSquareBracketWordByWordLyric(line: string): boolean {
  156. return /\[\d{2}:\d{2}\.\d{2,3}\]\S/.test(line);
  157. }
  158. private parseSquareBracketWordLine(
  159. line: string,
  160. offset: number,
  161. nextLine?: string
  162. ): { timeline: number, words: LyricWord[], translation?: string } {
  163. const words: LyricWord[] = [];
  164. let firstTimeline = -1;
  165. // 解析逐字歌词(原有逻辑)
  166. const regex = /\[(\d{2}:\d{2}[.:]\d{2,5})\]([^\[]*)/g;
  167. let match;
  168. while ((match = regex.exec(line)) !== null) {
  169. const timeStr = match[1];
  170. const word = match[2].trim();
  171. if (!word) continue;
  172. const timeline = this.parseTimeline(timeStr) - offset;
  173. if (firstTimeline < 0) firstTimeline = timeline;
  174. words.push(new LyricWord(word, timeline, 0));
  175. }
  176. // 计算词持续时间
  177. for (let i = 0; i < words.length - 1; i++) {
  178. words[i].duration = words[i + 1].startTime - words[i].startTime;
  179. }
  180. if (words.length > 0 && words[words.length - 1].duration === 0) {
  181. words[words.length - 1].duration = 200;
  182. }
  183. // 双语支持
  184. if (nextLine && /^\[\d{2}:\d{2}\.\d{2,3}\]/.test(nextLine)) {
  185. // 提取两行时间戳(需完全一致)
  186. const currentLineTime = line.match(/^\[(\d{2}:\d{2}\.\d{2,3})\]/)?.[1];
  187. const nextLineTime = nextLine.match(/^\[(\d{2}:\d{2}\.\d{2,3})\]/)?.[1];
  188. if (currentLineTime && nextLineTime && currentLineTime === nextLineTime) {
  189. const chineseText = nextLine.replace(/^\[\d{2}:\d{2}\.\d{2,3}\]/, '').trim();
  190. if (chineseText) { // 非空文本才作为翻译
  191. return {
  192. timeline: firstTimeline,
  193. words,
  194. translation: chineseText
  195. };
  196. }
  197. }
  198. }
  199. return { timeline: firstTimeline, words };
  200. }
  201. /******************** 时间解析增强 ********************/
  202. private parseTimeline2(timeString: string): number {
  203. // 增强支持毫秒/厘秒解析
  204. const parts = timeString.split(':');
  205. const minutes = parseInt(parts[0], 10);
  206. const secondParts = parts[1].split('.');
  207. const seconds = parseInt(secondParts[0], 10);
  208. const fraction = parseInt(secondParts[1], 10);
  209. // 根据小数位长度判断时间精度
  210. const milliseconds = secondParts[1].length === 2 ?
  211. fraction * 10 : // 厘秒转毫秒 (01 -> 10ms)
  212. fraction; // 毫秒直接使用
  213. return minutes * 60000 + seconds * 1000 + milliseconds;
  214. }
  215. // 判断是否是逐字歌词行
  216. private isWordByWordLyric(line: string): boolean {
  217. const hasBracketTimestamp = /$$\d{2}:\d{2}\.\d{2,3}$$\S/.test(line);
  218. const hasAngleTimestamp = /<\d{2}:\d{2}\.\d{2,3}>/.test(line);
  219. return hasBracketTimestamp || hasAngleTimestamp;
  220. }
  221. // 解析逐字歌词行
  222. private parseWordByWordLine(line: string, offset: number): { timeline: number, words: LyricWord[] } {
  223. const words: LyricWord[] = [];
  224. let firstTimeline = -1;
  225. const regex = /((?:<|$$)(\d{2}:\d{2}\.\d{2,3})(?:>|$$))([^<\[]*)/g;
  226. let match;
  227. while ((match = regex.exec(line)) !== null) {
  228. const [_, tag, timeStr, word] = match;
  229. if (word.trim() === '') continue;
  230. const timeline = this.parseTimeline(timeStr) - offset;
  231. if (firstTimeline < 0) firstTimeline = timeline;
  232. words.push(new LyricWord(word.trim(), timeline, 0));
  233. }
  234. // 设置每个词持续时间(下一个词开始时间-当前词开始时间)
  235. for (let i = 0; i < words.length - 1; i++) {
  236. words[i].duration = words[i + 1].startTime - words[i].startTime;
  237. }
  238. if (words.length > 0 && words[words.length - 1].duration === 0) {
  239. // 最后一个词持续200ms
  240. words[words.length - 1].duration = 200;
  241. }
  242. return { timeline: firstTimeline, words };
  243. }
  244. // 为逐字歌词拼接整行文本
  245. private populateTextForWordLyrics(lyricLines: LyricLine[]) {
  246. lyricLines.forEach(line => {
  247. if (line.words.length > 0) {
  248. line.text = line.words.map(word => word.word).join('');
  249. }
  250. });
  251. }
  252. private parseIdTag(line: string): string {
  253. let spr = line.split(":")
  254. let spr1 = spr[1]
  255. let result = spr1.replace("]", "")
  256. return result
  257. }
  258. //
  259. // private parseTimeline(timeString: string): number {
  260. // // 00:00.50
  261. // let timeStringList = timeString.split(':')
  262. // let minuteString = timeStringList[0] //00
  263. // let minute = Number.parseInt(minuteString)
  264. // let secondStrings = timeStringList[1] //00.50
  265. // let secondStringList = secondStrings.split(".")
  266. // let secondString = secondStringList[0] //00
  267. // let millionSecondString = secondStringList[1] //50
  268. // let seconds = Number.parseInt(secondString)
  269. // let millionSecond = Number.parseInt(millionSecondString)
  270. // return minute * 60000 + seconds * 1000 + millionSecond // covert to million seconds
  271. // }
  272. private parseTimeline(timeString: string): number {
  273. // 支持多种分隔符:`.` 或 `:`
  274. const parts = timeString.split(/[:.]/);
  275. if (parts.length < 2) {
  276. // printW(`Invalid timeline format: ${timeString}`);
  277. return 0;
  278. }
  279. // 解析分钟、秒、毫秒
  280. const minutes = parseInt(parts[0], 10) || 0;
  281. const seconds = parseInt(parts[1], 10) || 0;
  282. let milliseconds = 0;
  283. // 处理毫秒部分(可能为厘秒或毫秒)
  284. if (parts.length > 2) {
  285. const fraction = parts[2];
  286. // 根据小数位长度判断精度:2位为厘秒(需*10),3位以上为毫秒
  287. milliseconds = fraction.length <= 2 ?
  288. parseInt(fraction, 10) * 10 : // 厘秒转毫秒(如 "54" -> 540ms)
  289. parseInt(fraction.substring(0, 3), 10); // 截取前3位(如 "54001" -> 540ms)
  290. }
  291. return minutes * 60000 + seconds * 1000 + milliseconds;
  292. }
  293. /**
  294. * 解析纯文本歌词(没有时间标签的歌词)
  295. * @param src 歌词行数组
  296. * @returns Lyric 对象
  297. */
  298. private parsePlainTextLyric(src: Array<string>): Lyric {
  299. let lyricLines = new Array<LyricLine>()
  300. let title = ""
  301. let artist = ""
  302. let album = ""
  303. let by = ""
  304. let offset = 0
  305. for (let i = 0; i < src.length; i++) {
  306. let line = src[i].trim()
  307. // 跳过空行
  308. if (line.length === 0 || line === "\n" || line === "\r" || line === "\r\n") {
  309. continue
  310. }
  311. // 跳过元数据行(以 [ti:、[ar:、[al:、[by:、[offset: 等开头)
  312. if (line.startsWith('[ti:') || line.startsWith('[ar:') || line.startsWith('[al:') ||
  313. line.startsWith('[by:') || line.startsWith('[offset:') || line.startsWith('[length:') ||
  314. line.startsWith('[id:') || line.startsWith('[hash:') || line.startsWith('[sign:')) {
  315. // 解析元数据
  316. if (line.startsWith("[ti:")) {
  317. title = this.parseIdTag(line);
  318. } else if (line.startsWith("[ar:")) {
  319. artist = this.parseIdTag(line);
  320. } else if (line.startsWith("[al:")) {
  321. album = this.parseIdTag(line);
  322. } else if (line.startsWith("[by:")) {
  323. by = this.parseIdTag(line);
  324. } else if (line.startsWith("[offset:")) {
  325. offset = Number.parseInt(this.parseIdTag(line));
  326. }
  327. continue
  328. }
  329. // 跳过标签行(如 [Verse]、[Chorus] 等)
  330. if (line === "[Verse]" || line === "[Chorus]" || line === "[PreChorus]" ||
  331. line === "[Bridge]" || line === "[Intro]" || line === "[Outro]") {
  332. continue
  333. }
  334. // 跳过其他以 [ 开头但不是时间标签的行
  335. if (line.startsWith('[') && !/^\[\d{2}:\d{2}\.\d{2,3}\]/.test(line)) {
  336. continue
  337. }
  338. // 不添加时间标签,使用 -1 表示纯文本歌词
  339. lyricLines.push(new LyricLine(line, -1, -1))
  340. }
  341. // printD(`纯文本歌词解析完成,共 ${lyricLines.length} 行歌词`)
  342. // 标记为纯文本歌词
  343. let result = new Lyric(artist, title, album, by, offset, lyricLines, true)
  344. return result
  345. }
  346. }