Android音乐播放器中的歌词同步学习分析

时间:2022-11-03 10:15:14

在网上查了一下资料,感谢 http://www.cr173.com/html/20184_1.html 给了我思路,可以说他提供了最基本的歌词同步的功能,我在其上面添加了自己的修改的代码。

主要是自己为了实现歌词同步,并且通过移动seekbar,改变歌曲的歌词位置。当然还有自己不一样的地方。

首先歌词播放,是要一个子线程来操作,这个子线程负责在找到两段歌词之间的时间差,然后显示当前正在播放的歌词。

1

歌词部分

1.歌词的格式为.lrc 这是有一定格式的,最重要的是[MM:ss,mm]

以白玫瑰.lrc歌词为例

Android音乐播放器中的歌词同步学习分析

需要对歌词进行解析,歌词的实体类。

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()

这个方法,这是中文转化为拼音的方法。百度即可。