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

开始正文~~~

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 <= 0)
	{
		return false;
	}
	file_size += 2;

	/* 读歌词文件 */
	fp = fopen(lyricFile, "r");
	if(fp == NULL)
	{
		return false;
	}

	lyricBuffer = (char*)malloc(file_size*sizeof(char));
	if(lyricBuffer == NULL)
	{
		fclose(fp);
		return false;
	}
	memset(lyricBuffer,'\n',file_size*sizeof(char));

	ret = fread(lyricBuffer,1,file_size-2,fp);
	if(ret < 0)
	{
		fclose(fp);
		free(lyricBuffer);
		return false;
	}
	fclose(fp);
    lyricBuffer[file_size -1] = '\0';
	*lyricFileBuf = lyricBuffer;
	return true;
}

/*
写歌词文件
*/
bool writeLyricFile(char *lyricbuf,int lyriclen,char *lyricFile)
{
	if(lyricbuf == NULL || lyricFile == NULL || lyriclen <= 0)
	{
		return false;
	}

	FILE *fp = fopen(lyricFile, "w");
	if(fp == NULL)
	{
		return false;
	}

	fwrite(lyricbuf,1,lyriclen,fp);
	fclose(fp);

	return true;
}

readLyricFile是根据歌词文件路径,读取歌词,并申请动态内存。并做了相关判断,多读取失败,返回false,表明当前歌曲不存在歌词。其中,在读取歌词文件内存多存了两个'\n',主要是因为前面 praseLyricForLRC解析按照'\n',而实际歌词往往最后一行不存在换行符,导致最后一行无法解析。所以这里进行预处理。

writeLyricFile是写歌词文件。这里下载歌词时一个单独进程,这个进行获取到歌词文件内容,写入文件中。

2.5 加载、获取、释放歌词缓存

这里需要创建整个歌词文件的控制体:

/*
display lyric struct
*/
typedef struct{
	int   lyricoffset;
	lyric *node;
	char  *lyricFileBuf;
}LyricDisplayStruct;

lyricoffset是时间偏移,node是当前获取的歌词单元,lyricFileBuf是获取的整个歌词缓存,从readLyricFile从获取。

typedef struct{
	char title[200];
	char artst[100];
	char endtimestr[12];
	char currtimestr[12];
	char lyricformat[12];
	char lyricurl[256];
	char lastLyric[256];
	char currentLyric[256];
	char nextLyric[256];
	long endtime;
	long currtime;
	bool ispause;
	double progress;
}MusicInfo;

这里是显示歌词的结构体。包括了相关歌曲需要的参数。

创建两个全局变量:

/*歌词相关信息*/
LyricDisplayStruct lyricDisplayInfo;

MusicInfo g_musicinfo;

const char *lyricfile = "/tmp/lyric/lyricfile";

/*
初始化歌词参数
*/
void initLyric(void)
{
	lyricDisplayInfo.lyricoffset  = 0;
	lyricDisplayInfo.node         = NULL;
	lyricDisplayInfo.lyricFileBuf = NULL;
}

/*
加载歌词
*/
lyric* loadLyric(void)
{
	lyric *node = NULL;

	do{
		/* 初始化歌词参数 */
		lyricDisplayInfo.lyricoffset = 0;
		if(lyricDisplayInfo.node != NULL)
		{
			lyricDeleteItem(lyricDisplayInfo.node);
			lyricDisplayInfo.node = NULL;
		}
		if(lyricDisplayInfo.lyricFileBuf != NULL)
		{
			free(lyricDisplayInfo.lyricFileBuf);
			lyricDisplayInfo.lyricFileBuf = NULL;
		}

		/* 读取歌词文件 */
		pthread_mutex_lock(&onDownLoadLyricMutex);
		bool flag = readLyricFile(&lyricDisplayInfo.lyricFileBuf,lyricfile);
		pthread_mutex_unlock(&onDownLoadLyricMutex);
		if(!flag)
		{
			break;
		}

		/* 解析歌词文件 */
		if(strcmp("LRC",g_musicinfo.lyricformat) == 0)
		{
			praseLyricForLRC(lyricDisplayInfo.lyricFileBuf,&lyricDisplayInfo.node,&lyricDisplayInfo.lyricoffset,sizeof(g_musicinfo.currentLyric)-1);
		}
		else
		{
			break;
		}

		node = lyricDisplayInfo.node;
		if(node == NULL)
		{
			break;
		}

		lyricDisplayInfo.node = lyricDisplayInfo.node->next;

		/* 预加载 */
		lyricGetItemLyric(lyricDisplayInfo.node,g_musicinfo.lastLyric,g_musicinfo.currentLyric,g_musicinfo.nextLyric);

	}while(0);

	if(lyricDisplayInfo.node == NULL)
	{
		strcpy(g_musicinfo.currentLyric,"本节目暂无可显示内容");
	}

	return node;
}

/*
获取歌词
return:
	TRUE:需要刷新歌词
	FALSE:不需要刷新歌词
*/
bool getLyric(void)
{
	lyric *node;
	bool status = false;

	if(lyricDisplayInfo.node == NULL)
	{
		return status;
	}

	node = lyricFindItem(g_musicinfo.currtime+lyricDisplayInfo.lyricoffset,lyricDisplayInfo.node);
	if(node != lyricDisplayInfo.node)
	{
		status = lyricGetItemLyric(node,g_musicinfo.lastLyric,g_musicinfo.currentLyric,g_musicinfo.nextLyric);

		lyricDisplayInfo.node = node;
	}

	return status;
}


/*
释放歌词参数缓存
*/
void freeLyric(lyric* node)
{
	lyricDeleteItem(node);
	lyricDisplayInfo.node = NULL;

	if(lyricDisplayInfo.lyricFileBuf != NULL)
	{
		free(lyricDisplayInfo.lyricFileBuf);
		lyricDisplayInfo.lyricFileBuf = NULL;
	}

	lyricDisplayInfo.lyricoffset = 0;
}

