1. 程式人生 > >C++讀取歌詞(lrc)檔案,分解歌詞時間標籤和歌詞文字的方法

C++讀取歌詞(lrc)檔案,分解歌詞時間標籤和歌詞文字的方法

本人最近在寫一個音樂播放器,做了一個顯示歌詞的功能。雖然很多已經有很多人有自己的辦法,在這裡我還是想介紹一下我自己的方法。 讀取歌詞檔案並不困難,因為lrc格式的歌詞本身很有規律,下面為一個lrc檔案的一部分:
[ti:なわとび]
[ar:小泉花陽(CV.久保ユリカ)]
[al:「ラブライブ!」オリジナルソング CD3]
[by:萊特] 
[00:00.42]なわとび
[00:04.29]TVアニメ「ラブライブ!」オリジナルソング CD3
[00:06.40]作詞:畑亜貴
[00:08.43]作曲:rino
[00:10.40]編曲:藤田宜久
[00:12.39]歌:小泉花陽(CV.久保ユリカ)
[00:15.91]
[00:27.66]出會いがわたしを変えたみたい
[00:33.11]なりたい自分をみつけたの
[00:38.82]ずっとずっとあこがれを
[00:45.74]胸の中だけで育ててた
 歌詞檔案中的每一句由中括號組成的時間標籤和歌詞文字組成,其中時間標籤分別用冒號和圓點分隔了分鐘數、秒鐘數和毫秒數。毫秒數可以是1~3位。 另外歌詞檔案中每一行的時間標籤可能不止一個。 下面是讀取歌詞檔案的方法:
  1. 讀取歌詞檔案中和每一行,將每一行歌詞(包括時間標籤)存入一個string容器中。
  2. 依次處理剛剛得到的string容器中的每一個字串。
  3. 查詢字串的最後一個右中括號“]”,將最後一個右中括號後面的字元作為歌詞文字。
  4. 依次從第一個字元開始開始查詢左中括號“[”,將右邊兩個字元作為分鐘數,其右邊第4個字元開始兩個字元作為秒鐘數,第7個字元開始為毫秒數。
  5. 將得到到時間標籤和檔案作為一句歌詞存入容器。
  6. 繼續查詢左中括號“[”,如果找到了則重複4、5步驟,否則處理下一句歌詞。
另外歌詞的處理中還需要處理判斷歌詞編碼為ANSI還是UTF8的問題。 我寫了一個歌詞Lrycs類,下面是類的宣告:
#pragma once
#include<string>
#include<vector>
#include<fstream>
#include<iostream>
#include<algorithm>
#include"Common.h"
using std::ifstream;
using std::string;
using std::wstring;
using std::vector;

class CLyrics
{
private:
	struct Lyric
	{
		Time time;
		wstring text;
		bool operator<(const Lyric& lyric) const	//過載小於號運算子,用於對歌詞按時間標籤排序
		{
			return lyric.time > time;
		}
	};

	wstring m_file;		//歌詞檔案的檔名
	vector<Lyric> m_lyrics;		//儲存每一句歌詞(包含時間標籤和文字)
	vector<string> m_lyrics_str;	//儲存未拆分時間標籤的每一句歌詞
	CodeType m_code_type{ CodeType::ANSI };		//歌詞文字的編碼型別
	wstring m_ti;		//歌詞中的ti標籤

	void DivideLyrics();		//將歌詞檔案拆分成若干句歌詞,並儲存在m_lyrics_str中
	void DisposeLyric();		//獲得歌詞中的時間標籤和歌詞文字,並將文字從string型別轉換成wstring型別,儲存在m_lyrics中
	void JudgeCode();		//判斷歌詞的編碼格式

public:
	CLyrics(wstring& file_name);
	CLyrics(){}
	bool IsEmpty() const;		//判斷是否有歌詞
	wstring GetLyric(Time time, int offset) const;		//根據時間返回一句歌詞。第2個引數如果是0,則返回當前時間對應的歌詞,如果是-1則返回當前時間的前一句歌詞,1則返回後一句歌詞,以此類推。
	int GetLyricProgress(Time time) const;		//根據時間返回該時間所對應的歌詞的進度(0~1000)(用於使歌詞以卡拉OK樣式顯示)
	int GetLyricIndex(Time time) const;			//根據時間返回該時間對應的歌詞序號(用於判斷歌詞是否有變化)
	CodeType GetCodeType() const;		//獲得歌詞文字的編碼型別
};
Clyrics中定義了一個巢狀的Lyric結構體,用於儲存一句歌詞,其中包含了時間標籤(Time型別)和歌詞文字(string型別)。 Time的定義如下:
struct Time
{
	int min;
	int sec;
	int msec;
};

bool operator>(Time time1, Time time2)
{
	if (time1.min != time2.min)
		return (time1.min > time2.min);
	else if (time1.sec != time2.sec)
		return(time1.sec > time2.sec);
	else if (time1.msec != time2.msec)
		return(time1.msec > time2.msec);
	else return false;
}
Clyrics類的建構函式如下:
CLyrics::CLyrics(wstring& file_name) : m_file{ file_name }
{
	DivideLyrics();
	JudgeCode();
	DisposeLyric();
	std::sort(m_lyrics.begin(), m_lyrics.end());		//將歌詞按時間標籤排序
}
使用引數傳遞檔名,並儲存到m_file中,DivideLyrics()函式用於獲取歌詞檔案中的每一行歌詞,並存入m_lyrics_str中。 JudgeCode()函式用於判斷歌詞檔案的編碼型別是ANSI還是UTF8。 DisposeLyric()函式用於獲得每一句歌詞的時間標籤和文字,並根據不同的編碼型別統一轉換成Unicode編碼,儲存到wstring容器m_lyrics中。 DivideLyrics()函式的定義如下:
void CLyrics::DivideLyrics()
{
	ifstream OpenFile{ m_file };
	string current_line;
	while (!OpenFile.eof())
	{
		std::getline(OpenFile, current_line);		//從歌詞檔案中獲取一行歌詞
		m_lyrics_str.push_back(current_line);
	}
}
使用了std::getline函式獲取檔案中的每一行,並存入m_lyric_str容器中。 JudgeCode()函式的定義如下:
void CLyrics::JudgeCode()
{
	if (!m_lyrics_str.empty())		//確保歌詞不為空
	{
		//有BOM的情況下,前面3個位元組為0xef(-17), 0xbb(-69), 0xbf(-65)就是UTF8編碼
		if (m_lyrics_str[0].size() >= 3 && (m_lyrics_str[0][0] == -17 && m_lyrics_str[0][1] == -69 && m_lyrics_str[0][2] == -65))	//確保m_lyrics_str[0]的長度大於或等於3,以防止索引越界
		{
			m_code_type = CodeType::UTF8;
		}
		else				//無BOM的情況下
		{
			int i, j;
			bool break_flag{ false };
			for (i = 0; i < m_lyrics_str.size(); i++)		//查詢每一句歌詞
			{
				if (m_lyrics_str[i].size() <= 16) continue;		//忽略字元數為6以下的歌詞(時間標籤佔10個字元),過短的字串可能會導致將ANSI編成誤判為UTF8
				for (j = 0; j < m_lyrics_str[i].size(); j++)		//查詢每一句歌詞中的每一個字元
				{
					if (m_lyrics_str[i][j] < 0)		//找到第1個非ASCII字元時跳出迴圈
					{
						break_flag = true;
						break;
					}
				}
				if (break_flag) break;
			}
			if (i<m_lyrics_str.size() && IsUTF8Bytes(m_lyrics_str[i].c_str()))		//判斷出現第1個非ASCII字元的那句歌詞是不是UTF8編碼,如果是歌詞就是UTF8編碼
				m_code_type = CodeType::UTF8_NO_BOM;
		}
	}
}
先判斷前面3個位元組是否為UTF8的BOM,沒有BOM時再呼叫IsUTF8Bytes函式判斷UTF8。 DisposeLyric()函式的定義如下:
void CLyrics::DisposeLyric()
{
	int index;
	string temp;
	Lyric lyric;
	for (int i{ 0 }; i < m_lyrics_str.size(); i++)
	{
		if (i==0)
		{
			//查詢ti:標籤
			index = m_lyrics_str[i].find("ti:");
			int index2 = m_lyrics_str[i].find_first_of(']');
			if (index != string::npos) temp = m_lyrics_str[i].substr(index + 3, index2 - index - 3);
			m_ti = StrToUnicode(temp, m_code_type);
		}

		//獲取歌詞文字
		index = m_lyrics_str[i].find_last_of(']');		//查詢最後一個']',後面的字元即為歌詞文字
		if (index == string::npos) continue;
		temp = m_lyrics_str[i].substr(index + 1, m_lyrics_str[i].size() - index - 1);
		//將獲取到的歌詞文字轉換成Unicode
		if (temp.empty())		//如果時間標籤後沒有文字,顯示為“……”
			lyric.text = L"……";
		else
			lyric.text = StrToUnicode(temp, m_code_type);

		//獲取時間標籤
		index = -1;
		while (true)
		{
			index = m_lyrics_str[i].find_first_of('[', index + 1);		//查詢第1個左中括號
			if (index == string::npos) break;		//沒有找到左中括號,退出迴圈
			else if (index > m_lyrics_str[i].size() - 9) break;		//找到了左中括號,但是左中括號在字串的倒數第9個字元以後,也退出迴圈
			else if (m_lyrics_str[i][index + 1]>'9' || m_lyrics_str[i][index + 1] < '0') break;		//找到了左中括號,但是左中括號後面不是數字,也退出迴圈
			temp = m_lyrics_str[i].substr(index + 1, 2);		//獲取時間標籤的分鐘數
			lyric.time.min = atoi(temp.c_str());
			temp = m_lyrics_str[i].substr(index + 4, 2);		//獲取時間標籤的秒鐘數
			lyric.time.sec = atoi(temp.c_str());
			if (m_lyrics_str[i][index + 8] == ']')			//如果從左中括號往右數第8個字元就是右中括號了,說明這個時間標籤的毫秒數只有1位
			{
				lyric.time.msec = m_lyrics_str[i][index + 7] - '0';
				lyric.time.msec *= 100;
			}
			else
			{
				temp = m_lyrics_str[i].substr(index + 7, 2);		//獲取時間標籤的毫秒數(這裡只取兩位,乘以10後得到毫秒數)
				lyric.time.msec = atoi(temp.c_str()) * 10;
			}
			m_lyrics.push_back(lyric);
		}
	}
}
先查詢歌詞中的文字,再根據歌詞編碼轉換成Unicode。然後查詢時間標籤,這段程式碼能夠支援多處理時間標籤的歌詞。另外在讀取時間標籤的毫秒數時根據毫秒數的位數做了不同的處理。 下面是Clyric類用於對外部介面的函式的定義。 GetLyric()函式的定義如下:
wstring CLyrics::GetLyric(Time time, int offset) const
{
	for (int i{ 0 }; i < m_lyrics.size(); i++)
	{
		if (m_lyrics[i].time>time)		//如果找到第一個時間標籤比要顯示的時間大,則該時間標籤的前一句歌詞即為當前歌詞
		{
			if (i + offset - 1 < -1) return wstring{};
			else if (i + offset - 1 == -1) return m_ti;		//時間在第一個時間標籤前面,返回ti標籤的值
			else if (i + offset - 1 < m_lyrics.size()) return m_lyrics[i + offset - 1].text;
			else return wstring{};
		}
	}
	if (m_lyrics.size() + offset - 1 < m_lyrics.size())
		return m_lyrics[m_lyrics.size() + offset - 1].text;		//如果沒有時間標籤比要顯示的時間大,當前歌詞就是最後一句歌詞
	else
		return wstring{};
}
GetLyric函式用於根據一個時間返回對應的歌詞,函式中使用一個for迴圈查詢每一句歌詞的時間標籤,當找到第一個時間標籤比引數的時間大時,該時間標籤的前一句歌詞即為為返回的歌詞。 第2個引數用於返回當前歌詞的前後第n句歌詞。該引數為0時就返回該時間對應的歌詞,為1時返回該時間後一句歌詞,為-1時返回該時間的前一句歌詞,以此類推。如果沒有歌詞可以返回,則返回空字串。 GetLyricProgress()的定義如下:
int CLyrics::GetLyricProgress(Time time) const
{
	int lyric_last_time{ 1 };		//time時間所在的歌詞持續的時間
	int lyric_current_time{ 0 };		//當前歌詞在time時間時已經持續的時間
	for (int i{ 0 }; i < m_lyrics.size(); i++)
	{
		if (m_lyrics[i].time>time)
		{
			if (i == 0)
			{
				lyric_current_time = 0;
				lyric_last_time = 1;
			}
			else
			{
				lyric_last_time = m_lyrics[i].time - m_lyrics[i - 1].time;
				lyric_current_time = time - m_lyrics[i - 1].time;
			}
			if (lyric_last_time == 0) lyric_last_time = 1;
			return lyric_current_time * 1000 / lyric_last_time;
		}
	}
	//如果最後一句歌詞之後已經沒有時間標籤,該句歌詞預設顯示20秒
	lyric_current_time = time - m_lyrics[m_lyrics.size() - 1].time;
	lyric_last_time = 20000;
	return lyric_current_time * 1000 / lyric_last_time;
}
GetLyricProgress函式的作用是返回引數所在時間對應的當前歌詞的進度,返回的值範圍為0~1000,其作用是用於使歌詞以卡拉OK的樣式顯示。 其原理是行計算當前歌詞總共需持續的時間,用下一句歌詞的時間標籤減去當前歌詞的時間標籤得到; 然後計算引數所在的時間在當前歌詞中已經持續的時間,用引數的時間減去當前歌詞的時間標籤得到; 最後用當前歌詞已經持續的時間乘以1000再除以當前歌詞總共要持續的時間每即得到歌詞的進度。
int CLyrics::GetLyricIndex(Time time) const
{
	for (int i{ 0 }; i < m_lyrics.size(); i++)
	{
		if (m_lyrics[i].time>time)
			return i - 1;
	}
	return m_lyrics.size() - 1;
}
GetLyricIndex函式用於獲得歌詞的序號,用於判斷判斷歌詞是否變化。 下面是其他成員函式的定義:
inline CodeType CLyrics::GetCodeType() const
{
	return m_code_type;
}

inline bool CLyrics::IsEmpty() const
{
	return (m_lyrics.size() == 0);
}

Clyric類中使用到的全域性函式及列舉型別的定義如下:
enum class CodeType
{
	ANSI,
	UTF8,
	UTF8_NO_BOM
};

//將string型別的字串轉換成Unicode編碼的wstring字串
wstring StrToUnicode(const string& str, CodeType code_type)
{
	wchar_t str_unicode[256]{ 0 };
	int max{ 0 };
	if (code_type == CodeType::ANSI)
	{
		max = MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, NULL, 0);
		if (max > 255) max = 255;
		MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, str_unicode, max);
	}
	else
	{
		max = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, NULL, 0);
		if (max > 255) max = 255;
		MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, str_unicode, max);
	}
	return wstring{ str_unicode };
}

//判斷一個字串是否UTF8編碼
bool IsUTF8Bytes(const char* data)
{
	int charByteCounter = 1;  //計算當前正分析的字元應還有的位元組數
	unsigned char curByte; //當前分析的位元組.
	bool ascii = true;
	for (int i = 0; i < strlen(data); i++)
	{
		curByte = static_cast<unsigned char>(data[i]);
		if (charByteCounter == 1)
		{
			if (curByte >= 0x80)
			{
				ascii = false;
				//判斷當前
				while (((curByte <<= 1) & 0x80) != 0)
				{
					charByteCounter++;
				}
				//標記位首位若為非0 則至少以2個1開始 如:110XXXXX...........1111110X 
				if (charByteCounter == 1 || charByteCounter > 6)
				{
					return false;
				}
			}
		}
		else
		{
			//若是UTF-8 此時第一位必須為1
			if ((curByte & 0xC0) != 0x80)
			{
				return false;
			}
			charByteCounter--;
		}
	}
	if (ascii) return false;		//如果全是ASCII字元,返回false
	else return true;
}

以上CLyric類的全部程式碼,希望對那些同樣需要做播放器歌詞顯示的讀者有所幫助。