『HTML5梦幻之旅』 - 跟随歌曲显示当前歌词

好像哪位老师曾说过,音乐是世界通用语言。是的,听不懂英文,但总能欣赏英文歌曲吧。

很早以前就想做个音乐播放器,但是由于跟随歌曲显示当前歌词的效果一直实现不了,所以我的想法一直无法实现。不过,最近创意不佳,没心情开发游戏了,于是闲下来搞点小发明。这次就先模仿一下手机QQ音乐中歌词显示效果。

恰巧,年末新歌蛮多的,于是我就选了一首比较好听的歌曲——手写的从前。

先看本次演示截图:

技术分享

演示地址:http://wyh.wjjsoft.com/demo/lyrics/

上面的演示地址可能有一半以上的人都打不开,因为这首虽然很好听,但是啊,有11MB.……建议大家要听的话还是去网上自己找吧。

好了不扯远了,进入主题吧~


一,解读歌词文件

一般而言,歌词文件的格式都是一个时间对一句歌词的,例如:

[ti:手写的从前]
[ar:周杰伦]
[t_time:(04:57)]
[00:03.23]周杰伦 - 手写的从前
[00:06.56]词:方文山 曲:周杰伦
[00:11.43]这风铃跟心动很接近 这封信还在怀念旅行
[00:21.80]路过的爱情都太年轻 你是我想要再回去的风景
[00:31.79]这别离被瓶装成秘密 这雏菊美的像诗句
[00:39.30]而我在风中等你的消息 等月光落雪地
[00:48.92]等枫红染秋季等相遇 我重温午后的阳光
[00:58.24]将吉他斜背在肩上 跟多年前一样
[01:05.71]我们轻轻的唱 去任何地方
[01:14.54]我看着你的脸 轻刷著和弦
[01:20.00]情人节卡片 手写的永远
[01:24.05]还记得广场公园 一起表演
[01:29.27]校园旁糖果店 记忆里在微甜
[01:34.99]我看着你的脸 轻刷著和弦
[01:40.31]初恋是整遍手写的从前
[01:44.50]还记得 那年秋天说了再见
[01:49.51]当恋情已走远 我将你深埋在心里面
[02:16.25]微风需要竹林 溪流需要蜻蜓
[02:18.59]乡愁般的离开 需要片片浮萍
[02:21.17]记得那年的雨季 回忆里特安静
[02:23.84]哭过后的决定 是否还能进行
[02:26.52]我傻傻等待 傻傻等春暖花开
[02:28.80]等终等于等明等白 等爱情回来
[02:31.40]青春属于表白 阳光属于窗台
[02:33.95]而我想我属于 一个拥有你的未来
…………………………
[03:39.42]我重温午后的阳光
[03:44.10]将吉他斜背在肩上 跟多年前一样
[03:51.54]我们轻轻的唱 去任何地方
[04:00.53]我看着你的脸 轻刷著和弦
[04:05.98]情人节卡片 手写的永远
[04:10.15]还记得广场公园 一起表演
[04:15.03]校园旁糖果店 记忆里在微甜
[04:20.97]我看着你的脸 轻刷著和弦
[04:26.26]初恋是整遍手写的从前
[04:30.44]还记得 那年秋天说了再见
[04:35.50]当恋情已走远 我将你深埋在心里面
不难发现,在这里,除了前三行,方括号代表的不是区间而是时间。在前三行中,方括号里的内容分别代表:歌曲名,演唱者,音乐长度。但是这些算不上歌词吧,所以真正要处理的就是前三行后的内容。但是看上去要解析这些东西会很麻烦,那怎么办呢?想用正则表达式,正则又不熟悉……于是我只好想些歪门邪道的方法了。

其实要解析一种格式,说白了就是找规律。找规律嘛,听说测智商时就要看找规律的能力……

前面也说了,格式大致就是一个时间对应一句歌词,简化一下就是:[time] lyrics [time2] lyrics2 ...

经过我努力地挖掘规律,终于发现了其中的奥秘:

