LRC歌词解析,实现Linux设备播放音乐显示歌词 LRC解析

您所在的位置:网站首页 lilac歌词解析 LRC歌词解析,实现Linux设备播放音乐显示歌词 LRC解析

LRC歌词解析,实现Linux设备播放音乐显示歌词 LRC解析

2024-07-13 04:44| 来源: 网络整理| 查看: 265

开始正文~~~

1.关于LRC

       lrc是英文lyric(歌词)的缩写,被用做歌词文件的扩展名。以lrc为扩展名的歌词文件可以在各类数码播放器中同步显示。LRC 歌词是一种包含着“*:*”形式的“标签(tag)”的、基于纯文本的歌词专用格式。最早由郭祥祥先生(Djohan)提出并在其程序中得到应用。这种歌词文件既可以用来实现卡拉OK功能(需要专门程序),又能以普通的文字处理软件查看、编辑。当然,实际操作时通常是用专门的LRC歌词编辑软件进行高效编辑的。

1.1 格式

      1、标准格式: [分钟:秒.毫秒] 歌词

      注释:(如右图所示)中括号、冒号、点号全都要求英文输入状态;

      2、其他格式①:[分钟:秒] 歌词;

      3、其他格式②:[分钟:秒:毫秒] 歌词,与标准格式相比,秒后边的点号被改成了冒号

 1.2 标签    

  lrc歌词文本中含有两类标签:

    1.2.1标识标签(ID-tags)

    其格式为"[标识名:值]"。大小写等价。以下是预定义的标签。

    [ar:艺人名]

    [ti:曲名]

    [al:专辑名]

    [by:编者(指编辑LRC歌词的人)]

    [offset:时间补偿值] 其单位是毫秒,正值表示整体提前,负值相反。这是用于总体调整显示快慢的。

     1.2.2 时间标签(Time-tag)

    形式为"[mm:ss]"(分钟数:秒数)或"[mm:ss.ff]"。数字须为非负整数, 比如"[12:34.50]"是有效的,而"[0x0C:-34.50]"无效(但也有不太规范的歌词采用[00:-0.12]的方式表示负值以显示歌曲名,部分播放器是支持的)。 它可以位于某行歌词中的任意位置。一行歌词可以包含多个时间标签(比如歌词中的迭句部分)。根据这些时间标签,用户端程序会按顺序依次高亮显示歌词,从而实现卡拉OK功能。另外,标签无须排序。

    对于时间标签有两种形式

第一种比较简单,一个时间tag带一个歌词行

[00:00.50]蔡健雅 - 依赖

[00:07.94]词、曲:蔡健雅、陶晶莹

[00:11.60]关了灯把房间整理好

[00:15.48]凌晨三点还是睡不著

[00:19.64]你应该是不在 所以把电话挂掉

[00:30.39]在黑暗手表跟着心跳

第二种就是多个时间tag,带一个歌词行

[00:15.76]编曲:林迈可 [00:20.00][01:59.75] [02:01.18][00:21.76]狼牙月 伊人憔悴 [02:04.89][00:25.80]我举杯 饮尽了风雪 [02:10.98][00:31.73]是谁打翻前世柜 惹尘埃是非 [02:17.56][00:38.20]缘字诀 几番轮回 [02:21.66][00:42.35]你锁眉 哭红颜唤不回 [02:27.54][00:48.28]纵然青史已经成灰 我爱不灭 [02:34.89][00:55.60]繁华如三千东流水 [02:39.29][00:59.98]我只取一瓢爱了解 [02:43.39][01:04.33]只恋你化身的蝶; [02:47.60][01:08.82][03:38.00][03:22.97][01:44.00] [03:39.06][03:24.50][02:49.36][01:44.75][01:09.96]你发如雪 凄美了离别 [03:42.69][03:26.17][02:53.40][01:46.81][01:14.10]我焚香感动了谁 [03:47.09][03:28.08][02:58.33][01:48.54][01:18.91]邀明月 让回忆皎洁

并且时间标签是无序的,对于这种形式,要是一般的解析可能就会异常。

2.解析LRC

       既然要实现解析LRC歌词,就要完美支持各种情况,实现正确解析。考虑了一下,对于linux C的开发,核心就用链表来实现。核心思想就是:在开始播放的时候,解析歌词,将歌词的时间tag按照升序的形式以链表组织起来。取歌词的时候,以当前播放时间刻度查找链表,同时还要考虑快进、快退这种跳转的查找!

       现在我以代码从内向外的形式解析LRC歌词。

2.1 歌词结构体 /* 歌词结构体 */ typedef struct lyric{ struct lyric *next,*prev; long timescale; char *lyrictext; }lyric;

