LRC格式歌詞解析,實現Linux設備播放音樂顯示歌詞

       最近項目需要播放音樂顯示歌詞滾動,歌詞格式主要是LRC,完成項目之餘,這裏做了一下總結。

首先打個廣告:我最近開發的產品:蘇寧小Biu智能音箱鬧鐘。

是一款帶屏幕的智能音箱,主要實現鬧鐘功能,可以和小biu鬧鐘實現互動,類似於小米的小愛音箱,阿里的天貓精靈。

感興趣的同學可以買一臺哈。購買鏈接:

https://product.suning.com/0000000000/10729185289.html?safp=d488778a.13701.productWrap.3&safc=prd.3.ssdsn_pic01-1_jz#pro_detail_tab

開始正文~~~

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,*text,*out;
	int argc = 0;

	if(lyricTextLine == NULL || timeScale == NULL)
	{
		return -1;
	}

	/* 解析一行歌詞所有的時間刻度 */
	text = lyricTextLine;
	while(argc < MaxtimeScale)
	{
		p1 = strchr(text, '[');
		if(p1 == NULL)
		{
			break;
		}

		p2 = strchr(text, ']');
		if(p2 == NULL)
		{
			break;
		}

		p1++;
		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;
	}

	ui_printf("line=%d",line);

	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);

	*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,如果加載歌詞失敗,說明沒有歌詞,顯示固定提示內容。每一段時間間隔 getLyric,我這裏是每100ms一次。可以滿足需求,要求高的話,可是間隔短點,播放結束freeLyric。

這裏就完成整個歌詞的解析。

3 顯示效果

手機這個顯示屏拍攝的效果不是很好,失真很嚴重。可以看出已經實現了需求,完成歌詞解析。

在放一張UED圖,和小Biu鬧鐘效果顯示一樣。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章