1)我们先把所有歌词后面\n给去掉,这样一来,歌词就连成了一排;

2)每一条时间+歌词之间通过“[”来分隔;

3)每一条中的时间和歌词之间通过“]”来分割;

有了这些发现,接下来的工作就是把这些字符串转换成程序里的数据结构。这里我准备选择了JSON作为装载数据的结构。有了这个JSON,我们要通过时间取歌词就可以直接到这个JSON中找了。


OK,歌词这种肤浅的东西就被我粗略地解读了。该上代码了。


二,手写的代码

先来看最基本的HTML代码。为了方便,本次开发用到了lufylegend.js,希望了解此引擎的朋友可以看看我以前的文章,大多数都是关于它的。

<!DOCTYPE html>
<html>
<head>
	<title>Lyrics</title>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
	<script type="text/javascript" src="./lib/lufylegend-1.9.7.simple.min.js"></script>
	<script type="text/javascript" src="./lib/lufylegend.LoadingSample1-0.1.0.min.js"></script>
	<script type="text/javascript" src="./js/ytMain.js"></script>
</head>
<body>
<div id="mylegend"></div>
</body>
</html>
我们还用到了ytMain.js这个文件,在这个文件里,首先是初始化引擎和一系列准备工作:
LInit(50, "mylegend", 350, 550, main);

var datalist = {};
var music = null;

function main () {
	document.body.style.background = "black";
	document.body.style.margin = "0px";
	document.body.style.padding = "0px";
	if (LGlobal.mobile) {
		LGlobal.stageScale = LStageScaleMode.SHOW_ALL;
	}
	LGlobal.screen(LGlobal.FULL_SCREEN);

	var loadData = [
		{path : "./js/ytLyrics.js"},

		{name : "pic", path : "./resource/albumpic.jpg"},
		{name : "lyrics", path : "./resource/shou_xie_de_cong_qian.js"},
		{name : "music", path : "./resource/shou_xie_de_cong_qian.mp3"}
	];

	var loadingLayer = new LoadingSample1();
	addChild(loadingLayer);

	LLoadManage.load(
		loadData,
		function (p) {
			loadingLayer.setProgress(p);
		},
		function (r) {
			datalist = r;

			loadingLayer.remove();

			addBackgroundPic();
			addTitle();
			addAlbumPic();
			addMusic();
			addLyrics();
		}
	);
}

以上代码要做的就是首先使用LInit这个引擎内部函数初始化整个界面,并且进行全屏处理,然后通过静态类LLoadManage的load函数来加载图片和音乐,并将加载好的内容放到datalist中,然后调用addBackgroundPic,addTitle,addAlbumPic,addMusic,addLyrics这几个函数来添加显示对象和播放音乐。至于刚说到的LLoadManage和LInit等引擎中的用法可以参看lufylegend.js的API手册:

http://lufylegend.com/api/zh_CN/out/index.html

为了方便本地测试,我直接把歌词写到了shou_xie_de_cong_qian.js中。这么做可以避免使用ajax来读取文本文件而产生的本地运行时的问题。当然在实际应用中,歌词实质就是一个文本文件,是通过ajax等方式来读取这种文件,然后用我前面提到的类似的方法来解读文件。接下来先来看看shou_xie_de_cong_qian.js的内容:

var lyrics = "[ti:手写的从前][ar:周杰伦][t_time:(04:57)][00:03.23]周杰伦 - 手写的从前[00:06.56]词:方文山 曲:周杰伦[00:11.43]这风铃跟心动很接近 这封信还在怀念旅行[00:21.80]路过的爱情都太年轻 你是我想要再回去的风景[00:31.79]这别离被瓶装成秘密 这雏菊美的像诗句[00:39.30]而我在风中等你的消息 等月光落雪地[00:48.92]等枫红染秋季等相遇 我重温午后的阳光[00:58.24]将吉他斜背在肩上 跟多年前一样[01:05.71]我们轻轻的唱 去任何地方[01:14.54]我看着你的脸 轻刷著和弦[01:20.00]情人节卡片 手写的永远[01:24.05]还记得广场公园 一起表演[01:29.27]校园旁糖果店 记忆里在微甜…………";
图个方便,我就直接手动把歌词全部变成了一行。然后该到添加显示对象这一步了:

