1. 程式人生 > >Android音樂播放器中的歌詞同步學習分析

Android音樂播放器中的歌詞同步學習分析

在網上查了一下資料,感謝 http://www.cr173.com/html/20184_1.html 給了我思路,可以說他提供了最基本的歌詞同步的功能,我在其上面添加了自己的修改的程式碼。

主要是自己為了實現歌詞同步,並且通過移動seekbar,改變歌曲的歌詞位置。當然還有自己不一樣的地方。

首先歌詞播放,是要一個子執行緒來操作,這個子執行緒負責在找到兩段歌詞之間的時間差,然後顯示當前正在播放的歌詞。

1

歌詞部分

1.歌詞的格式為.lrc 這是有一定格式的,最重要的是[MM:ss,mm]

以白玫瑰.lrc歌詞為例


需要對歌詞進行解析,歌詞的實體類。

MyLrc.java

public class MyLrc implements Comparable<Object>{
	private int time;
	private String lyric;
	public int getTime() {
		return time;
	}
	public void setTime(int time) {
		this.time = time;
	}
	public String getLyric() {
		return lyric;
	}
	public void setLyric(String lyric) {
		this.lyric = lyric;
	}
	//放在set集合中可以看下面要求進行排序
	@Override
	public int compareTo(Object arg0) {
		int later=0;
		if(arg0 instanceof MyLrc)
		{
			later=((MyLrc)arg0).getTime();
		}
		return this.time-later;
	}
	@Override
	public String toString() {
		return this.time+""+this.lyric;
	}
	
}

 實現comparaTo的方法的目的是在把物件放入TreeSet中的時候,按照歌詞時間的循序放入,方便之後拿出來。比較使用(我覺得這部很重要,因為後期需要判斷拉動SeekBar的時候找到對應的時間的位置)

LrcUtil.java

//對歌詞進行解析
public class LrcUtil {

	private static TreeSet<MyLrc> tree;

