最近項目需要播放音樂顯示歌詞滾動,歌詞格式主要是LRC,完成項目之餘,這裏做了一下總結。
首先打個廣告:我最近開發的產品:蘇寧小Biu智能音箱鬧鐘。
是一款帶屏幕的智能音箱,主要實現鬧鐘功能,可以和小biu鬧鐘實現互動,類似於小米的小愛音箱,阿里的天貓精靈。
感興趣的同學可以買一臺哈。購買鏈接:
開始正文~~~
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: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鬧鐘效果顯示一樣。