function addTitle () {
	var txt = new LTextField();
	txt.text = "手写的从前";
	txt.color = "white";
	txt.size = 25;
	txt.x = (LGlobal.width - txt.getWidth()) / 2;
	txt.y = 30;
	addChild(txt);
}

function addBackgroundPic () {
	var bmpd = new LBitmapData(datalist["pic"]);
	var bmp = new LBitmap(bmpd);
	bmp.scaleX = bmp.scaleY = 2;
	bmp.x = (LGlobal.width - bmp.getWidth()) / 2;
	bmp.y = (LGlobal.height - bmp.getHeight()) / 2;
	addChild(bmp);

	var curtain = new LSprite();
	curtain.graphics.drawRect(0, "", [0, 0, LGlobal.width, LGlobal.height], true, "black");
	curtain.alpha = 0.8;
	addChild(curtain);
}

function addAlbumPic () {
	var bmpd = new LBitmapData(datalist["pic"]);
	var bmp = new LBitmap(bmpd);
	bmp.scaleX = bmp.scaleY = 0.6;
	bmp.x = (LGlobal.width - bmp.getWidth()) / 2;
	bmp.y = 100;
	addChild(bmp);
}

function addMusic () {
	music = new LSound(datalist["music"]);
	music.play();
}

function addLyrics () {
	var lyricsLayer = new ytLyrics(music, getLyrics(lyrics));
	lyricsLayer.x = (LGlobal.width - lyricsLayer.getWidth()) / 2;
	lyricsLayer.y = 300;
	addChild(lyricsLayer);
}
这些函数中主要用到了lufylegend中的几个类:LTextField文本类,LBitmap和LBitmapData位图类,LSound音乐类,LSprite精灵类。具体用法请查阅API参考手册。在这里我们只谈addLyrics这个函数。这个函数中用到了我自己写的一个类ytLyrics,这个类派生自LSprite类,是个显示对象,作用就正如类名所示,是用来显示歌词的,有两个参数,第一个是当前播放的音乐,第二个是音乐的歌词。这个类留到过一会儿讲。

接下来是ytMain.js中解析歌词的函数:

function getLyrics (content) {
	var result = new Array();
	var cArr = content.split("[");
	cArr.shift();

	for (var i = 0; i < cArr.length; i++) {
		var o = cArr[i].split("]");

		if (o.length >= 2 && o[1] != "") {
			var tArr = o[0].split(":"), t = 0;

			if (tArr.length >= 2) {
				var mtArr = tArr[0].split(""), mt = 0;

				for (var k = 0; k < mtArr.length; k++) {
					if (Number(mtArr[k]) > 0) {
						mt += mtArr[k] * Math.pow(10, mtArr.length - k - 1);
					}
				}

				t += mt * 60;

				var stArr = tArr[1].split("."), intStArr = stArr[0].split(""), st = 0;

				for (var j = 0; j < intStArr.length; j++) {
					if (Number(intStArr[j]) > 0) {
						st += intStArr[j] * Math.pow(10, intStArr.length - j - 1);					
					}
				}

				t += Number(st + "." + stArr[1]);
			}

			result.push({time : t, content : o[1]});
		}
	}

	return result;
}

代码算不上长,但是本次研究到的精髓就在这里。在shou_xie_de_cong_qian.js中,我们已经定义了一个lyrics变量来装载歌词,因此我们目前传给这个函数的参数就是lyrics。在这里我主要用到了String的split来切割字符串,并循环由切割而得到的数组,将其继续解析下去,直到分出时间和歌词。由于歌词的格式不算太难,所以我就只分了两次就获取了时间和相应的歌词(第一次按“[”来分,第二次按“]”来分)。为了去除没有歌词的那三项,我通过判断第二次切割得到的数组中第二个元素是否为""来屏蔽掉没有歌词的那几项。大家可以仔细看一下代码,不难发现我把两次切割而得到的结果先放入JSON,然后再把JSON放到了result这个数组中,最后返回这个result数组。

