这是系列博客的第三篇,这一篇主要讲讲如何实现lrc歌词的解析,这个对于很多mp3的播放的同时看到歌词,十分重要。这也是其中比较重要的功能。
那就需要首先看下lrc文件的基本构造,这样才能够按照固定的规律去解析。
[ar:许嵩]
[ti:半城烟沙]
[00:00.79] 《半城烟沙》
[00:04.20] 词/曲/制作人/演唱:许嵩
[00:08.42] 和声编写/和声:许嵩
[00:11.62] 录音/混音:许嵩
[00:33.96] 有些爱像断线纸鸢 结局悲余手中线
[00:42.24] 有些恨像是一个圈 冤冤相报不了结
[00:50.57] 只为了完成一个夙愿 还将付出几多鲜血
[00:57.94] 忠义之言 自欺欺人的谎言
[01:06.99] 有些情入苦难回绵 窗间月夕夕成玦
[01:15.27] 有些仇心藏却无言 腹化风雪为刀剑
[01:23.55] 只为了完成一个夙愿 荒乱中邪正如何辨
[01:30.97] 飞沙狼烟将乱我 徒有悲添
[01:39.41] 半城烟沙 兵临池下
[01:43.37] 金戈铁马 替谁争天下
[01:47.53] 一将成 万骨枯 多少白发送走黑发
[01:55.87] 半城烟沙 随风而下
[01:59.98] 手中还有 一缕牵挂
[02:04.15] 只盼归田卸甲 还能捧回你沏的茶
[02:46.34] 有些情入苦难回绵
[02:49.64] 窗间月夕夕成玦
[02:54.63] 有些仇心藏却无言
[02:57.88] 腹化风雪为刀剑
[03:02.34] AiAiAi为了完成一个夙愿
[03:06.42] 荒乱中邪正如何辨
[03:10.28] 飞沙狼烟将乱我 徒有悲添
[03:18.62] 半城烟沙 兵临池下
[03:22.73] 金戈铁马 替谁争天下
[03:26.90] 一将成 万骨枯 多少白发送走黑发
[03:35.18] 半城烟沙 随风而下
[03:39.24] 手中还有 一缕牵挂
[03:43.46] 只盼归田卸甲 还能捧回你沏的茶
[03:51.75] 半城烟沙 兵临池下
[03:55.86] 金戈铁马 替谁争天下
[04:00.02] 一将成 万骨枯 多少白发送走黑发
[04:08.30] 半城烟沙 血泪落下
[04:12.42] 残骑裂甲 铺红天涯
[04:16.59] 转世燕还故榻 为你衔来二月的花
这就是一般标准的lrc的文件格式,前面可能会有ti,ar,al,by等对应的字段,这些字段表示的是mp3文件的取名,歌手,专辑,还有歌词制作者。
后面就是我们需要的信息了,关于前面是时间的格式[04:16.59],不多这个是需要转换时间的,这个等会代码里面会有处理,我们通过解析时间字段和内容字段来放到ArrayList里面,这样就会有对应的时间和对应的内容,方便在播放的时候同步歌词根据播放的时间。
下面介绍下这个类,这个类主要是拿来存储时间和对应的内容,为什么我没用Map和TreeMap等操作,主要是因为这些没有下标,在歌词搜索的时候很难寻找,为了对后面的同步能够实现,我采用了类的存储方法。
LrcList.java
package com.flashmusic.tool;
/**
* Created by zhouchenglin on 2016/4/20.
*/
public class LrcList {
//保存当前时间
private long currentTime;
//保存内容
private String content;
public long getCurrentTime() {
return currentTime;
}
public void setContent(String content) {
this.content = content;
}
public void setCurrentTime(long currentTime) {
this.currentTime = currentTime;
}
public String getContent() {
return content;
}
}
下面介绍下LrcInfo.java
因为我们可能需要ti,by等字段的显示,所以我们采用了这个类来做一个整体的构造,虽然最后我没用到这些值,但是作为解析,我觉得还是很有必要的。
package com.flashmusic.tool;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* Created by zhouchenglin on 2016/4/19.
*/
public class LrcInfo {
private String title;//标题
private String artist;//歌手
private String album;//专辑名字
private String bySomeBody;//歌词制作者
private String offset;
private String language; //语言
private String errorinfo; //错误信息
//保存歌词信息和时间点
ArrayList<LrcList> lrcLists;
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public void setErrorinfo(String errorinfo) {
this.errorinfo = errorinfo;
}
public String getErrorinfo() {
return errorinfo;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getArtist() {
return artist;
}
public void setArtist(String artist) {
this.artist = artist;
}
public String getAlbum() {
return album;
}
public void setAlbum(String album) {
this.album = album;
}
public String getBySomeBody() {
return bySomeBody;
}
public void setBySomeBody(String bySomeBody) {
this.bySomeBody = bySomeBody;
}
public String getOffset() {
return offset;
}
public void setOffset(String offset) {
this.offset = offset;
}
public ArrayList<LrcList> getLrcLists() {
return lrcLists;
}
public void setLrcLists(ArrayList<LrcList> lrcLists) {
this.lrcLists = lrcLists;
}
}
这两个信息都处理完了,接下来主要是通过LrcParse.java来解析,这个里面就是具体的对Lrc的处理。
package com.flashmusic.tool;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by zhouchenglin on 2016/4/19.
*/
public class LrcParse {
private LrcInfo lrcInfo = new LrcInfo();
public static String charSet = "gbk";
//mp3歌词存放地地方
private String Path;
//mp3时间
private long currentTime;
//MP3对应时间的内容
private String currentContent;
//保存时间点和内容
ArrayList<LrcList> lrcLists = new ArrayList<LrcList>();
private InputStream inputStream;
public LrcParse(String path) {
this.Path = path.replace(".mp3", ".lrc");
}
public LrcInfo readLrc() {
//定义一个StringBuilder对象,用来存放歌词内容
StringBuilder stringBuilder = new StringBuilder();
try {
inputStream = new FileInputStream(this.Path);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, charSet));
String str = null;
//逐行解析
while ((str = reader.readLine()) != null) {
if (!str.equals("")) {
decodeLine(str);
}
}
//全部解析完后,设置lrcLists
lrcInfo.setLrcLists(lrcLists);
return lrcInfo;
} catch (FileNotFoundException e) {
e.printStackTrace();
LrcList lrcList = new LrcList();
//设置时间点和内容的映射
lrcList.setContent("歌词文件没发现!");
lrcLists.add(lrcList);
lrcInfo.setLrcLists(lrcLists);
return lrcInfo;
} catch (IOException e) {
e.printStackTrace();
LrcList lrcList = new LrcList();
//设置时间点和内容的映射
lrcList.setContent("木有读取到歌词!");
lrcLists.add(lrcList);
lrcInfo.setLrcLists(lrcLists);
return lrcInfo;
}
}
/**
* 单行解析
*/
private LrcInfo decodeLine(String str) {
if (str.startsWith("[ti:")) {
// 歌曲名
lrcInfo.setTitle(str.substring(4, str.lastIndexOf("]")));
// lrcTable.put("ti", str.substring(4, str.lastIndexOf("]")));
} else if (str.startsWith("[ar:")) {// 艺术家
lrcInfo.setArtist(str.substring(4, str.lastIndexOf("]")));
} else if (str.startsWith("[al:")) {// 专辑
lrcInfo.setAlbum(str.substring(4, str.lastIndexOf("]")));
} else if (str.startsWith("[by:")) {// 作词
lrcInfo.setBySomeBody(str.substring(4, str.lastIndexOf("]")));
} else if (str.startsWith("[la:")) {// 语言
lrcInfo.setLanguage(str.substring(4, str.lastIndexOf("]")));
} else {
//设置正则表达式,可能出现一些特殊的情况
String timeflag = "\\[(\\d{1,2}:\\d{1,2}\\.\\d{1,2})\\]|\\[(\\d{1,2}:\\d{1,2})\\]";
Pattern pattern = Pattern.compile(timeflag);
Matcher matcher = pattern.matcher(str);
//如果存在匹配项则执行如下操作
while (matcher.find()) {
//得到匹配的内容
String msg = matcher.group();
//得到这个匹配项开始的索引
int start = matcher.start();
//得到这个匹配项结束的索引
int end = matcher.end();
//得到这个匹配项中的数组
int groupCount = matcher.groupCount();
for (int index = 0; index < groupCount; index++) {
String timeStr = matcher.group(index);
Log.i("", "time[" + index + "]=" + timeStr);
if (index == 0) {
//将第二组中的内容设置为当前的一个时间点
currentTime = str2Long(timeStr.substring(1, timeStr.length() - 1));
}
}
//得到时间点后的内容
String[] content = pattern.split(str);
//将内容设置为当前内容,需要判断只出现时间的情况,没有内容的情况
if (content.length == 0) {
currentContent = "";
} else {
currentContent = content[content.length - 1];
}
LrcList lrcList = new LrcList();
//设置时间点和内容的映射
lrcList.setCurrentTime(currentTime);
lrcList.setContent(currentContent);
lrcLists.add(lrcList);
}
}
return this.lrcInfo;
}
private long str2Long(String timeStr) {
//将时间格式为xx:xx.xx,返回的long要求以毫秒为单位
Log.i("", "timeStr=" + timeStr);
String[] s = timeStr.split("\\:");
int min = Integer.parseInt(s[0]);
int sec = 0;
int mill = 0;
if (s[1].contains(".")) {
String[] ss = s[1].split("\\.");
sec = Integer.parseInt(ss[0]);
mill = Integer.parseInt(ss[1]);
Log.i("", "s[0]=" + s[0] + "s[1]" + s[1] + "ss[0]=" + ss[0] + "ss[1]=" + ss[1]);
} else {
sec = Integer.parseInt(s[1]);
Log.i("", "s[0]=" + s[0] + "s[1]" + s[1]);
}
//时间的组成
return min * 60 * 1000 + sec * 1000 + mill * 10;
}
}
该类解析函数返回对应的LrcInfo类,如果过程中有错误,例如文件没找到,解析错误,都会在时间点里面存储,对应的错误。
同时这里面需要注意编码的问题,大部分歌词文件都是gbk编码,不排除utf-8编码,所以处理的时候,解析出来是乱码,可能就是编码问题。
调用的时候这样就行了
LrcParse a =new LrcParse(Environment.getExternalStorageDirectory().getAbsolutePath()+"/幻听.MP3");
LrcInfo lrcInfo = a.readLrc();
这里面为什么传入的路径是.mp3,主要是因为我们需要获取同一目录下的歌词文件,所以一般我们得到mp3的文件路径,接着替换下对应的名字,所以只有歌词名字和歌名相同才能够显示了。
好了,解析就介绍这么多了,下一篇准备介绍下,如何在播放器中实现歌词的同步,这个需要重新绘制控件来显示。
生命在于不断努力,继续向前。