C++讀取歌詞(lrc)檔案,分解歌詞時間標籤和歌詞文字的方法
阿新 • • 發佈:2019-01-02
本人最近在寫一個音樂播放器,做了一個顯示歌詞的功能。雖然很多已經有很多人有自己的辦法,在這裡我還是想介紹一下我自己的方法。
讀取歌詞檔案並不困難,因為lrc格式的歌詞本身很有規律,下面為一個lrc檔案的一部分:
Clyric類中使用到的全域性函式及列舉型別的定義如下:
以上CLyric類的全部程式碼,希望對那些同樣需要做播放器歌詞顯示的讀者有所幫助。
[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位。
另外歌詞檔案中每一行的時間標籤可能不止一個。
下面是讀取歌詞檔案的方法:
- 讀取歌詞檔案中和每一行,將每一行歌詞(包括時間標籤)存入一個string容器中。
- 依次處理剛剛得到的string容器中的每一個字串。
- 查詢字串的最後一個右中括號“]”,將最後一個右中括號後面的字元作為歌詞文字。
- 依次從第一個字元開始開始查詢左中括號“[”,將右邊兩個字元作為分鐘數,其右邊第4個字元開始兩個字元作為秒鐘數,第7個字元開始為毫秒數。
- 將得到到時間標籤和檔案作為一句歌詞存入容器。
- 繼續查詢左中括號“[”,如果找到了則重複4、5步驟,否則處理下一句歌詞。
Clyrics中定義了一個巢狀的Lyric結構體,用於儲存一句歌詞,其中包含了時間標籤(Time型別)和歌詞文字(string型別)。 Time的定義如下:#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類的建構函式如下: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(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類的全部程式碼,希望對那些同樣需要做播放器歌詞顯示的讀者有所幫助。