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
#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类的全部代码,希望对那些同样需要做播放器歌词显示的读者有所帮助。

你可能感兴趣的:(C++读取歌词(lrc)文件,分解歌词时间标签和歌词文本的方法)