初探<audio>标签实现同步显示歌词

提醒:本文最后更新于 4304 天前,文中所描述的信息可能已发生改变,请谨慎使用。

今天群里的小胖说用<audio>标签写了个听评书的网页给父母用,技术实在是低级真心非常有孝心的说!然后我也顺带去了解了一下audio标签现在的支持情况,一不小心就脑补了一些新知识。看到了audio标签有duration属性和currentTime属性,分别用来读取歌曲总时间和当前播放时间,正好弥补了我之前想写播放器同步显示歌词一直卡在的如何获取播放时间的问题上。所以就开始操刀了。

歌曲和歌词的准备

我暂时是写到了之前的虾米搜索页面里面,实现搜索获得歌曲用audio标签播放并显示歌词。所以歌曲和歌词文件自然就来自盗版帝国虾米了。由于虾米的歌曲文件都是MP3格式的,所以悲剧如FireFox等虽然支持了HTML5但是不支持MP3格式的文件这让我也是感到很捉急的,所以暂时是推荐大家使用Chrome浏览器制作并访问的。关于目前各浏览器的支持情况可以看这里:居然连IE都支持了!这里利用了一下虾米的API:http://www.xiami.com/song/playlist/id/歌曲ID 获取歌曲的相关信息,其中就有歌曲地址和歌词。获取到的XML文件中,歌曲地址是加密放置的,至于解析原理,大家可以参见虾米音乐文件绝对地址解析。详细的解释一下就是:

获取到的字符串第一个字母是矩阵的行数,除去第一个字母的字符总数除以行数得到的整数是每行放置多少个字符,余数则是前余数行要在末尾多放置一个字符。这样就能组成引文中的矩阵了。按照竖行读取,^替换为0的处理后就可以获得绝对地址了。

懒惰如我为了方便自己,我把解析的过程写成了函数。最开始我是将字符串打散成为二维数组,然后嵌套循环连接字符串。后来觉得打散成一位数组应该会更方便一点,所以为了算获取数组索引顺序的规律,死了好多个脑细胞啊,果然数学已经忘到姥姥家了。

歌词由于后期要用js处理,js又不能跨域获取什么的。所以只能劳烦PHP获取到歌词地址后远程抓取过来了。以上解析得到绝对地址和歌词都是在服务器端完成的。我写成了API的形式,https://imnerd.org/lab/search/api.php?id=歌曲ID可以查看获取到的内容格式。

audio歌曲播放与暂停

果然我还是对解码什么的比较感兴趣,一不小心就偏题了呢。好啦好啦,以上都不应该是本文的重点啦,下面我们开始好好的蹂躏学习一下audio标签啦。audio标签的简单写法为

<audio src="歌曲地址"></audio>

控制歌曲的播放和暂停也是非常简单的,只要利用play()和pause()事件就好了,代码如下:

m = document.getElementsByTagName('audio')[0];
m.play(); //歌曲的播放
m.pause(); //歌曲的暂停

想要了解更多audio标签的属性和事件的话可以点击这里:HTML 5 视频/音频参考手册

同步显示歌词的实现

我在开头已经讲了,audio的currentTime属性可以获取当前播放时间。又如果你刚才有去点击上面的参考手册的链接的话,你就会发现audio还有一个神奇的timeupdate事件,目的是当目前的播放位置已更改时做出动作。也就是说我们可以利用timeupdate事件在currentTime变化的时候做出判断,然后放置相应的歌词。

基本方法大概就是这个样子,播放器的当前播放时间我们已经获取到了,剩下的就是获取歌词的播放时间的问题了。由于标准的歌词每行应该是“[你好我是播放时间,我的格式是分:秒]你好我是该播放时间下的歌词”这种格式的,想必大家早就了解了吧。利用正则获取split分割等一系列方法将歌词分割成一个二维数组,一纬为每行歌词,二维为每行歌词的时间和每行歌词的内容。其中歌词的时间由于是分钟:秒的形式,需要转化为单位为秒的数字。下面给出歌词转化为二维数组的方法。方法可能坑爹不是最好的,但是基本凑合,这一部分大家可以随意发挥:

//写成函数的形式大家可能会更好理解一点
function parseLyric(text) {
lyric = text.split('\r\n'); //先按行分割
var _l = lyric.length; //获取歌词行数
lrc = new Array(); //新建一个数组存放最后结果
for(i=0;i<_l;i++) {
	var d = lyric[i].match(/\[\d{2}:\d{2}((\.|\:)\d{2})\]/g);  //正则匹配播放时间
	var t = lyric[i].split(d); //以时间为分割点分割每行歌词,数组最后一个为歌词正文
	if(d != null) { //过滤掉空行等非歌词正文部分
		//换算时间,保留两位小数
		var dt = String(d).split(':'); //不知道为什么一定要转换时间为字符串之后才能split,难道之前正则获取的时间已经不是字符串了么? 
		var _t = Math.round(parseInt(dt[0].split('[')[1])*60+parseFloat(dt[1].split(']')[0])*100)/100; //这一步我自己都觉得甚是坑爹啊!
		lrc.push([_t, t[1]]);
	}
return lrc;
}

这样我们只要匹配歌词每行的时间和crrentTime就行了,如果当前播放时间超过了这一句歌词的时间,则显示这一句歌词,否则不显示。这是我们下面代码的关键判断。另外,这里我使用了一个自我感觉还不错的小技巧:显示完歌词之后立即将这行歌词从歌词数组中剔除出去,这样我们就永远只要将currentTime和lrc[1][0]比较就好了,而不用麻烦的去每个都去比较一下。

m.addEventListener('timeupdate', function() {
	if(audio.currentTime > lrc[1][0]) {
		document.getElementById('lrc').innerHTML = lrc[1][1];
		lrc.shift();
	}
},false);
//addEventListener等同于m.timeupdate(function() {...});

歌曲播放并显示歌词的思路大概就这个样子了,具体的代码可以去看我的DEMO。DEMO只是给出了大概的功能,UI方面都还木有美化,大家习惯就好,这个是在是力不从心啊!本人一向也不是很在意UI的。最后的最后,感谢一下Jclyn帮我做了测试并推荐了本文开头的歌曲。哦,又突然想起来了,DEMO的范例是用JQuery操作的,虽然跟Javascript有些不同,但是对于思路什么的是没有问题的,有不懂的可以留言提问哦,我会看心情耐心解答的。

Avatar
怡红公子 擅长前端和 Node.js 服务端方向。热爱开源时常在 Github 上活跃,也是博客爱好者,喜欢将所学内容总结成文章分享给他人。

0 评论