Android练习项目 Mp3播放器实现 歌词解析(三)

这是系列博客的第三篇,这一篇主要讲讲如何实现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的文件路径,接着替换下对应的名字,所以只有歌词名字和歌名相同才能够显示了。

好了,解析就介绍这么多了,下一篇准备介绍下,如何在播放器中实现歌词的同步,这个需要重新绘制控件来显示。

生命在于不断努力,继续向前。

你可能感兴趣的:(Android练习项目 Mp3播放器实现 歌词解析(三))