	// 將對應的lrc檔案轉化為treeMap,分別對應的時間以及歌詞
	public LrcUtil(InputStream musicTitle) {
		TreeSet<MyLrc> treeset = new TreeSet<MyLrc>();
		// 用來存放歌曲的時間和對應的歌詞
		InputStreamReader inReader = null;
		BufferedReader reader = null;
		try {
			inReader = new InputStreamReader(musicTitle);
			reader = new BufferedReader(inReader);
			String line = "";
			while ((line = reader.readLine()) != null) {
				// 對那行歌詞進行分割,判斷,然後儲存
				String[] substr = line.split("\\]");
				for (String ss : substr) {
					if (ss.contains("[") && ss.contains(":")
							&& ss.contains(".")) {
						String sss = ss.replaceAll("\\[", "");
						String[] timeStart = sss.split(":");
						String[] timeEnd = timeStart[1].split("\\.");
						// 計算出當前的時間的毫秒數
						int time = (Integer.valueOf(timeStart[0]) * 60 + Integer
								.valueOf(timeEnd[0]))
								* 1000
								+ Integer.valueOf(timeEnd[1]) * 10;
						// 對應的時間放一個對應的歌詞
						MyLrc lrc = new MyLrc();
						lrc.setTime(time);
						lrc.setLyric(substr[substr.length - 1]);
						treeset.add(lrc);
					}
				}
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				inReader.close();
				reader.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		tree = treeset;
	}
	//獲得lrc檔案的歌詞點的列表
	public List<String> getWords() {
		List<String> list = new ArrayList<String>();
		Iterator<MyLrc> it = tree.iterator();
		while (it.hasNext()) {
			MyLrc my = it.next();
			list.add(my.getLyric());
		}
		return list;
	}
    //獲得lrc檔案的時間點的列表
	public List<Integer> getTimes() {
		List<Integer> list = new ArrayList<Integer>();
		Iterator<MyLrc> it = tree.iterator();
		while (it.hasNext()) {
			MyLrc my = it.next();
			list.add(my.getTime());
		}
		return list;
	}
}

Lrc工具類在構造方法中傳入一個InputStream流,由於現在是本地測試,所以本地先放入lrc檔案。Android中可以把歌詞檔案放在Assets檔案中,但是有些歌是中文的,而Assets檔案中是不允許放中文的檔案的

顯示為非法名字,所以我打算把中文歌曲的名字轉化為拼音再查詢。

Activity中歌詞顯示的程式碼

根據當前歌曲的進度啟一個子執行緒,sleep一段時間,其他時間重新整理控制元件顯示。

private void initLrcThread(int currentPosition) {
		LrcUtil lrcUtil = null;
		try {
			lrcUtil = new LrcUtil(getAssets().open(Trans2PinYin.trans2PinYin(currentMusic.getTitle())+".lrc"));
			playing_text_lrc.init(Trans2PinYin.trans2PinYin(currentMusic.getTitle())+".lrc");
		} catch (IOException e) {
			ToastInfo(R.string.notfind_lrc);
		}
		if(lrcUtil==null)
			return;
		final List<Integer> lrctime = lrcUtil.getTimes();
		if(currentPosition>lrctime.get(lrctime.size()-1))
			return;
		int position = 0;
		for (int i = 0; i < lrctime.size()-1; i++) {
			if (currentPosition < lrctime.get(0)) {
				position = 0;
				break;
			} else if (currentPosition > lrctime.get(i)
					&& currentPosition < lrctime.get(i + 1)) {
				position = i;
				break;
			}
		}
		final int p = position;
		//找到對應位置的歌詞
		playing_text_lrc.changeIndex(p);
		// 起一個子執行緒進行歌詞顯示
		new Thread() {
			int i = p;

			public void run() {
				while (!mService.isPause()) {
					handler.post(new Runnable() {
						@Override
						public void run() {
							playing_text_lrc.invalidate();
						}
					});
					try {
						if (i == 0)
							Thread.sleep(lrctime.get(i));
						else {
							Thread.sleep(lrctime.get(i + 1) - lrctime.get(i));
						}
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					i++;
					//防止下標越界!
					if (i+1 >= lrctime.size())
						break;
				}
				;
			}
		}.start();
		
	}


這裡是對自定義控制元件顯示歌詞的操作,如果沒找到對應的lrc就打的Toast即可。

對於歌曲顯示的子執行緒。當歌曲在播放的時候。執行緒sleep一段時間後對自定義的歌詞控制元件進行重新整理。每次重新整理都會走空間中的ondraw的方法。

在ondraw方法中,重新整理一次就跳到下一個歌詞。

當然了,這裡通過SeekBar移動的時候可以得到當前歌曲的位置(時間點),通過這個時間點(currentPosition)找到對應的getTimes列表中的歌詞的位置

<span style="white-space:pre">	</span>for (int i = 0; i < lrctime.size()-1; i++) {
<span style="white-space:pre">				</span>if (currentPosition < lrctime.get(0)) {
<span style="white-space:pre">				</span>position = 0;
<span style="white-space:pre">				</span>break;
<span style="white-space:pre">				</span>} else if (currentPosition > lrctime.get(i)
<span style="white-space:pre">						</span>&& currentPosition < lrctime.get(i + 1)) {
<span style="white-space:pre">					</span>position = i;
<span style="white-space:pre">					</span>break;
<span style="white-space:pre">				</span>}
<span style="white-space:pre">	</span>}
並且同時改變
playing_text_lrc.changeIndex(p);

LRCTextView.java

public class LRCTextView extends TextView {
	private List<String> mWordsList = new ArrayList<String>();
	private Paint mLoseFocusPaint;
	private Paint mOnFocusePaint;
	private float mX = 0;
	private float mMiddleY = 0;
	private float mY = 0;
	private static final int DY = 50;
	private int mIndex = 0;
	private Context context;

	public LRCTextView(Context context) throws IOException {
		super(context);
		this.context = context;
	}

	public LRCTextView(Context context, AttributeSet attrs) throws IOException {
		super(context, attrs);
		this.context = context;
	}

	public LRCTextView(Context context, AttributeSet attrs, int defStyle)
			throws IOException {
		super(context, attrs, defStyle);
		this.context = context;
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);

		Paint p = mLoseFocusPaint;
		// 未走到init未找到歌詞
		if (p == null) {
			p=new Paint();
			p.setTextSize(50);
			canvas.drawText("未找到歌詞", mX-100, mMiddleY, p);
			return;
		}
		p.setTextAlign(Paint.Align.CENTER);
		Paint p2 = mOnFocusePaint;
		p2.setTextAlign(Paint.Align.CENTER);
		// 防止下表越界
		if (mIndex >= mWordsList.size())
			return;
		canvas.drawText(mWordsList.get(mIndex), mX, mMiddleY, p2);

		int alphaValue = 25;
		float tempY = mMiddleY;
		for (int i = mIndex - 1; i >= 0; i--) {
			tempY -= DY;
			if (tempY < 0) {
				break;
			}
			p.setColor(Color.argb(255 - alphaValue, 245, 245, 245));
			canvas.drawText(mWordsList.get(i), mX, tempY, p);
			alphaValue += 25;
		}
		alphaValue = 25;
		tempY = mMiddleY;
		for (int i = mIndex + 1, len = mWordsList.size(); i < len; i++) {
			tempY += DY;
			if (tempY > mY) {
				break;
			}
			p.setColor(Color.argb(255 - alphaValue, 245, 245, 245));
			canvas.drawText(mWordsList.get(i), mX, tempY, p);
			alphaValue += 25;
		}
		mIndex++;
	}

	@Override
	protected void onSizeChanged(int w, int h, int ow, int oh) {
		super.onSizeChanged(w, h, ow, oh);

		mX = w * 0.5f;
		mY = h;
		mMiddleY = h * 0.3f;
	}

	public void init(String musicName) throws IOException {
		setFocusable(true);

		LrcUtil lrcHandler = new LrcUtil(context.getAssets().open(musicName));
		mWordsList = lrcHandler.getWords();

		mLoseFocusPaint = new Paint();
		mLoseFocusPaint.setAntiAlias(true);
		mLoseFocusPaint.setTextSize(30);
		mLoseFocusPaint.setColor(Color.BLACK);
		mLoseFocusPaint.setTypeface(Typeface.SERIF);

		mOnFocusePaint = new Paint();
		mOnFocusePaint.setAntiAlias(true);
		mOnFocusePaint.setColor(Color.RED);
		mOnFocusePaint.setTextSize(50);
		mOnFocusePaint.setTypeface(Typeface.SANS_SERIF);
	}

	public void changeIndex(int i) {
		mIndex = i;
	}
這裡我稍微修改了這個方法,傳入一個changeIndex用於Seekbar被拉動時候,歌詞的改變。其他的與http://www.cr173.com/html/20184_1.html相似

而在每次歌曲切換或者進度條移動的時候呼叫這個方法即可

initLrcThread(currentPosition);

這裡用到了

Trans2PinYin.trans2PinYin(currentMusic.getTitle()

這個方法,這是中文轉化為拼音的方法。百度即可。