LyricParser.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import { IParser } from './IParser';
  2. import { Lyric } from '../bean/Lyric';
  3. import { LyricLine } from '../bean/LyricLine';
  4. import { printD, printW } from '../extensions/Extension';
  5. import { LyricWord } from '../bean/LyricWord';
  6. /**
  7. * The parser to parse the string array of a standard lyric file.
  8. */
  9. export class LyricParser implements IParser {
  10. // Example lyric file:
  11. // [ti:画心]
  12. // [ar:张靓颖]
  13. // [al:432326]
  14. // [by:]
  15. // [offset:0]
  16. // [00:00.10]画心 - 张靓颖
  17. // [01:05.49]看不穿 是你失落的魂魄
  18. /**
  19. * Parse the string array to a Lyric.
  20. * @param src The lyric source, a string array.
  21. * @returns A lyric instance.
  22. */
  23. parse(src: Array<string>): Lyric {
  24. let lyricLines = new Array<LyricLine>()
  25. let title = ""
  26. let artist = ""
  27. let album = ""
  28. let by = ""
  29. let offset = 0
  30. const ignoredTags = [ 'hash', 'sign', 'qq', 'total','Outro']; // 定义需要忽略的标签
  31. for (let i = 0; i < src.length; i++) {
  32. let line = src[i]
  33. console.info(`content The line of file:line ${line}`);
  34. if (line == "" || line == "\n" || line == "\r" || line == "\r\n" || line == "[Verse]" || line == "[Chorus]"
  35. || line == "[PreChorus]" || line == "[PreChorus]"||line == "[Bridge]") {
  36. printW("the lyric line is empty, carriage return or line feed, line index= " + i)
  37. continue
  38. }
  39. // 检查是否是需要忽略的标签
  40. const shouldIgnore = ignoredTags.some(tag => line.indexOf(tag) > 0);
  41. if (shouldIgnore) {
  42. printW(`the lyric line contains ignored tag, line index= ${i}`);
  43. continue;
  44. }
  45. if (line.indexOf("ti") > 0) {
  46. title = this.parseIdTag(line)
  47. } else if (line.indexOf("ar") > 0) {
  48. artist = this.parseIdTag(line)
  49. } else if (line.indexOf("al") > 0) {
  50. album = this.parseIdTag(line)
  51. } else if (line.indexOf("by") > 0) {
  52. by = this.parseIdTag(line)
  53. } else if (line.indexOf("offset") > 0) {
  54. offset = Number.parseInt(this.parseIdTag(line))
  55. } else {
  56. // 新增逐字歌词解析逻辑[mm:ss.xx] <mm:ss.xx>
  57. if (this.isWordByWordLyric(line)) {
  58. const { timeline, words } = this.parseWordByWordLine(line, offset);
  59. lyricLines.push(new LyricLine('', timeline, -1, words))
  60. continue;
  61. }
  62. // 新增:逐字歌词[]检测方括号逐字歌词格式 [mm:ss.xxx] 文字
  63. if (this.isSquareBracketWordByWordLyric(line)) {
  64. const { timeline, words } = this.parseSquareBracketWordLine(line, offset);
  65. if (words.length > 0) {
  66. lyricLines.push(new LyricLine('', timeline, -1, words))
  67. }
  68. } else {
  69. // 原逻辑处理,但支持逐字歌词
  70. // [00:00.10]画心 - 张靓颖
  71. // [01:05.49][02:08.40]看不穿 是你失落的魂魄
  72. let spr = line.split(']');
  73. if (spr.length <= 1) {
  74. printW("the lyric line is no timestamp, line index= " + i)
  75. continue
  76. }
  77. // parse text
  78. let text = spr[spr.length-1]
  79. // ... 原来的文本解析逻辑保持不变 ...
  80. for (let i = 0; i < spr.length - 1; i++) {
  81. let timeline = spr[i].replace("[", "");
  82. let timeStamp = this.parseTimeline(timeline);
  83. lyricLines.push(new LyricLine(text, timeStamp - offset, -1));
  84. }
  85. }
  86. }
  87. }
  88. lyricLines.sort((l1, l2) => {
  89. return l1.beginTime - l2.beginTime
  90. })
  91. for (let i = 0;i < lyricLines.length; i++) {
  92. let lyricLine = lyricLines[i]
  93. if (i == lyricLines.length - 1) {
  94. lyricLine.nextTime = lyricLine.beginTime + 1000 - offset
  95. } else {
  96. let next = lyricLines[i+1]
  97. lyricLine.nextTime = next.beginTime
  98. }
  99. }
  100. // 为逐字歌词填充text(拼接所有歌词词)
  101. this.populateTextForWordLyrics(lyricLines);
  102. let result = new Lyric(artist, title, album, by, offset, lyricLines)
  103. return result
  104. }
  105. // 检测方括号格式的逐字歌词 [00:00.000]文[00:01.000]字
  106. private isSquareBracketWordByWordLyric(line: string): boolean {
  107. return /\[\d{2}:\d{2}\.\d{2,3}\]\S/.test(line);
  108. }
  109. // 解析方括号格式的逐字歌词行
  110. private parseSquareBracketWordLine(line: string, offset: number):
  111. { timeline: number, words: LyricWord[] } {
  112. const words: LyricWord[] = [];
  113. let firstTimeline = -1;
  114. // 正则匹配:[00:00.000]中文字
  115. const regex = /\[(\d{2}:\d{2}\.\d{2,3})\]([^\[]*)/g;
  116. let match;
  117. while ((match = regex.exec(line)) !== null) {
  118. const timeStr = match[1]; // 时间部分 00:00.000
  119. const word = match[2].trim(); // 歌词文本
  120. if (!word) continue; // 跳过空词
  121. const timeline = this.parseTimeline2(timeStr) - offset;
  122. if (firstTimeline < 0) firstTimeline = timeline;
  123. words.push(new LyricWord(word, timeline, 0));
  124. }
  125. // 计算每个词的持续时间
  126. for (let i = 0; i < words.length - 1; i++) {
  127. words[i].duration = words[i + 1].startTime - words[i].startTime;
  128. }
  129. if (words.length > 0 && words[words.length - 1].duration === 0) {
  130. words[words.length - 1].duration = 200; // 默认200ms
  131. }
  132. return { timeline: firstTimeline, words };
  133. }
  134. /******************** 时间解析增强 ********************/
  135. private parseTimeline2(timeString: string): number {
  136. // 增强支持毫秒/厘秒解析
  137. const parts = timeString.split(':');
  138. const minutes = parseInt(parts[0], 10);
  139. const secondParts = parts[1].split('.');
  140. const seconds = parseInt(secondParts[0], 10);
  141. const fraction = parseInt(secondParts[1], 10);
  142. // 根据小数位长度判断时间精度
  143. const milliseconds = secondParts[1].length === 2 ?
  144. fraction * 10 : // 厘秒转毫秒 (01 -> 10ms)
  145. fraction; // 毫秒直接使用
  146. return minutes * 60000 + seconds * 1000 + milliseconds;
  147. }
  148. // 判断是否是逐字歌词行
  149. private isWordByWordLyric(line: string): boolean {
  150. const hasBracketTimestamp = /$$\d{2}:\d{2}\.\d{2,3}$$\S/.test(line);
  151. const hasAngleTimestamp = /<\d{2}:\d{2}\.\d{2,3}>/.test(line);
  152. return hasBracketTimestamp || hasAngleTimestamp;
  153. }
  154. // 解析逐字歌词行
  155. private parseWordByWordLine(line: string, offset: number): { timeline: number, words: LyricWord[] } {
  156. const words: LyricWord[] = [];
  157. let firstTimeline = -1;
  158. const regex = /((?:<|$$)(\d{2}:\d{2}\.\d{2,3})(?:>|$$))([^<\[]*)/g;
  159. let match;
  160. while ((match = regex.exec(line)) !== null) {
  161. const [_, tag, timeStr, word] = match;
  162. if (word.trim() === '') continue;
  163. const timeline = this.parseTimeline(timeStr) - offset;
  164. if (firstTimeline < 0) firstTimeline = timeline;
  165. words.push(new LyricWord(word.trim(), timeline, 0));
  166. }
  167. // 设置每个词持续时间(下一个词开始时间-当前词开始时间)
  168. for (let i = 0; i < words.length - 1; i++) {
  169. words[i].duration = words[i + 1].startTime - words[i].startTime;
  170. }
  171. if (words.length > 0 && words[words.length - 1].duration === 0) {
  172. // 最后一个词持续200ms
  173. words[words.length - 1].duration = 200;
  174. }
  175. return { timeline: firstTimeline, words };
  176. }
  177. // 为逐字歌词拼接整行文本
  178. private populateTextForWordLyrics(lyricLines: LyricLine[]) {
  179. lyricLines.forEach(line => {
  180. if (line.words.length > 0) {
  181. line.text = line.words.map(word => word.word).join('');
  182. }
  183. });
  184. }
  185. private parseIdTag(line: string): string {
  186. let spr = line.split(":")
  187. let spr1 = spr[1]
  188. let result = spr1.replace("]", "")
  189. return result
  190. }
  191. private parseTimeline(timeString: string): number {
  192. // 00:00.50
  193. let timeStringList = timeString.split(':')
  194. let minuteString = timeStringList[0] //00
  195. let minute = Number.parseInt(minuteString)
  196. let secondStrings = timeStringList[1] //00.50
  197. let secondStringList = secondStrings.split(".")
  198. let secondString = secondStringList[0] //00
  199. let millionSecondString = secondStringList[1] //50
  200. let seconds = Number.parseInt(secondString)
  201. let millionSecond = Number.parseInt(millionSecondString)
  202. return minute * 60000 + seconds * 1000 + millionSecond // covert to million seconds
  203. }
  204. }