next和prev分别指向前一行和后一行歌词。timescale就是时间标签换算成整数形式,以此为基准进行排序,lyrictext就是指向需要显示的具体歌词内容。

简单画个图:

使用链表的好处就是在解析一行有多个时间tag的时候,获取时间值,按照顺序插入对应位置,歌词文本都指向同一个地方。

2.2 链表代码

既然用到链表,就必然用到链表建立、遍历、插入和删除。代码如下:

/* 创建新的歌词链表 */ lyric * lyricCreateItem(void) { lyric * node = (lyric *)malloc(sizeof(lyric)); if(node != NULL) { memset(node,0,sizeof(lyric)); } return node; } /* 插入链表 时间刻度按照升序排列 */ lyric * lyricInsertItem(lyric *prev,lyric * newItem) { lyric *node,*prevnode; if(prev == NULL) { return newItem;//表头 } if(newItem == NULL) { return prev; } if(newItem->timescale >= prev->timescale) { prev->next = newItem; newItem->prev = prev; return newItem; } else { prevnode = prev; node = prev->prev; while(node != NULL && node->timescale > newItem->timescale) { prevnode = node; node = node->prev; } if(node != NULL ) { newItem->next = node->next; node->next->prev = newItem; node->next = newItem; newItem->prev = node; } else { newItem->next = prevnode; prevnode->prev = newItem; } return prev; } } /* 查找下一组节点 */ lyric * lyricFindItem(long timeScale,lyric *item) { lyric *nextnode,*prevnode,*node=item; if(node == NULL) { return NULL; } if(timeScale >= node->timescale) { /* 正常播放 or 快进 */ nextnode = node->next; while(nextnode != NULL && timeScale >= nextnode->timescale) { node = nextnode; nextnode = nextnode->next; } return node; } else { /* 快退 */ prevnode = node->prev; while(prevnode != NULL && timeScale < prevnode->timescale) { prevnode = prevnode->prev; } return prevnode; } } /* 获取歌词 */ bool lyricGetItemLyric(lyric *item,char *lastLyric,char *currentLyric,char *nextLyric) { lyric *node = item; bool value = false; if(node == NULL) { return value; } if(currentLyric != NULL && node->lyrictext != NULL) { strcpy(currentLyric,node->lyrictext); value = true; } if(lastLyric != NULL) { node = item->prev; if(node != NULL && node->lyrictext != NULL) { strcpy(lastLyric,node->lyrictext); } else { *lastLyric = '\0'; } value = true; } if(nextLyric != NULL) { node = item->next; if(node != NULL && node->lyrictext != NULL) { strcpy(nextLyric,node->lyrictext); } else { *nextLyric = '\0'; } value = true; } return value; } lyric * lyricDeleteItem(lyric * item) { lyric *nextnode,*node = item; while(node != NULL) { nextnode = node->next; free(node); node = nextnode; } return item; }

lyricInsertItem代码作为插入,新的歌词单元时间tag如果大于等于前一个,则直接放在前一个后面,并把当前新的歌词单元作为下一次的插入的prev;

lyricFindItem查找链表,timeScale是当前播放时间刻度,item是当前记录的歌词单元,按照时间刻度进行对比:

1.若播放进度大于等于当前进度,则查找是否大于下一个歌词单元,如果是,则将下一个歌词单元输出,如果是快进,则可能是连续跳转好几个歌词单元,直到在小于下一个歌词单元停下。

2.若播放进度小于当前进度,表明是快退,进行时间刻度对比,一直到跳转到小于前一个歌词单元停止。

lyricGetItemLyric:item是获取的当前单元,因为需求需要显示当前歌词,前一个歌词,后一个歌词。所以通过当前获取的歌词单元,分别获取。

2.3 LRC解析

下面进行LRC解析:

/* 解析LRC格式歌词,获取时间刻度 */ long getTimeScaleForLRC(char *time) { int minute,second,millisecond; if(time == NULL) { return -5; } minute = second = millisecond = 0; do{ //----------------------------分 while(isdigit(*time)) { minute = minute*10 + ((*time++) - '0'); } if((*time++) == '\0') { break; } //----------------------------秒 while(isdigit(*time)) { second = second*10 + ((*time++) - '0'); } if((*time++) == '\0') { break; } //----------------------------毫秒 while(isdigit(*time)) { millisecond = millisecond*10 + ((*time++) - '0'); } }while(0); millisecond = millisecond < 100?millisecond*10:millisecond; return (minute*60+second)*1000+millisecond; } /* 解析一行LRC格式歌词 */ int praseLyricLineForLRC(char *lyricTextLine,int *offset,long *timeScale,int MaxtimeScale,char **outLyricText) { char *p1,*p2,*p3,*text,*out; int argc = 0; if(lyricTextLine == NULL || timeScale == NULL) { return -1; } /* 解析一行歌词所有的时间刻度 */ text = lyricTextLine; while(argc < MaxtimeScale) { p1 = strchr(text, '['); if(p1 == NULL) { break; } p3 = strchr(p1, ':'); if(p3 == NULL) { break; } p2 = strchr(p3, ']'); if(p2 == NULL) { break; } p1++; *p2 = '\0'; if(isdigit(*p1)) { *(timeScale+(argc++)) = getTimeScaleForLRC(p1); } else if(strstr(p1,"offset") != NULL) { p1 = strchr(p1,':'); if(p1 != NULL && offset != NULL) { text = p1+1; *p2 = '\0'; *offset = atoi(text); } } else { text = text;//解决[01:22:21][歌词] break; } text = p2+1; } if(outLyricText != NULL) { *outLyricText = text; } return argc; } /* 解析LRC格式歌词 lyricBuf 读取的整个歌词缓存 */ int praseLyricForLRC(char *lyricBuf,lyric **node,int *offset,int maxLyricTextLen) { char *lyricLineStart,*lyricLineEnd,*lyricText; lyric *prev,*item; int i,argc,line,lyriclen; long timeScale[20]; if(lyricBuf == NULL) { return -1; } item = lyricCreateItem(); if(item == NULL) { return -2; } item->timescale = 0; item->lyrictext = NULL; prev = lyricInsertItem(NULL,item); *node = item; lyricLineStart = lyricBuf; line = 1; while((lyricLineEnd = strchr(lyricLineStart,'\n')) != NULL) { *lyricLineEnd = '\0'; /* 解析一行歌词 */ argc = praseLyricLineForLRC(lyricLineStart,offset,timeScale,sizeof(timeScale)/sizeof(long),&lyricText); for(i = 0;i < argc ; i++) { if(lyricText == NULL || (lyriclen = strlen(lyricText)) == 0) { break; } if(timeScale[i] < 0) { continue; } /* 保证歌词后面copy不溢出 */ if(lyriclen > maxLyricTextLen) { lyricText[maxLyricTextLen] = '\0'; } item = lyricCreateItem(); if(item == NULL) { continue; } item->timescale = timeScale[i]; item->lyrictext = lyricText; prev = lyricInsertItem(prev,item); line++; } lyricLineStart = lyricLineEnd+1; } return line; }

praseLyricForLRC:lyricBuf是读取的整个歌词文件存放的缓存,node是输出的链表第一个单元,offset是标识标签中的[offset:时间补偿值] ,这里LRC解析,不对“标识标签”其他单元做解析,直接忽略,但需要对[offset]进行解析,进行进度的调整,maxLyricTextLen是允许最大显示歌词的长度,防止内存溢出。

因为LRC歌词形式是一行一行的,先读取一行,对其解析,从中获取时间刻度,显示歌词指针,offset。然后创建歌词链表,在进行插入。

praseLyricLineForLRC就是对其中一行进行解析;

getTimeScaleForLRC是获取时间刻度,并换算成整型,单位ms。

这里整个LRC解析就完毕了。

下面进行歌词文件的处理。

2.4 歌词文件的处理 /* 读取文件大小 */ long get_file_size(const char *path) { struct stat statbuff; if(stat(path, &statbuff) < 0) { return -1; } else { return statbuff.st_size; } } /* 读歌词文件 */ bool readLyricFile(char** lyricFileBuf,char *lyricFile) { long file_size = 0; char *lyricBuffer = NULL; FILE *fp; int ret; if(lyricFileBuf == NULL || lyricFile == NULL) { return false; } /* 对应歌词文件是否存在 */ if(access(lyricFile,F_OK) != 0) { return false; } /* 获取文件长度 */ file_size = get_file_size(lyricFile); if(file_size next;这行语句是因为在praseLyricForLRC函数中创建了一个时间刻度为0的初始链表头,主要是为了获取固定的链表头,便于在freeLyric中从第一个链表开始释放内存,防止内存泄漏。所以loadLyric输出也是第一个链表头。代码示例:

/* 显示歌词示例 */ void displayLyric(void) { lyric* node = loadLyric(); while(1) { if(getLyric()) { //显示歌词 } //其他业务逻辑 } /* 释放歌词缓存 */ freeLyric(node); }

在音乐播放开始时,加载歌词loadLyric,如果加载歌词失败,说明没有歌词,显示固定提示内容。每一段时间间隔 getLyric,我这里是每100ms一次。可以满足需求,要求高的话,可是间隔短点,播放结束freeLyric。

这里就完成整个歌词的解析。

3 显示效果

如上已经成功可以显示歌词了

手机拍的失真比较严重,上传一张UED图。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

4 兼容非标准格式歌词

在实际使用的时候,出现一些非标准的格式歌词:

[ti:真的爱你] [ar:BEYOND] [al:275957] [by:] [offset:0] [00:00.00]真的爱你 (Live) - BEYOND [00:08.56]词:梁美薇 [00:17.13]曲:黄家驹 [00:25.69]无法可修饰的一对手 [00:29.39]带出温暖永远在背后 [00:32.55]纵使啰嗦始终关注 [00:34.98]不懂珍惜太内疚 [00:37.93] [00:38.79]沉醉于音阶她不赞赏 [00:42.14]母亲的爱却永远未退让 [00:45.54]决心冲开心中挣扎 [00:48.14]亲恩终可报答 [00:50.94] [00:51.74]春风化雨暖透我的心 [00:54.94]一生眷顾无言地送赠 [00:59.34] [00:59.99]是你多么温馨的目光 [01:03.25]教我坚毅望着前路 [01:06.45]叮嘱我跌倒不应放弃 [01:11.85] [01:12.40]没法解释怎可报尽亲恩 [01:15.95]爱意宽大是无限 [01:19.00]请准我说声真的爱你 [01:24.50] [01:37.66]无法可修饰的一对手 [01:41.46]带出温暖永远在背后 [01:44.46]纵使啰嗦始终关注 [01:46.99]不懂珍惜太内疚 [01:49.95] [01:50.50]仍记起温馨的一对手 [01:54.10]始终给我照顾未变样 [01:57.36]理想今天终于等到 [01:59.80]分享光辉盼做到 [02:02.65] [02:03.25]春风化雨暖透我的心 [02:06.55]一生眷顾无言地送赠 [02:10.75] [02:11.35]是你多么温馨的目光 [02:14.85]教我坚毅望着前路 [02:17.95]叮嘱我跌倒不应放弃 [02:23.90]没法解释怎可报尽亲恩 [02:27.40]爱意宽大是无限 [02:30.56]请准我说声真的爱你 [02:36.01] [03:01.73]春风化雨暖透我的心 [03:04.68]一生眷顾无言地送赠 [03:08.98] [03:09.58]是你多么温馨的目光 [03:12.88]教我坚毅望着前路 [03:16.04]叮嘱我跌倒不应放弃 [03:21.39] [03:22.04]没法解释怎可报尽亲恩 [03:25.44]爱意宽大是无限 [03:28.59]请准我说声真的爱你 [03:34.15] [03:34.70]是你多么温馨的目光 [03:38.05]教我坚毅望着前路 [03:41.25]叮嘱我跌倒不应放弃 [03:47.15]没法解释怎可报尽亲恩 [03:50.76]爱意宽大是无限 [03:53.76]请准我说声真的爱你#

标准歌词格式都是用分行符标识一行歌词,在解析歌词的时候,已分行符'\n'截取一行歌词,然后解析。如上歌词出现了没有分行符,一整段都是一行,导致解析错误,显示错误信息!

这个非标准歌词有个特点,就是用空格表示一行歌词(上面歌词中的空格可能不怎么明显,但的确存在)。

既然做了歌词解析,那么就要尽可能的兼容异常的情况出现。所以决定对上述歌词进行兼容。

思路:

以空格表示一行歌词,但可能存在歌词中就存在空格的情况:“真的爱你 (Live) - BEYOND”,但一行歌词结束是空格后面紧接着'[',所以以这两个字符为标识符进行解析。

之前的代码以'\n'为一行歌词:(lyricLineEnd = strchr(lyricLineStart,'\n')) != NULL,对这段代码进行改造:

/* 发现一行歌词歌曲行 已'\n'表示一行,同时为了兼容非标准格式歌词,已空格后面紧跟[,标识一个一行 */ char* findLyricLine(char *lyric,int lyriclen) { char ch; int i = 0; if(lyric == NULL) { return NULL; } lyriclen = lyriclen - 1;//在获取歌词缓存的时候多补了一个'\n' while(i++ < lyriclen)//防止没有如下特殊字符出现,一整行歌词带入解析 { ch = *lyric; if(ch == '\0') { return NULL; } else if(ch == '\n') { return lyric; } else if(ch == ' ' && (*(lyric+1) == '[')) { return lyric; } lyric++; } return NULL; }

praseLyricForLRC函数中:

...

totallyriclen = strlen(lyricBuf);

while((lyricLineEnd = findLyricLine(lyricLineStart,totallyriclen)) != NULL )  {

...

其余保持不变。

这样就可以完美兼容非标准格式歌词了。

留个疑问:

为啥findLyricLine函数代码参数要带入歌词总长,while(i++ < lyriclen) 这句语句又是干嘛呢?



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3