最近打算仿網易雲音樂的音樂播放器,除了網路框架、介面資料等這些外,最核心的就是音樂的播放和歌詞的顯示。
考慮到歌詞顯示控制元件涉及到歌詞解析,自定義控制元件的實現等等諸多方面,可能文章的篇幅上會比較冗長,同時也為了方便自己和碼友們能夠根據自己的需求和愛好各取所需,將《歌詞顯示控制元件的實現上》這篇文章分成上、下兩篇,分別是《歌詞顯示控制元件的實現上——歌詞解析》和《歌詞顯示控制元件的實現下——歌詞展示自定義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機動車!