1. 程式人生 > >android 音樂播放器關於歌詞的處理

android 音樂播放器關於歌詞的處理

         當我們製作音樂播放器中我覺得歌詞的處理是比較難的一塊, 對於音樂播放和媒體控制我們可以使用MediaPlayer來搞定,它提供了媒體控制的介面,使得我們對於媒體控制來說變得比較簡單。但對於顯示歌詞來說就比較複雜了一點,例如讓歌詞一個字一個字高亮、快進時控制歌詞處理或者倍速播放時歌詞的處理等等, 這裡我想介紹簡單讓一行歌詞高亮顯示, 等這行歌詞唱完,讓下一行歌詞高亮顯示。

1. 解析歌詞檔案

常見的歌詞檔案有:.lrc 和 .txt格式, 內容格式為:[00:02.59] 飄洋過海來看你

所以我們先要去解析歌詞檔案, 定義一個類去儲存每行解析出來的資料。

public class Lyric {
	public String lricString;
	public int sleepTime;
	public int timePoint;
}
我們對照著 "[00:02.59] 飄洋過海來看你" 來看, lrcString儲存的是"飄洋過海來看你", sleepTime儲存的是這句歌詞播放時間, 就是下一句歌詞開始播的時間減去本句歌詞播放的時間,timePoint就是將 “[00:02.59]” 時間文字解析出來轉換成的秒數。

現在我們開始解析歌詞檔案然後將資料儲存到Lyrc中

public class LrcUtils {
	private static List<Lyric> lyricList;

