此篇文章主要补充 C#制作简单的本地音乐播放器(一) 中的“歌词显示”部分的内容。
页面设计部分
相关内容
本程序使用的歌词文件为lrc格式,lrc是英文lyric(歌词)的缩写,lrc歌词是一种通过编辑器把歌词按歌曲歌词出现的时间编辑成一个文件,在播放歌曲时同步依次显示出来的一种歌词文件。其大致格式如下图所示:
lrc歌词文本中含有两类标签:一是标识标签,其格式为“[标识名:值]”主要包含以下预定义的标签:[ar:歌手名]、[ti:歌曲名]、[al:专辑名]、[by:编辑者(指lrc歌词的制作人)]、[offset:时间补偿值] (其单位是毫秒,正值表示整体提前,负值相反。这是用于总体调整显示快慢的,但多数的MP3可能不会支持这种标签)。二是时间标签,形式为“[mm:ss]”或“[mm:ss.fff]”(分钟数:秒数:毫秒数),时间标签需位于某行歌词中的句首部分,一行歌词可以包含多个时间标签(比如歌词中的迭句部分)。当歌曲播放到达某一时间点时,MP3就会寻找对应的时间标签并显示标签后面的歌词文本,这样就完成了“歌词同步”的功能。
主要思路
了解了lrc歌词文件的格式后就要对其进行处理了,首先是找到与歌曲文件同路径下的同名lrc文件,将时间戳与歌词进行分离,在歌曲进行播放时,通过判断歌曲的当前时间来找到与之相符的歌词,并显示在所设计的控件上。
代码实现部分
1.找到对应的lrc文件
首先声明一个是否有对应歌词的布尔变量。
bool havelrc = false; //为是否含有歌词声明布尔变量
之后添加一个查找歌词的方法,先获取当前播放歌曲的路径(上篇文章的方法中listBox中的项即为该歌曲的路径),再通过字符串的截取,判断当前播放歌曲的后缀,将歌曲路径字符串的后缀更改为lrc,判断是否存在该文件。其中关于字符串的截取替换等操作可以参考一下C#字符串的分割与截取。
public void showLyric()
{
//判断当前播放的的歌曲是否有歌词文件
//获得当前正在播放的歌曲
string songpath = listBox_music.SelectedItem.ToString();
string b = songpath.Substring(songpath.LastIndexOf(".") + 1);
//显示按要求分割的最后一个值
string newgeci = songpath.Replace(b, "lrc");
//判断是否有歌词文件,是则分割歌词,否则提示未找到歌词文件
if (File.Exists(newgeci))
{
havelrc = true;
timeslist = Lrcs.Lrc.Lrctime(newgeci);
wordslist = Lrcs.Lrc.Lrcword(newgeci);
}
else
{
havelrc = false;
}
}
2.分割对应的lrc文件
找到相对应的文件后,下一步就是要对该文件进行分割了。新建lrcs类,其中新建一个分割时间戳的方法,一个分割歌词的方法。如下所示的是不对标识标签进行处理的简易方法,如需详细分割方法,可以参考这位大佬的文章 C#解析lrc歌词文件 。
using System;
using System.Collections;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
namespace mymusicplayer
{
public class Lrcs
{
public class Lrc
{
/// <summary>
/// 获得歌词信息
/// </summary>
/// <param name="LrcPath">歌词路径</param>
/// <returns>返回歌词信息</returns>
public static ArrayList Lrcword(string LrcPath)
{
ArrayList wordslist = new ArrayList();
using (FileStream fs = new FileStream(LrcPath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
string line;
using (StreamReader sr = new StreamReader(fs, Encoding.GetEncoding("UTF-8")))
{
while ((line = sr.ReadLine()) != null)
{
if (line.StartsWith("[ti:")){}
else if (line.StartsWith("[ar:")){}
else if (line.StartsWith("[al:")){}
else if (line.StartsWith("[by:")){}
else if (line.StartsWith("[offset:")){}
else
{
Regex regex = new Regex(@"\[([0-9.:]*)\]+(.*)", RegexOptions.Compiled);
string woooooord = line.Substring(line.LastIndexOf("]") + 1);
wordslist.Add(woooooord);
}
}
}
}
return wordslist;
}
/// <summary>
/// 获得歌词时间
/// </summary>
/// <param name="LrcPath">歌词路径</param>
/// <returns>返回歌词时间</returns>
public static ArrayList Lrctime(string LrcPath)
{
Lrc lrc = new Lrc();
ArrayList timeslist = new ArrayList();
using (FileStream fs = new FileStream(LrcPath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
string line;
using (StreamReader sr = new StreamReader(fs, Encoding.Default))
{
while ((line = sr.ReadLine()) != null)
{
if (line.StartsWith("[ti:")) { }
else if (line.StartsWith("[ar:")) { }
else if (line.StartsWith("[al:")) { }
else if (line.StartsWith("[by:")) { }
else if (line.StartsWith("[offset:")) { }
else
{
Regex regex = new Regex(@"\[([0-9.:]*)\]+(.*)", RegexOptions.Compiled);
MatchCollection mc = regex.Matches(line);
double time = TimeSpan.Parse("00:" + mc[0].Groups[1].Value).TotalSeconds;
timeslist.Add(time);
}
}
}
}
return timeslist;
}
/// <summary>
/// 处理信息(私有方法)
/// </summary>
/// <param name="line"></param>
/// <returns>返回基础信息</returns>
static string SplitInfo(string line)
{
return line.Substring(line.IndexOf(":") + 1).TrimEnd(']');
}
}
}
}
3.获取当前歌曲时间
当对歌词文件的处理完成之后,下一步就应该考虑显示歌词,首先就是要获取当前播放到的时间,这里只是提一下,此句代码在歌词显示部分已包含。
//获得当前播放器的时间
double currenttime = myMediaPlayer.Ctlcontrols.currentPosition;
4.显示当前歌曲歌词
这里的处理方法即是利用timer控件的Tick方法,获取当前播放时间,并遍历时间戳数组,将符合时间范围的歌词数组中的项显示在label上,我使用了5个label,第2个label显示当前正在播放的句子,第1个label显示正在播放的上一句,第3、4、5个label显示正在播放的下三句,形成滚动的效果,当播放完成或开始播放时,第1、3、4、5个label的数组位置可能越界,为其添加try方法,并在越界时使label不显示内容,具体代码如下:
注:当歌词显示到最后一句时,第二层的循环语句中,[i+1]项会数组越界,导致无法跳转歌词,一时间没有好的解决办法,只能靠try使其报错后进行跳转,如果有更好的办法,可以替换掉,能告诉我一下更好。
//歌词计时器方法
private void timer_lrc_Tick(object sender, EventArgs e)
{
if (havelrc == true)
{
//获得当前播放器的时间
double currenttime = myMediaPlayer.Ctlcontrols.currentPosition;
for (int i = 0; i <= timeslist.Count - 1; i++)
{
try
{
if (currenttime >= double.Parse(timeslist[i].ToString()) && currenttime < double.Parse(timeslist[i + 1].ToString()))
{
try
{ label_lrc1.Text = wordslist[i - 1].ToString(); }
catch (Exception)
{ label_lrc1.Text = "";}
label_lrc2.Text = wordslist[i].ToString();
try
{ label_lrc3.Text = wordslist[i + 1].ToString(); }
catch (Exception)
{ label_lrc3.Text = "";}
try
{ label_lrc4.Text = wordslist[i + 2].ToString();}
catch (Exception)
{ label_lrc4.Text = "";}
try
{ label_lrc5.Text = wordslist[i + 3].ToString();}
catch (Exception)
{ label_lrc5.Text = "";}
}
}
catch (Exception)
{
try
{ label_lrc1.Text = wordslist[i - 1].ToString();}
catch (Exception)
{ label_lrc1.Text = "";}
label_lrc2.Text = wordslist[i].ToString();
try
{ label_lrc3.Text = wordslist[i + 1].ToString();}
catch (Exception)
{ label_lrc3.Text = "";}
try
{ label_lrc4.Text = wordslist[i + 2].ToString();}
catch (Exception)
{ label_lrc4.Text = ""; }
try
{ label_lrc5.Text = wordslist[i + 3].ToString();}
catch (Exception)
{ label_lrc5.Text = "";}
}
}
}
else if (havelrc == false)
{
label_lrc1.Text = "";
label_lrc2.Text = "抱歉,未找到该曲目歌词";
label_lrc3.Text = "";
label_lrc4.Text = "添加歌词方法详见【关于歌词】";
label_lrc5.Text = "";
}
}
后记部分
此时的音乐播放器就具备了基本的显示歌词功能,关于lrc歌词的问题,其实也可以从网络上搜到该歌曲的lrc格式歌词文本,复制后保存为txt文件,将其名称与歌曲文件命名一致,并更改后缀为lrc就可以了。
关于歌曲的多文件添加、将歌曲列表的显示路径更改为显示歌曲名、还有仍未解决原帖中提到的下次打开程序时加载历史歌曲列表的问题,后续应该会再写,最后,希望这篇文章对你能有一点帮助。纰漏之处,还望海涵。