好像哪位老师曾说过,音乐是世界通用语言。是的,听不懂英文,但总能欣赏英文歌曲吧。
很早以前就想做个音乐播放器,但是由于跟随歌曲显示当前歌词的效果一直实现不了,所以我的想法一直无法实现。不过,最近创意不佳,没心情开发游戏了,于是闲下来搞点小发明。这次就先模仿一下手机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
欢迎继续关注我的博客