本人最近在写一个音乐播放器,做了一个显示歌词的功能。虽然很多已经有很多人有自己的办法,在这里我还是想介绍一下我自己的方法。
读取歌词文件并不困难,因为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编成误判为UTF8for (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);//将获取到的歌词文本转换成Unicodeif (temp.empty())     //如果时间标签后没有文本,显示为“……”lyric.text = L"……";elselyric.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;      //如果没有时间标签比要显示的时间大,当前歌词就是最后一句歌词elsereturn 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 此时第一位必须为1if ((curByte & 0xC0) != 0x80){return false;}charByteCounter--;}}if (ascii) return false;       //如果全是ASCII字符,返回falseelse return true;
}

以上CLyric类的全部代码,希望对那些同样需要做播放器歌词显示的读者有所帮助。
												

C++读取歌词(lrc)文件,分解歌词时间标签和歌词文本的方法相关推荐

  1. python两种方法读取、修改文件的创建时间、修改时间、访问时间

    看到网上有人出于特种目前,需要修改文件的创建时间和修改时间(访问时间是只要在操作系统里打开文件,系统就会自动更改最后的访问时间,因此此时间无意义,于是在网上查阅结合自己的经验,归纳 一下可行方案,在  ...

  2. php中文歌词,详细介绍HTML5使用Audio标签实现歌词同步的效果

    HTML5的最强大之处莫过于对媒体文件的处理,如利用一个简单的vedio标签就可以实现视频播放.类似地,在HTML5中也有对应的处理音频文件的标签,那就是audio标签.通过本文给大家介绍HTML5使 ...

  3. php更新时间就变成1970了,phpcms调用文章发布时间标签显示1970的解决方法

    strtotime() 函数将任何英文文本的日期时间描述解析为 Unix 时间戳. 复制代码代码如下: {date('Y',strtotime($updatetime))} 大写Y显示 2013, 小 ...

  4. Android自定义View来实现解析lrc歌词同步滚动、上下拖动、缩放歌词等功能

    http://blog.csdn.net/ouyang_peng/article/details/50813419 前言 一LRC歌词文件简介 1什么是LRC歌词文件 2LRC歌词文件的格式 LRC歌 ...

  5. LRC歌词解析,实现Linux设备播放音乐显示歌词 LRC解析

    开始正文~~~ 1.关于LRC lrc是英文lyric(歌词)的缩写,被用做歌词文件的扩展名.以lrc为扩展名的歌词文件可以在各类数码播放器中同步显示.LRC 歌词是一种包含着"*:*&qu ...

  6. html怎么读取lrc文件,lrc文件怎么打开?lrc是什么文件?

    lrc文件怎么打开?lrc是什么文件? lrc是歌词 文件的扩展名,一般用记事本打开. 关于lrc是什么文件?lrc是英文lyric(歌词)的缩写,被用做歌词文件的扩展名.以lrc为扩展名的歌词文件可 ...

  7. 用批处理整理百度MP3上歌曲排行榜MP3及LRC文件的批量下载链接地址(含图文教程)

    http://bbs.wuyou.com/viewthread.php?tid=192322 本文结构如下: 一.缘起:问题的提出 二.试探:徒劳而返 三.峰回路转:芝麻!开门! 四.万事俱备:xml ...

  8. python歌词统计单词词频_Python爬虫网易云歌词及词频统计

    采用词云对邓紫棋的热门前50歌曲进行可视化展示. 本次可视化步骤需要掌握的内容有:了解爬虫的原理 掌握xpath的用法 掌握词云工具wordcloud的使用 了解分词根据jieba的使用 首先,需要找 ...

  9. 小米手机安装https证书报错:无法安装该证书 因为无法读取该证书文件

    Fiddler]手机安装https证书报错:无法安装该证书 因为无法读取该证书文件 之前在手机上使用 "ip:端口号" 的方法就能直接在手机上自动下载安装fiddler证书,但是现 ...

最新文章

  1. HDUOJ---2112HDU Today
  2. 怎么删除结构体数组中的一组数据_数据结构-栈
  3. 多线程处理海量数据的解决方案
  4. kafka偏移量保存到mysql里_Kafka 新版消费者 API(二):提交偏移量
  5. 关于云原生,这是最详细的技术知识
  6. css属性 content
  7. 人工智能切入垂直领域 风口已至?
  8. php dns失败,dns错误是什么意思
  9. leetcode python3 简单题225. Implement Stack using Queues
  10. 网络安全实验三 PGP 实现邮件加密和签名
  11. 人脸识别,结构光名词记录
  12. 亚马逊智能音箱无故发出笑声,多名用户被吓尿
  13. zookeeper入门篇
  14. vue3 + router-view + keepalive parentComponent.ctx.deactivate is not a function
  15. M480 EMAC驱动01-EMAC底层接口
  16. 新一代iPad Pro外形泄露:方形后摄瞩目
  17. Steamsets安装教程
  18. canvas - 基础知识 - 绘制剪纸图形
  19. mysql-8.0.16-winx64_mysql-8.0.16-winx64的最新安装教程
  20. 直流无刷电机霍尔传感器2种安装方式

热门文章

  1. 写在2021最后一天
  2. 孟岩:通证经济设计的七个原则,八个陷阱和十一个模板
  3. Android 刘海屏 适配
  4. APP移动应用测试策略与工具思维导图
  5. 专访中国信通院云大所栗蔚:ChatGPT的成功揭示了云计算作为数字世界“中枢神经”的价值
  6. [洛谷P3975][TJOI2015]弦论
  7. 神经网络架构搜索(NAS)综述
  8. 利用aether api实现从指定maven仓库下载jar包
  9. python爬取网站时,一键获取headers、url等信息(真的是让我爬取网站时,省了不少力气,作为小秘密分享给大家喽)
  10. 网络爬虫在业务中的应用