安卓音乐播放器中歌词同步问题
音乐文件是.lrc格式的,lrc格式的文件,是MP3播放器唯一能识别的歌词文件,在MP3播放器中可以去同步
显示歌词。它是一种包含着"[]"形式的"标签"的、基于纯文本的歌词专用格式。
三种编码的学习(GBK、GB2312、UTF-8):
GB2312是中国规定的汉字编码,也可以说是简体中文的字符集编码;
GBK是GB2312的扩展,除了兼容GB2312外,它还能显示繁体中文,还有日文的假名。
UTF-8和GBK的区别:
字符均使用双字节来表示,只不过区分中文,将其最高位都定成1.
至于UTF-8编码则是用以解决国际上字符的一种多字节编码,它对英文使用8位(即一个字节),中文
使用24位(三个字节)来编码。对于英文字符使用较多的论坛则用UTF-8节省空间。
GBK包括全部中文字符;UTF-8则包含全世界所有国家需要用到的字符。
UTF-8编码的文字可以在各国各种支持UTF-8字符集的浏览器上显示。
比如:如果是UTF-8编码,则在外国人的英文IE上也能显示中文,而无需它们下载的中文语言支持包。
UTF-8是国际编码,它的通用性较好,外国人也可以浏览论坛,GBK是国家编码,通用性比UTF-8差,
不过UTF-8占用的数据库比GBK大。
lrc歌词文本中含有两类标签:‘
1、标识标签(ID-tags):其格式为"[标识名:值]"。
2、时间标签(time-tag):形式为"mm:ss".当歌曲播放到达某一时间点时,MP3就会寻找对应的时间标签
并显示标签后面的歌词文本,这样就完成了"歌词同步"的功能。
但是LRC文件并不能做到词的同步,只能做到行的同步。
以一首歌曲为例,
[ti:回忆的沙漏]
[ar:邓紫棋]
[al:G.E.M.]
[by:]
[offset:0]
[00:02.50]回忆的沙漏 - G.E.M. 邓紫棋
[00:04.62]词:庭竹
[00:05.72]曲:G.E.M.
[00:15.03]拼图一片片失落
[00:18.56]像枫叶的冷漠
[00:21.87]墙上的钟
[00:23.79]默默数着寂寞
[00:29.30]咖啡飘散过香味
[00:33.06]剩苦涩陪着我
[00:36.68]想念的心
[00:39.44]埋葬我在深夜的脆弱
[00:44.21]无尽的苍穹
[00:46.15]满天的星座
[00:47.83]你的光亮一闪而过
[00:51.34]只想要记住这永恒的瞬间
[00:57.34]像流星的坠落
[01:01.34]灿烂夺去了轮廓
[01:05.78]这刹那过后
[01:07.60]世界只是 回忆的沙漏
[01:11.78]像流星的坠落
[01:15.73]绚丽地点亮了整个星空
[01:19.93]像你故事在我生命留下
[01:23.97]不褪色的伤口
[01:29.56]湖水守候着沉默
[01:33.03]等待天边的月
[01:36.45]孤独的水面
[01:38.67]却漆黑整夜
[01:44.12]夜雾凝结的泪光
[01:47.64]被蒸发在角落
[01:51.22]他无情地
[01:54.09]遗忘我在追忆的漩涡
[01:58.82]无尽的苍穹
[02:00.58]满天的星座
[02:02.29]你的光亮
[02:03.61]一闪而过
[02:05.99]只想要记住这永恒的瞬间
[02:11.85]像流星的坠落
[02:15.83]灿烂夺去了轮廓
[02:20.33]这刹那过后
[02:22.25]世界只是 回忆的沙漏
[02:26.33]像流星的坠落
[02:30.25]绚丽地点亮了整个星空
[02:34.56]像你故事在我生命留下
[02:38.50]不褪色的伤口
[02:41.90]在黑夜的尽头
[02:43.90]是你的捉弄
[02:45.77]和无声的伤痛
[02:49.07]燃烧过后 只剩静默
[03:00.74]像流星的坠落
[03:04.96]灿烂夺去了轮廓
[03:09.36]这刹那过后
[03:11.10]世界只是 回忆的沙漏
[03:16.73]流星坠落
[03:19.35]绚丽地点亮了整个星空
[03:23.58]像你故事在我生命留下
[03:27.58]不褪色的伤口
ar(artist) 艺人名,ti(title)歌曲名,al(album)专辑名,by()编者(指编辑LRC歌词的人)
offset()时间补偿值,其单位是毫秒,正值表示整体提前,负值相反。
歌词中的时间格式比较同意:[00:00.20]等,00:表示分钟,00:表示秒数,.50表示毫秒数,
最后将它们转化为毫秒数处理才比较方便。
转:http://www.cr173.com/html/20184_1.html
最近在做一款android手机上的音乐播放器,学习到了很多东西,像是Fragment,ActionBar的使用等等,这里就先介绍一下歌词同步的实现问题。
歌词同步的实现思路很简单:获取歌词文件LRC中的时间和歌词内容,然后在指定的时间内播放相应的内容。获取不难,难就在于如何在手机屏幕上实现歌词的滚动。
先上效果图:
先从最基本的读取歌词文件开始:
Public class LrcHandle { private List mWords = new ArrayList(); private List mTimeList = new ArrayList(); //处理歌词文件 public void readLRC(String path) { File file = new File(path); try { FileInputStream fileInputStream = new FileInputStream(file); InputStreamReader inputStreamReader = new InputStreamReader( fileInputStream, "utf-8"); BufferedReader bufferedReader = new BufferedReader( inputStreamReader); String s = ""; while ((s = bufferedReader.readLine()) != null) { addTimeToList(s); if ((s.indexOf("[ar:") != -1) || (s.indexOf("[ti:") != -1) || (s.indexOf("[by:") != -1)) { s = s.substring(s.indexOf(":") + 1, s.indexOf("]")); } else { String ss = s.substring(s.indexOf("["), s.indexOf("]") + 1); s = s.replace(ss, ""); } mWords.add(s); } bufferedReader.close(); inputStreamReader.close(); fileInputStream.close(); } catch (FileNotFoundException e) { e.printStackTrace(); mWords.add("没有歌词文件,赶紧去下载"); } catch (IOException e) { e.printStackTrace(); mWords.add("没有读取到歌词"); } } public List getWords() { return mWords; } public List getTime() { return mTimeList; } // 分离出时间 private int timeHandler(String string) { string = string.replace(".", ":"); String timeData[] = string.split(":"); // 分离出分、秒并转换为整型 int minute = Integer.parseInt(timeData[0]); int second = Integer.parseInt(timeData[1]); int millisecond = Integer.parseInt(timeData[2]); // 计算上一行与下一行的时间转换为毫秒数 int currentTime = (minute * 60 + second) * 1000 + millisecond * 10; return currentTime; } private void addTimeToList(String string) { Matcher matcher = Pattern.compile( "\\[\\d{1,2}:\\d{1,2}([\\.:]\\d{1,2})?\\]").matcher(string); if (matcher.find()) { String str = matcher.group(); mTimeList.add(new LrcHandle().timeHandler(str.substring(1, str.length() - 1))); } } }
一般歌词文件的格式大概如下:
[ar:艺人名]
[ti:曲名]
[al:专辑名]
[by:编者(指编辑LRC歌词的人)]
[offset:时间补偿值] 其单位是毫秒,正值表示整体提前,负值相反。这是用于总体调整显示快慢的。
但也不一定,有时候并没有前面那些ar:等标识符,所以我们这里也提供了另一种解析方式。
歌词文件中的时间格式则比较统一:[00:00.50]等等,00:表示分钟,00.表示秒数,.50表示毫秒数,当然,我们最后是要将它们转化为毫秒数处理才比较方便。
处理完歌词文件并得到我们想要的数据后,我们就要考虑如何在手机上滚动显示我们的歌词并且与我们得到的时间同步了。
先是布局文件:
<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" >
<Button
android:id="@+id/button"
android:layout_width="60dip"
android:layout_height="60dip"
android:text="@string/停止" />
<com.example.slidechange.WordView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/button" />
WordView是自定义的TextView,它继承自TextView:
public class WordView extends TextView {
private List mWordsList = new ArrayList();
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;
public WordView(Context context) throws IOException {
super(context);
init();
}
public WordView(Context context, AttributeSet attrs) throws IOException {
super(context, attrs);
init();
}
public WordView(Context context, AttributeSet attrs, int defStyle)
throws IOException {
super(context, attrs, defStyle);
init();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.BLACK);
Paint p = mLoseFocusPaint;
p.setTextAlign(Paint.Align.CENTER);
Paint p2 = mOnFocusePaint;
p2.setTextAlign(Paint.Align.CENTER);
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;
}
@SuppressLint("SdCardPath")
private void init() throws IOException {
setFocusable(true);
LrcHandle lrcHandler = new LrcHandle();
lrcHandler.readLRC("/sdcard/陪我去流浪.lrc");
mWordsList = lrcHandler.getWords();
mLoseFocusPaint = new Paint();
mLoseFocusPaint.setAntiAlias(true);
mLoseFocusPaint.setTextSize(22);
mLoseFocusPaint.setColor(Color.WHITE);
mLoseFocusPaint.setTypeface(Typeface.SERIF);
mOnFocusePaint = new Paint();
mOnFocusePaint.setAntiAlias(true);
mOnFocusePaint.setColor(Color.YELLOW);
mOnFocusePaint.setTextSize(30);
mOnFocusePaint.setTypeface(Typeface.SANS_SERIF);
}
}
最主要的是覆盖TextView的onDraw()和onSizeChanged()。
在onDraw()中我们重新绘制TextView,这就是实现歌词滚动实现的关键。歌词滚动的实现思路并不复杂:将上一句歌词向上移动,当前歌词字体变大,颜色变黄突出显示。
我们需要设置位移量DY = 50。颜色和字体大小我们可以通过设置Paint来实现。
我们注意到,在我实现的效果中,距离当前歌词越远的歌词,就会变透明,这个可以通过p.setColor(Color.argb(255 - alphaValue, 245, 245, 245))来实现。
接着就是主代码:
public class MainActivity extends Activity {
private WordView mWordView;
private List mTimeList;
private MediaPlayer mPlayer;
@SuppressLint("SdCardPath")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mPlayer.stop();
finish();
}
});
mWordView = (WordView) findViewById(R.id.text);
mPlayer = new MediaPlayer();
mPlayer.reset();
LrcHandle lrcHandler = new LrcHandle();
try {
lrcHandler.readLRC("/sdcard/陪我去流浪.lrc");
mTimeList = lrcHandler.getTime();
mPlayer.setDataSource("/sdcard/陪我去流浪.mp3");
mPlayer.prepare();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
}
final Handler handler = new Handler();
mPlayer.start();
new Thread(new Runnable() {
int i = 0;
@Override
public void run() {
while (mPlayer.isPlaying()) {
handler.post(new Runnable() {
@Override
public void run() {
mWordView.invalidate();
}
});
try {
Thread.sleep(mTimeList.get(i + 1) - mTimeList.get(i));
} catch (InterruptedException e) {
}
i++;
if (i == mTimeList.size() - 1) {
mPlayer.stop();
break;
}
}
}
}).start();
}
}
歌词的显示需要重新开启一个线程,因为主线程是播放歌曲的。
代码很简单,功能也很简单,最主要的是多多尝试,多多修改,就能明白代码的原理了。