歌詞顯示控制元件的實現上——歌詞解析

Android機動車發表於2017-10-31

最近打算仿網易雲音樂的音樂播放器,除了網路框架、介面資料等這些外,最核心的就是音樂的播放和歌詞的顯示。

考慮到歌詞顯示控制元件涉及到歌詞解析,自定義控制元件的實現等等諸多方面,可能文章的篇幅上會比較冗長,同時也為了方便自己和碼友們能夠根據自己的需求和愛好各取所需,將《歌詞顯示控制元件的實現上》這篇文章分成上、下兩篇,分別是《歌詞顯示控制元件的實現上——歌詞解析》和《歌詞顯示控制元件的實現下——歌詞展示自定義View》。而今天將要分享的是上篇,主要講解關於*.lrc檔案的解析。

我們本文的目的是將lrc格式的歌詞檔案進行解析,並能將其展示到介面。

先看下效果:

這裡寫圖片描述

ok,開始切入正題

一、瞭解歌詞檔案結構

寫過音樂播放器的朋友可能都瞭解過歌詞檔案的規範格式,既然是歌詞顯示的控制元件,就必然需要清楚地瞭解歌詞檔案的組成規範,才能準確無誤的解析歌詞檔案,得到我們想要的資訊。

那我們先看一個最普通的歌詞檔案:

[ti:一個人的北京]
[ar:好妹妹樂隊]
[al:南北]
[by:]
[offset:0]
[00:00.10]一個人的北京 - 好妹妹樂隊
[00:00.20]詞:秦昊
[00:00.30]曲:秦昊
[00:00.40]
[00:30.16]你有多久沒有看到 滿天的繁星
[00:37.34]城市夜晚虛偽的光明 遮住你的眼睛
[00:44.40]連週末的電影 也變得不再有趣
[00:51.71]疲憊的日子裡 有太多的問題
[00:59.21]
[01:00.96]你有多久單身一人 不再去旅行
[01:08.20]習慣下班回到家裡 冷冰冰的空氣
[01:15.58]愛情這東西 你已經不再有勇氣
[01:22.64]情歌有多動聽 你就有多懷疑
[01:30.60]許多人來來去去 相聚又別離
[01:38.29]也有人喝醉哭泣 在一個人的北京
[01:45.16]也許我成功失意 慢慢的老去
[01:52.76]能不能讓我留下片刻的回憶
[01:58.95]
[01:59.67]許多人來來去去 相聚又別離
[02:07.23]也有人匆匆逃離 這一個人的北京
[02:14.30]也許有一天我們 一起離開這裡
[02:21.86]離開了這裡 在晴朗的天氣
[02:28.38]
[02:58.98]你有多久單身一人 不再去旅行
[03:06.36]習慣下班回到家裡 冷冰冰的空氣
[03:13.55]愛情這東西 你已經不再有勇氣
[03:20.69]情歌有多動聽 你就有多懷疑
[03:28.53]許多人來來去去 相聚又別離
[03:36.22]也有人喝醉哭泣 在一個人的北京
[03:43.28]也許我成功失意 慢慢的老去
[03:50.82]能不能讓我留下片刻的回憶
[03:57.64]許多人來來去去 相聚又別離
[04:05.25]也有人匆匆逃離 這一個人的北京
[04:12.31]也許有一天我們 一起離開這裡
[04:19.88]離開了這裡 在晴朗的天氣
[04:26.62]許多人來來去去 相聚又別離
[04:34.24]也有人匆匆逃離 這一個人的北京
[04:41.37]也許有一天我們 一起離開這裡
[04:48.87]離開了這裡 在晴朗的天氣
[04:55.08]

複製程式碼

所有歌詞檔案——*.lrc檔案 都是以一個標準來進行製作的(如上)。

從上述內容可以得出:

  • 所有標籤(包括時間標籤)全部由“[”和“]”兩個符合標識,其對應各自內容
  • 一行只表達一條資訊
  • "ti"表示標題、"ar"表示歌手、"al"表示專輯、"by"表示製作、"offset:"表示時間偏移量
  • "[mm:ss.ms]"表示歌詞時間和內容

對比json和xml結構的資料,歌詞這樣的資料結構更加簡單和清晰。

瞭解清楚歌詞檔案結構,我們就能對症下藥:

二、開始解析

既然瞭解了歌詞檔案的組成部分,那麼解析歌詞檔案也就不難,就是簡單的檔案內容讀取:

  • 1、首先獲取*.lrc歌詞檔案的二進位制流InputStream,
  • 2、再又轉換成字元流
  • 3、然後再呼叫BufferedReader的readLine()方法逐行讀取檔案內容

就能獲得檔案內容了,在這裡有一點需要注意的是,各種流在使用結束後一定要呼叫close()方法關閉。

下面就是實現歌詞檔案的解析工作:

1、實體類

首先,需要準備兩個類主要用於歌詞解析結果的快取:

LineInfo:歌詞行資訊:包含行開始時間和歌詞行內容 LyricInfo:歌詞資訊:包含標題、歌手、專輯等等

/**
 * Description: 歌詞  每行資訊 實體類
 * Created by jia on 2017/10/31.
 * 人之所以能,是相信能
 */
public class LineInfo {

    private String content;

    private long startTime;

