『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

欢迎继续关注我的博客

时间: 2024-12-20 14:22:14

『HTML5梦幻之旅』 - 跟随歌曲显示当前歌词的相关文章

『HTML5梦幻之旅』 - 仿Qt示例Drag and Drop Robot(换装机器人)

起源 在Qt的示例中看到了一个有趣的demo,截图如下: 这个demo的名字叫Drag and Drop Robot,简单概括而言,在这个demo中,可以把机器人四周的颜色拖动到机器人的各个部位,比如说头,臂,身躯等,然后这个部位就会变成相应的颜色,类似于换装小游戏. 上图就是经过愚下的拖动颜色使其简略换装后的样子. 当然,拖动颜色使部件变色的功能并不难实现,关键在于这个机器人是动态的,我们要研究的就恰恰是这个机器人动画是怎么做出来的. 光凭两张图片我们无法知道这个动画到底是什么样子的,大家可以

『HTML5梦幻之旅』 - 炫酷的节日贺卡

刚过完春节,想必大家收到了各种祝福和贺卡吧-Y某我今年也为同学和家人准备了贺卡.不一样的是,我的贺卡可不是made from树,而是一行行代码凝聚而来的. 考虑到本次开发需要的功能不多,所以就没有用库件了,利用纯Html5 Canvas API来完成本次梦幻之旅:节日贺卡.虽然用到的Canvas API不多,但是效果还是蛮理想的- 首先上截图吧: 哎呀,看到了截图,各位是不是领悟了传说中的炫酷华丽(luàn qī bā zāo)? 测试地址:http://wyh.wjjsoft.com/demo

『HTML5梦幻之旅』 - 仿Qt演示样例Drag and Drop Robot(换装机器人)

起源 在Qt的演示样例中看到了一个有趣的demo.截图例如以下: 这个demo的名字叫Drag and Drop Robot,简单概括而言,在这个demo中,能够把机器人四周的颜色拖动到机器人的各个部位,比方说头.臂,身躯等.然后这个部位就会变成相应的颜色.相似于换装小游戏. 上图就是经过愚下的拖动颜色使其简略换装后的样子. 当然,拖动颜色使部件变色的功能并不难实现,关键在于这个机器人是动态的,我们要研究的就恰恰是这个机器人动画是怎么做出来的. 光凭两张图片我们无法知道这个动画究竟是什么样子的,

『HTML5梦幻之旅』 - 舞动色彩,Canvas下实现颜色动画

注:为了方便起见,本次开发用到了开源引擎lufylegend,官方地址如下:http://lufylegend.com/lufylegend 今天来学习下HTML5 Canvas颜色动画.什么是颜色动画呢?以我的理解就是以某种颜色过渡到另一种颜色.和这个效果有点类似:http://w3school.com.cn/tiy/t.asp?f=css3_animation1 上面的demo是用css3实现,而我们今天要用的是Canvas.Canvas并没有相关的API,所以要想实现这种效果,只有靠自己了

『HTML5挑战经典』是英雄就下100层-开源讲座(二)危险!英雄

本篇为<『HTML5挑战经典』是英雄就下100层-开源讲座>第二篇,需要用到开源引擎lufylegend,可以到这里下载: 下载地址:http://lufylegend.googlecode.com/files/lufylegend-1.7.1.rar API文档:http://lufylegend.com/lufylegend/api 却说我们的英雄能顺利地从天而降了,不过丝毫没有悬念,他一定会被摔死的,因为还没有跳板出现.我每次路过时都看到我们的英雄是边下降边大叫:help! help!然

NHibernate框架与BLL+DAL+Model+Controller+UI 多层架构十分相似--『Spring.NET+NHibernate+泛型』概述、知识准备及介绍(一)

原文://http://blog.csdn.net/wb09100310/article/details/47271555 1. 概述 搭建了Spring.NET+NHibernate的一个数据查询系统.之前没用过这两个框架,也算是先学现买,在做完设计之 后花了一周搭建成功了.其中,还加上了我的一些改进思想,把DAO和BLL之中相似且常用的增删改查通过泛型T抽象到了DAO和BLL的父类中,其DAO 和BLL子类只需继承父类就拥有了这些方法.和之前的一个数据库表(视图)对应一个实体,一个实体对应一

『听歌识曲』Angular+Electron打造PC端网易云音乐

缘起峡谷 众所周知, web应用是可以打包成桌面应用的. 而我一直没太在意这个事儿, 直到某一天夜晚... 那天, 我独自在召唤师峡谷晃荡, 披荆斩麻, 享受着杀戮的快感. 那把比赛不是普通的排位, 是我赌上荣耀的一战, 因为 当时我已经0胜点了... 果不其然, 在这几位提款机队友的帮助下, 我成功拿下SVP. 随之而来的便是掉段的通知弹窗. 作为前端工程师, 我敏锐地注意到, 客户端弹窗的内容带着<br>标签. 纳尼!? LOL是Web写的? 这不可能! 一般客户端都是C++写的, 更何况

『后缀自动机入门 SuffixAutomaton』

本文的图片材料多数来自\(\mathrm{hihocoder}\)中详尽的\(SAM\)介绍,文字总结为原创内容. 确定性有限状态自动机 DFA 首先我们要定义确定性有限状态自动机\(\mathrm{DFA}\),一个有限状态自动机可以用一个五元组\((\mathrm{S},\Sigma,\mathrm{st},\mathrm{end},\delta)\)表示,他们的含义如下: \(1.\) \(\mathrm{S}\) 代表自动机的状态集 \(2.\) \(\Sigma\) 代表字符集,也称字

老秦『十里桃花招商』Q849852

老秦『十里桃花招商』Q849852 十里桃花招商老秦是一个信誉至上的上级,对下级更是无私的教导 十里桃花招商老秦秉持著赚钱一起赚的心态! 十里桃花招商老秦把下级当做是兄弟.是亲人.更是最重要的工作伙伴! 十里桃花招商老秦在市场已经混了六年,信誉更是无庸置疑! 十里桃花招商老秦团队目前已有二十馀人,团队还在持续扩大中! 十里桃花招商老秦诚邀您一起加入我们! 跟著十里桃花招商老秦,即时知道市场动态!