Skip to content

LRC歌词格式和编解码实现

前言

  • 音乐软件在播放歌曲时,能同步显示歌词,一般都是引用了对应的歌词文件,而歌词文件主要要存储的就是歌词内容,以及每一句歌词、每一个歌词文字的播放时间。由此引申出的歌词格式就很多了,有LRC、SRT、SSA等,其中LRC是最为流行和广泛使用的格式。

  • LRC是一般认为是逐行歌词。

LRC格式

  • 尽管LRC歌词有标准规定,但它在网络上流传的非标准格式也非常多,所以,如果你希望做一个兼容性足够高的LRC解析器,很有必要考虑兼容常见的非标准格式做好一些容错。

  • 下面我们先来介绍一下它的格式:

标识标签(ID-tags)

  • 格式为 [标识名称:值]。大小写等价。

  • 标准预定义了一些标签:

    • [ar:艺术家];示例:[ar:coolight]

    • [ti:歌曲名称];示例:[ti:hello world]

    • [al:专辑名称];示例:[al:hello]

    • [by:制作LRC歌词的作者];示例:[by:coolight]

    • [offset:[+/-]时间补偿值] 其单位是毫秒(1秒=1000毫秒),正值表示整体提前,负值相反。这是用于总体调整显示快慢。示例:[offset:0], [offset:100], [offset:+100], [offset:-200]

  • 当然你也可以自定义标签,一般自定义标签的名称不应当由数字开头

  • 标签之后的内容需要忽略,比如:

    • [ti:hello world] aaabbbccc

    • 标签是[ti:hello world]

    • 之后的内容是aaabbbccc,这部分应对忽略掉

  • 一般是一个标签占一行,但也需要考虑兼容一行多标签

  • 注意兼容允许内部的空格,比如:[ offset : +200 ] 内容

时间标签(Time-tag)

  • 这个比较复杂,非标准格式也是多种多样

  • 注意1秒=1000毫秒

  • 下面我们用m代表分钟,s代表秒,f代表毫秒,常见格式有:

    • [mm:ss.ff],即[分钟:秒.(毫秒/10)],分钟和秒之间用冒号:隔开,而秒和毫秒之间用点.隔开,毫秒部分是真实毫秒值 / 10得到的,因此它是一个二位数。

    • [mm:ss:ff],同上,差别在于秒和毫秒之间用冒号:隔开而不是点.

    • [mm:ss.fff],同上,差别在于毫秒部分不需要除以10,是真实的毫秒值,因此有3位

    • [mm:ss:fff],同上,差别在于秒和毫秒之间用冒号:隔开而不是点.

    • [mm:ss],这个格式没有了毫秒部分

  • 一般来讲,分钟、秒、毫秒都会是正整数,但应当考虑兼容匹配负数,如果出现负数,则可以考虑将负数置零或者忽略这一行歌词不予显示

  • 时间标签是可以出现一行多时间的,比如:

    • [01:10][01:20][01:50]hello

    • 这应对解析为当播放到 [01:10]、[01:20]、[01:50]这三个时间点时,显示hello这行歌词

  • 注意时间标签的内容可能为空格行或者没有内容,即:

    • [01:10]

    • [01:10]

  • 注意最后自己排一下序,歌词文件内的不一定是排好序的,应对自己按照时间重新排序。

翻译歌词

  • LRC中表示翻译歌词,常见的有两种格式:

    • 原文歌词有时间标签,翻译歌词没有时间标签:

    • 原文歌词:[01:10]hello world

    • 翻译歌词:你好 世界

    • 原文歌词和翻译歌词的时间完全相同:

    • 原文歌词:[01:10]hello world

    • 翻译歌词:[01:10]你好 世界

  • 显示时,当出现以上两种情况应对视为翻译歌词,播放到时,两句歌词同时亮起

  • 重排序时主要考虑无时间戳的翻译歌词仍应跟随原文歌词,对于时间相同的歌词,应对保持他们原来的顺序,即使用稳定排序,如二路归并。

解码实现

  • 首先将LRC歌词文本按行分割成一个字符串数组,注意分割符号应当使用 \n 和 \r,这是因为不同操作系统的换行符不同,有些是 \n,有些是 \r\n。

  • 遍历字符串数组,将字符串行的左右空白符移除,如果移除完后,这个字符串是空字符串,则丢弃这一行。

  • 开始逐行解码,可使用正则表达式匹配标识标签和时间标签:

    • 标识标签:

    • RegExp(r"^[\s\S][\s([^\d])\s\😦[\s\S])][\s\S]$")

    • 最基本的部分是 [([^\d])\😦\s\S)],用于匹配 [{key}:{value}]

    • 然后添加一些空白符匹配,增加容错率即可

  • 时间标签:

    • 这需要分两次匹配,因为一行可能包含多个时间

    • RegExp(r"^[\s\S]?(([[+-]?\d+\:[+-]?\d+([.:][+-]?\d+)?]\s)+)([\s\S]*)$"),先检查这一行内是否有时间标签,如果有则进行下一步。

    • RegExp(r"([([+-]?\d+)\😦[+-]?\d+)(.:)?]){1}?")可以将每一个时间提取出来。

    • 上面这两个正则表达式支持的时间格式:

      • ① [mm:ss.ff]

      • ② [mm:ss]

      • ③ [+-mm:+-ss.+-ff]

    • 如果时间有出现负值,需要将整个时间置零。

    • 计算毫秒部分ff的真实值:

      • ① 如果ff部分是两位数,则 ff=毫秒/10,即真实毫秒值为 ff * 10,转成秒为 ff * 10 / 1000 = ff / 100

      • ② 如果ff部分是三位数,则 ff=毫秒,转成秒为 ff / 1000

    • 对于没有标签的行,应当作为无时间的翻译歌词处理,单行解析时作为-1时间的歌词返回即可,最终排序时作为翻译歌词判断依据。

  • 解析完每一行歌词后,将歌词行使用稳定排序算法,如二路归并进行重排序,但在排序之前,需要将歌词时间重新赋值,这是因为部分翻译歌词没有时间,直接排序会导致错乱,因此需要给这些歌词赋值为上一行歌词的时间,让翻译歌词和原文歌词时间一致。然后进行重排序。

编码实现

  • 编码要简单一些,主要是歌词时间需要规范化,时间不能出现负数,并遵循[mm:ss.ff]格式,其中ff是 真实毫秒值/10 得到的,因此它只有两位数。

  • 注意ff要补足前导零,比如ff值只有7,需要补为 07,否则可能会被认为ff是 真实毫秒值/100。

  • 分钟和秒数的前导零没那么重要,但最好也是不足2位则补足前导零。

注意事项

  • LRC本质仍然是文本,自然就会有编码问题,一般是UTF-8,当然也有ANSI。

  • 一般的歌曲文件,如flac、mp3等都是可以内嵌歌词的,也可以外置一个和歌曲文件同目录、同名、仅文件后缀为lrc的歌词文件。

Dart-LRC编解码器开源实现