也许你会发现我用到了t这个变量,这个变量也就是最后传到result中歌词对应的时间。为什么要另外弄一个t来显示时间呢?原因很简单,在HTML5 Audio中获取当前播放到的时间时,得到的是以秒作为单位,但是我们的歌词中时间的形式是[分:秒],所以我们就必须要把这种格式转换为以秒为单位的数字。实现方法就在上面提供的代码中。主要还是用split来切割这些时间字符串,然后得到以分为单位的数字,和以秒得到的数字。然后t = 以分得到的数字*60+以秒的数字,就能得到最后的结果了。

解析完歌词,再来看显示歌词的类ytLyrics,此物乃lufylegend中LSprite之后,得其父之显示属性,又另添己能,终可显示歌词也(译文:这个类是lufylegend引擎中LSprite的子类,有父类LSprite各项显示方面的属性和方法,再加上另外拓展的功能,最终就能显示歌词了):

function ytLyrics (music, lyricsList) {
	var s = this;
	LExtends(s, LSprite, []);

	s.index = 0;
	s.list = lyricsList;
	s.music = music;
	s.scrollY = 0;

	s.contentLayer = new LSprite();
	s.addChild(s.contentLayer);

	s.showContent();

	s.addEventListener(LEvent.ENTER_FRAME, s.loop);
}

ytLyrics.prototype.showContent = function () {
	var s = this;

	for (var i = 0; i < s.list.length; i++) {
		var txt = new LTextField();
		txt.text = s.list[i].content;
		txt.color = "white";
		txt.x = (LGlobal.width - txt.getWidth()) / 2;
		txt.y = s.contentLayer.getHeight() + 20;
		s.contentLayer.addChild(txt)
	}

	s.scrollY = 30;

	var maskLayer = new LSprite();
	maskLayer.graphics.drawRect(0, "", [0, 0, LGlobal.width, 200]);

	s.contentLayer.mask = maskLayer;

	s.contentLayer.filters = [new LDropShadowFilter()]
};

ytLyrics.prototype.loop = function (e) {
	var s = e.currentTarget;

	if (s.index >= s.list.length) {
		return;
	}

	var ct = s.music.getCurrentTime();

	var minT = s.list[s.index].time,
	maxT = ((s.index + 1) < s.list.length) ? s.list[s.index + 1].time : s.music.length;

	if (ct >= minT && ct <= maxT) {
		var preTxt = s.contentLayer.getChildAt(s.index - 1);

		if (preTxt) {
			preTxt.color = "white";

			LTweenLite.to(s.contentLayer, 1, {
				y : s.contentLayer.y - preTxt.getHeight() - 20
			});
		}

		var currentTxt = s.contentLayer.getChildAt(s.index);

		if (currentTxt) {
			currentTxt.color = "#33FF00";
		}

		s.index ++;
	}
};
代码也不长,主要用到了ENTER_FRAME时间轴事件来驱动滚动歌词。具体来说就是在时间轴时间中,不停地获取歌曲当前时间,然后在歌词列表中得到当前歌词对应的时间和下一条歌词的时间,比较歌曲的当前时间和当前歌词对应的时间、下一条歌词的时间,以此得到是否播放下一条歌词。显示方面还是用到了lufylegend,还是那句话,想要了解参考手册即可。


ok,运行代码就得到了咱们想要的效果。

三,源代码下载

上面讲了点精髓部分,其他的边角还需要大家自己来研究了,如果有任何不清除的地方,欢迎大家到本文下方留言。我会尽力回复各位的~

源代码下载地址:http://wyh.wjjsoft.com/downloads/lyrics.zip


本文到此就结束了,欢迎大家交流~

----------------------------------------------------------------

欢迎大家转载我的文章。

转载请注明:转自Yorhom‘s Game Box

http://blog.csdn.net/yorhomwang

欢迎继续关注我的博客

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。