	// 對應的get/set方法略
	
}
複製程式碼
/**
 * Description: 歌詞資訊  實體類
 * Created by jia on 2017/10/31.
 * 人之所以能,是相信能
 */
public class LyricInfo {

    // 偏移量
    private long offset;
    // 名字
    private String title;
    // 作者
    private String artist;
    // 專輯
    private String album;
    // 製作
    private String by;

    // 歌詞
    private List<LineInfo> lines;

	// 對應的get/set方法略
}
複製程式碼

2、解析工具類

首先因為在實體類中,包括以後自定義View時的時間都是以毫秒為單位的long型別,所以我們需要一方法將時間標籤中的內容轉為long型別的毫秒值:

/**
 * 從字串中獲得時間值
 */
private static long measureStartTimeMillis(String str) {
	long minute = Long.parseLong(str.substring(1, 3));
	long second = Long.parseLong(str.substring(4, 6));
	long millisecond = Long.parseLong(str.substring(7, 9));
	return millisecond + second * 1000 + minute * 60 * 1000;
}
複製程式碼

然後就是逐行解析:

    /**
     * 逐行解析歌詞
     *
     * @param info 實體類
     * @param line 每行內容
     */
    private static void analyzeLyricByLine(LyricInfo info, String line) {

        int index = line.lastIndexOf("]");

        // 標題
        if (!TextUtils.isEmpty(line) && line.startsWith("[ti:")) {
            info.setTitle(line.substring(4, index).trim());
            return;
        }

        // 歌手
        if (!TextUtils.isEmpty(line) && line.startsWith("[ar:")) {
            info.setArtist(line.substring(4, index).trim());
            return;
        }

        // 專輯
        if (!TextUtils.isEmpty(line) && line.startsWith("[al:")) {
            info.setAlbum(line.substring(4, index).trim());
            return;
        }

        // 製作
        if (!TextUtils.isEmpty(line) && line.startsWith("[by:")) {
            info.setBy(line.substring(4, index).trim());
            return;
        }

        // 偏移量
        if (!TextUtils.isEmpty(line) && line.startsWith("[offset:")) {
            info.setOffset(Long.parseLong(line.substring(8, index).trim()));
            return;
        }

        // 歌詞內容
        if (line!=null && index == 9 && line.trim().length() >= 10) {
            LineInfo lineInfo = new LineInfo();
            lineInfo.setStartTime(measureStartTimeMillis(line.substring(0, 10)));
            if(line.length()==10){
                lineInfo.setContent("");
            }else{
                lineInfo.setContent(line.substring(10, line.length()));
            }
            info.getLines().add(lineInfo);// 新增到歌詞集合中
            return;
        }

        return;
    }
複製程式碼

我們需要對各種標籤進行判斷和解析,然後賦值給實體類物件。

首先拿到"]"字元的索引,然後擷取對應標籤的內容進行匹配,分別進行賦值。

特別的想說一句:解析歌詞時,可能會遇到某行有時間但沒有歌詞內容,就做了這樣一個處理:if(line.length()==10) lineInfo.setContent("");

3、從輸入流中讀取,並呼叫步驟2中方法逐行解析

    /**
     * 解析歌詞
     *
     * @param ins
     * @param charsetName
     */
    public static LyricInfo initLyric(InputStream ins, String charsetName) {

        if (ins == null) return null;

        try {
            LyricInfo lyricInfo = new LyricInfo();
            lyricInfo.setLines(new ArrayList<LineInfo>());

            InputStreamReader inputStreamReader = new InputStreamReader(ins, charsetName);
            BufferedReader reader = new BufferedReader(inputStreamReader);
            String line = null;
            // 逐行解析
            while ((line = reader.readLine()) != null) {
                analyzeLyricByLine(lyricInfo, line);
            }
            reader.close();
            ins.close();
            inputStreamReader.close();

            return lyricInfo;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
複製程式碼

因為歌詞檔案不論在assets下還是在SD卡上,我們必須都得獲取輸入流,設定編碼格式,然後呼叫analyzeLyricByLine逐行解析,將解析完的資料設定給新建的實體類並返回。

這裡我們核心使用的是**BufferedReader 的 readLine()**方法。

三、解析驗證

這裡為了方便,我將歌詞檔案放在了assets下

try {
	InputStream is = getAssets().open("beijing.lrc");
	LyricInfo lyricInfo = LyricParser.initLyric(is, "utf-8");
	StringBuffer stringBuffer = new StringBuffer();
	if(lyricInfo != null && lyricInfo.getLines() != null) {
		int size = lyricInfo.getLines().size();
		for (int i = 0; i < size; i ++) {
			stringBuffer.append(lyricInfo.getLines().get(i).getContent() + "\n");
		}
		tv_lyric.setText(stringBuffer.toString());
	}else{
		tv_lyric.setText("解析失敗");
	}

} catch (IOException e) {
	e.printStackTrace();
	tv_lyric.setText("解析失敗");
}
複製程式碼

這裡就很簡單了,不再累贅,注意一下使用StringBuilder拼接每行的歌詞內容,每次拼接完成後加換行,才能出現我們想要的結果。

再看下效果:

這裡寫圖片描述

下一篇,關於展示歌詞的自定義View的文章,我會抓緊時間釋出,敬請期待!

想要獲取更多精彩內容,您還可以關注我的微信公眾號——Android機動車

歌詞顯示控制元件的實現上——歌詞解析

相關文章