	/**
	 * 讀取檔案
	 */
	public static List<Lyric> readLRC(File f) {
		try {
			if (f == null || !f.exists()) {
				lyricList = null;
			} else {
				lyricList = new Vector<Lyric>();
				InputStream is = new BufferedInputStream(new FileInputStream(f));
				BufferedReader br = new BufferedReader(new InputStreamReader(
						is, getCharset(f)));
				String strTemp = "";
				while ((strTemp = br.readLine()) != null) {
					strTemp = <span style="font-family:Arial, Helvetica, sans-serif;font-size:10px;">processLRC</span>(strTemp);
				}
				br.close();
				is.close();
				// 對歌詞進行排序
				Collections.sort(lyricList, new Sort());
				// 計算每行歌詞的停留時間
				for (int i = 0; i < lyricList.size(); i++) {

					Lyrc one = lyricList.get(i);
					if (i + 1 < lyricList.size()) {
						Lyric two = lyricList.get(i + 1);
						one.sleepTime = two.timePoint - one.timePoint;
					}
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return lyricList;
	}

	/**
	 * 處理一行內容
	 */
	private static String processLRC(String text) {
		try {
			int pos1 = text.indexOf("[");
			int pos2 = text.indexOf("]");

			if (pos1 >= 0 && pos2 != -1) {
				Long time[] = new Long[getPossiblyTagCount(text)];
				time[0] = timeToLong(text.substring(pos1 + 1, pos2));
				if (time[0] == -1)
					return "";
				String strLineRemaining = text;
				int i = 1;
				while (pos1 >= 0 && pos2 != -1) {

					strLineRemaining = strLineRemaining.substring(pos2 + 1);
					pos1 = strLineRemaining.indexOf("[");
					pos2 = strLineRemaining.indexOf("]");
					if (pos2 != -1) {
						time[i] = timeToLong(strLineRemaining.substring(
								pos1 + 1, pos2));
						if (time[i] == -1)
							return ""; // LRCText
						i++;
					}
				}

				Lyric tl = null;
				//防止有的歌詞檔案是這種格式:[00:01:23][00:03:02]重複歌詞
				//就是歌詞重複的放在一起,將多個時間戳放在一起,所以在解析完歌詞需要排序一下。
				for (int j = 0; j < time.length; j++) {
					if (time[j] != null) {
						tl = new Lyric();
						tl.timePoint = time[j].intValue();
						tl.lricString = strLineRemaining;
						lyrcList.add(tl);
					}
				}
				return strLineRemaining;
			} else
				return "";
		} catch (Exception e) {
			return "";
		}
	}
        //獲取一行中的時間標籤的個數,為了防止將重複歌詞放在一行上顯示
	private static int getPossiblyTagCount(String Line) {
		String strCount1[] = Line.split("\\[");
		String strCount2[] = Line.split("\\]");
		if (strCount1.length == 0 && strCount2.length == 0)
			return 1;
		else if (strCount1.length > strCount2.length)
			return strCount1.length;
		else
			return strCount2.length;
	}

	/**
	 * 時間轉換,將time格式時間轉換成秒
	 */
	public static long timeToLong(String Time) {
		try {
			String[] s1 = Time.split(":");
			int min = Integer.parseInt(s1[0]);
			String[] s2 = s1[1].split("\\.");
			int sec = Integer.parseInt(s2[0]);
			int mill = 0;
			if (s2.length > 1)
				mill = Integer.parseInt(s2[1]);
			return min * 60 * 1000 + sec * 1000 + mill * 10;
		} catch (Exception e) {
			return -1;
		}
	}

	/**
	 * 判斷檔案編碼,防止檔案解析成亂碼
	 */
	public static String getCharset(File file) {
		String charset = "GBK";
		byte[] first3Bytes = new byte[3];
		try {
			boolean checked = false;
			BufferedInputStream bis = new BufferedInputStream(
					new FileInputStream(file));
			bis.mark(0);
                       //一般讀取前3個位元組就可以判斷檔案的編碼格式
			int read = bis.read(first3Bytes, 0, 3);
			if (read == -1)
				return charset;
			if (first3Bytes[0] == (byte) 0xFF && first3Bytes[1] == (byte) 0xFE) {
				charset = "UTF-16LE";
				checked = true;
			} else if (first3Bytes[0] == (byte) 0xFE
					&& first3Bytes[1] == (byte) 0xFF) {
				charset = "UTF-16BE";
				checked = true;
			} else if (first3Bytes[0] == (byte) 0xEF
					&& first3Bytes[1] == (byte) 0xBB
					&& first3Bytes[2] == (byte) 0xBF) {
				charset = "UTF-8";
				checked = true;
			}
			bis.reset();
			if (!checked) {
				int loc = 0;
				while ((read = bis.read()) != -1) {
					loc++;
					if (read >= 0xF0)
						break;
					if (0x80 <= read && read <= 0xBF)
						break;
					if (0xC0 <= read && read <= 0xDF) {
						read = bis.read();
						if (0x80 <= read && read <= 0xBF)
							continue;
						else
							break;
					} else if (0xE0 <= read && read <= 0xEF) {
						read = bis.read();
						if (0x80 <= read && read <= 0xBF) {
							read = bis.read();
							if (0x80 <= read && read <= 0xBF) {
								charset = "UTF-8";
								break;
							} else
								break;
						} else
							break;
					}
				}
			}
			bis.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return charset;
	}
        //按照timePoint的大小進行升序排列
	private static class Sort implements Comparator<Lyrc> {
		public Sort() {
		}

		public int compare(Lyric tl1, Lyric tl2) {
			return sortUp(tl1, tl2);
		}

		private int sortUp(Lyric tl1, Lyric tl2) {
			if (tl1.timePoint < tl2.timePoint)
				return -1;
			else if (tl1.timePoint > tl2.timePoint)
				return 1;
			else
				return 0;
		}
	}
}

我們可以直接使用LrcUtils類, 匯入到你的工程中,呼叫LrcUtils.readLRC(File);方法傳入歌詞檔案就會返回解析的歌詞資料. 

下面介紹LrcUtils類中的邏輯:

(1)首先從歌詞檔案中讀取資料(按行讀取, 在讀取資料時呼叫方法getCharset獲取檔案的編碼格式防止讀取資料出現亂碼), 每讀取一行呼叫processLRC去將此行資料解析儲存到Lyrc中.

(2)在processLRC中處理一行中多個時間戳的邏輯, 因為有的歌詞檔案為了方便把重複的歌詞的時間放在一起。

(3)將獲取的解析資料按照timePoint排序, 因為防止(2)中描述的情況,重複歌詞的時間放在一起的問題,如果不排序的話,會導致歌詞順序亂套.

(4)計算每行歌詞停留的時間

什麼是為了防止重複歌詞放在一行上?

歌詞是這樣的:

[02:17.62][00:27.46]為你我用了半年的積蓄
[02:21.05][00:31.99]飄洋過海的來看你
[02:24.81][00:35.60]為了這次相聚
[02:27.59][00:38.16]我連見面時的呼吸都曾反覆練習
[02:33.48][00:43.79]言語從來沒能將我的情誼表達千萬分之一
[02:40.91][00:51.47]為了這個遺憾
[02:44.19][00:54.65]我在夜裡想了又想不肯睡去
[02:50.40][01:00.88]記憶它總是慢慢的積累
[02:54.50][01:04.92]在我心中無法抹去
[02:58.22][01:07.93]為了你的承諾
[03:00.77][01:10.72]我在最絕望的時候都忍住不哭泣
[03:08.46][01:17.06]陌生的城市啊!
[03:12.81][01:22.20]熟悉的角落裡
[03:16.63][01:26.99]也曾彼此安慰
[03:19.31][01:30.13]也曾相擁嘆息
[03:21.36][01:31.72]不管將會面對什麼樣的結局
[03:58.95][03:25.36][01:35.83]在漫天風沙裡 望著你遠去
[04:02.66][03:29.38][01:39.70]我竟悲傷的不能自己
[04:06.82][03:34.02][01:43.83]多盼能送君千里
[04:09.25][03:35.95][01:46.15]直到山窮水盡
[04:11.46][03:38.51][01:58.96][01:48.44]一生和你相依

一句歌詞前面多個時間tag.

2. 編寫自定義TextView 去顯示歌詞

       (1)繼承TextView, 處理onDraw方法

        (2)歌詞繪製,區分當前行與普通行, 將當前行繪製在控制元件中心。指定兩種Paint,來繪製兩種不同文字。

        (3)每隔一個時間段(就是Lyrc中的sleepTime欄位)更新顯示內容,向上滾動

public class LricView extends TextView {

	private List<Lyric> lyricList;
	// 標記當前行
	private int currentLine = 0;
	private Paint currentPaint;
	private Paint otherPaint;
	private int currentColor = Color.GREEN;
	private int currentTextSize = 18;
	
	private int otherColor = Color.BLACK;
	private int otherTextSize = 15;
	
	// 行間距
	private int lineSpace = 25;
	//當前歌詞字型
	private Typeface currentTypeface = Typeface.DEFAULT_BOLD;
	//其他歌詞字型
	private Typeface otherTypeface = Typeface.SERIF;

	private Handler handler = new Handler() {

		@Override
		public void handleMessage(Message msg) {
			invalidate(); // 重新整理,會再次呼叫onDraw方法
			super.handleMessage(msg);
		}

	};

	public LricView(Context context, AttributeSet attrs) {
		super(context, attrs);
		currentPaint = new Paint();
		otherPaint = new Paint();
		lyricList = LrcUtils.readLRC(new File("/data/local/tmp/123456.lrc"));

		currentPaint.setColor(currentColor);
		currentPaint.setTextSize(currentTextSize);
		currentPaint.setTextAlign(Align.CENTER); // 畫在中間
		currentPaint.setTypeface(currentTypeface);
		
		otherPaint.setColor(otherColor);
		otherPaint.setTextSize(otherTextSize);
		otherPaint.setTextAlign(Align.CENTER);
		otherPaint.setTypeface(otherTypeface);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		if (lyricList != null && currentLine < lyricList.size()) {
			Lyric lyrc = null;
			//繪製播放過的歌詞
			for (int i = currentLine - 1; i >= 0; i--) {
				lyric = lyricList.get(i);
			canvas.drawText(lyrc.lricString, getWidth() / 2,
					getHeight() / 2 + lineSpace * (i - currentLine), otherPaint);
			}
			lyric = lyrcList.get(currentLine);
			// 繪製正在播放的歌詞
			canvas.drawText(lyrc.lricString, getWidth() / 2,
					getHeight() / 2, currentPaint);
                        //繪製未播放的歌詞
			for (int i = currentLine + 1; i < lyrcList.size(); i++) {
				lyric = lyricList.get(i);
				canvas.drawText(lyrc.lricString, getWidth() / 2,
						getHeight() / 2 + lineSpace * (i - currentLine), otherPaint);
			}
			lyric = lyricList.get(currentLine);
			handler.sendEmptyMessageDelayed(10, lyrc.sleepTime);
			currentLine++;
		} else {
			canvas.drawText("未找到歌詞", getWidth() / 2,
					getHeight() / 2, currentPaint);
		}
		super.onDraw(canvas);
	}

}

在自定義LricView中呼叫LrcUtils.readLRC方法傳入歌詞檔案獲取歌詞資訊,然後通過handler去控制多長時間進行繪製.  LricView 可以直接使用.

3. 使用LricView

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" >

    <com.example.lrcdemo.LricView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

在佈局檔案中直接使用即可.

最終實現結果:


自此歌詞處理完成, 貼出的程式碼都是可以直接使用參考, 關於快進快退音樂和倍速播放,歌詞的處理邏輯就是修改lyric中的SleepTime, 不過邏輯也挺繞的.  這個歌詞處理邏輯是我在做音樂播放器時參考學習別的教程, 在這裡整理出來給大家參考, 如果有任何問題可以留言.