loadLyric函数中 lyricDisplayInfo.node = lyricDisplayInfo.node->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) 这句语句又是干嘛呢?

  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
### 回答1: JavaScript 可以通过以下步骤来解析 LRC 歌词并匹配时间显示在页面: 1. 使用 JavaScript 的 `fetch` 函数或者 XMLHttpRequest 对象获取 LRC 歌词文件的内容。 2. 使用正则表达式或者其他方法将 LRC 歌词文件内容按照行分割成一个数组,每一行对应一句歌词。 3. 对于每一行歌词,使用正则表达式或者其他方法提取出歌词内容和时间标签。 4. 将时间标签转换为可以用来匹配的时间,可以使用 JavaScript 的 `Date` 对象进行转换。 5. 使用 JavaScript 定时器(如 `setInterval` 函数)不断地获取当前时间,并与 LRC 歌词中的时间进行比较。 6. 当当前时间与 LRC 歌词中的时间匹配时,将对应的歌词内容显示在页面上。 例如,下面是一个简单的代码示例,它可以使用 JavaScript 加载 LRC 歌词文件,并解析出时间标签和歌词内容: ``` fetch('lyrics.lrc') .then(response => response.text()) .then(text => { // 将 LRC 歌词文件内容按行分割成数组 const lines = text.split('\n'); // 遍历每一行歌词 for (const line of lines) { // 使用正则表达式提取时间标签和歌词内容 const match = line. ### 回答2: 通过JavaScript解析lrc歌词并匹配时间,我们可以实现歌词同步显示在页面上。首先,我们需要将lrc歌词文件读取为字符串。可以使用XMLHttpRequest对象创建一个HTTP请求,然后使用open()和send()方法来发送请求并获取歌词文件的内容。 当我们获取到歌词文件的内容后,我们需要将其分行解析。可以使用split()方法,根据换行符"\n"将文件内容分割成一行一行的数组。然后,我们遍历数组,对每一行进行解析解析每一行时,我们可以使用正则表达式来提取歌词中的时间和文本信息。正则表达式可以匹配类似"[00:48.666]我曾经跨过山和大海"的歌词格式。我们可以使用exec()方法来获取匹配的结果,并提取时间和文本。 获取到时间和文本后,我们可以将时间转换成毫秒级的时间戳,并保存在一个数组中。同时,我们也保存歌词文本在另一个数组中。 接下来,在页面中创建一个可显示歌词的区域。可以使用HTML中的<div>元素来显示歌词。通过脚本查找到这个<div>元素,并为其设置一个id。然后,使用getElementById()方法获取这个元素的引用。 为了实现同步显示歌词,我们可以使用定时器来不断更新歌词显示。通过setInterval()方法来实现定时器,可以设定一个时间间隔来不断刷新。在每个时间间隔内,我们可以获取当前歌曲播放的时间,并与歌词时间戳进行匹配。 匹配到当前时间对应的歌词后,我们可以将其显示在页面上。通过innerHTML属性设置<div>元素的内容,将匹配到的歌词文本赋值给innerHTML属性即可。 总结起来,通过JavaScript解析lrc歌词文件,并使用定时器来匹配当前播放时间对应的歌词,再通过innerHTML属性将歌词文本显示在页面上,我们就能够实现lrc歌词显示和同步播放的功能。这样,用户在听歌的同时还能够看到当前播放歌词,更加深入地体验音乐的情感。 ### 回答3: JavaScript可以通过解析LRC歌词文件,并将其与当前播放时间进行匹配,然后将匹配到的歌词显示在页面上。 要实现这个功能,我们可以首先将LRC歌词文件读取为一个字符串,然后通过字符串分割和正则表达式来提取每一行的时间和歌词内容。 首先,我们需要将LRC歌词文件读取为一个字符串: ```javascript // 假设LRC歌词文件已经在某个元素中定义了一个“data-lrc”属性 let lrcString = document.getElementById("lrcElement").dataset.lrc; ``` 接下来,我们可以通过字符串的分割和正则表达式来提取每一行的时间和歌词内容: ```javascript // 将歌词按行分割为数组 let lines = lrcString.split('\n'); // 创建一个存储时间和歌词的数组 let lyrics = []; // 遍历每一行歌词 for (let line of lines) { // 匹配时间的正则表达式 let timeRegExp = /\[(\d{2}):(\d{2})\.(\d{2})\]/g; // 匹配所有时间标签 let timeTags = line.match(timeRegExp); // 匹配歌词内容 let text = line.replace(timeRegExp, '').trim(); // 将时间和歌词内容存入数组 for (let timeTag of timeTags) { let time = timeTag.replace('[', '').replace(']', ''); let [minutes, seconds, milliseconds] = time.split(':'); let totalMilliseconds = parseInt(minutes) * 60 * 1000 + parseInt(seconds) * 1000 + parseInt(milliseconds); lyrics.push({ time: totalMilliseconds, text: text }); } } ``` 最后,我们可以使用当前的播放时间与歌词数组进行比较,并将匹配到的歌词显示在页面上: ```javascript // 假设当前播放时间为playTime let playTime = 5000; // 遍历歌词数组,找到与当前播放时间匹配的歌词 for (let i = 0; i < lyrics.length; i++) { if (lyrics[i].time <= playTime && playTime < lyrics[i+1].time) { // 显示匹配到的歌词内容 document.getElementById("lyricsElement").innerText = lyrics[i].text; break; } } ``` 通过以上的步骤,我们能够解析LRC歌词文件并在页面上匹配并显示与当前播放时间相对应的歌词内容。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值