本人最近在写一个音乐播放器,做了一个显示歌词的功能。虽然很多已经有很多人有自己的办法,在这里我还是想介绍一下我自己的方法。
读取歌词文件并不困难,因为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位。
另外歌词文件中每一行的时间标签可能不止一个。
下面是读取歌词文件的方法:
- 读取歌词文件中和每一行,将每一行歌词(包括时间标签)存入一个string容器中。
- 依次处理刚刚得到的string容器中的每一个字符串。
- 查找字符串的最后一个右中括号“]”,将最后一个右中括号后面的字符作为歌词文本。
- 依次从第一个字符开始开始查找左中括号“[”,将右边两个字符作为分钟数,其右边第4个字符开始两个字符作为秒钟数,第7个字符开始为毫秒数。
- 将得到到时间标签和文件作为一句歌词存入容器。
- 继续查找左中括号“[”,如果找到了则重复4、5步骤,否则处理下一句歌词。
另外歌词的处理中还需要处理判断歌词编码为ANSI还是UTF8的问题。
我写了一个歌词Lrycs类,下面是类的声明:
#pragma once
#include
#include
#include
#include
#include
#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 m_lyrics; //储存每一句歌词(包含时间标签和文本)
vector 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
先判断前面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(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类的全部代码,希望对那些同样需要做播放器歌词显示的读者有所帮助。