一、功能简介:
这个由AS2纯代码实现的单行卡拉OK风格歌词同步MP3多曲播放器,除了播放音乐,同步显示LRC歌词的基本功能之外;最富特色的莫过于它能让歌词字条从左到右渐显推入,然后又继续向右渐隐淡出;它还可以在XML列表中为每个MP3指定一张背景图片,播放MP3时能同步进行更换;另外,全屏显示并自动适应屏幕大小、开关歌词条的水平运动动画效果、为歌词条添加或和移除倒角投影滤镜以改变其视觉效果、控制MP3的播放暂停和曲目切换,等等,也是它的实用功能。
二、技术要点:
《梦回AS2纯代码卡拉OK动态歌词同步配图MP3播放器》仅有244行代码(其中包含11行注释)。但“麻雀虽小,五脏俱全”,它综合应用了多种实用的AS2开发技术:
1、屏幕尺寸大小自动适应:侦听舞台尺寸变化,让LRC同步歌词显示条始终位于舞台正下方,播放控制条始终位于舞台右下方,MP3配图始终垂直顶对齐,高度始终等于舞台高度-100像素,宽度以高度为标准比例缩放,水平居中对齐;
2、用AS2代码创建右键菜单;
3、用AS2代码创建动态文本框、并使用TextFormat类设置文本格式;
4、用AS2代码在舞台上创建影片剪辑元件并添加倒角和投影滤镜;
5、用AS2代码流式加载MP3文件并控制其播放暂停;
6、用AS2代码为动态文本框设置HTML格式文本,并使用asfunction协议通过其中的链接,调用函数;
7、用AS2代码加载并解析外部XML文件,获取其节点上自定义属性的值;
8、用AS2代码加载并解析外部LRC文件,用字符串拆分的方法将其中包含的时间标签和对应的歌词句子存贮到两个数组中;
9、用AS2代码的MovieClipLoader类加载外部图片文件,并为其添加侦听器;
10、用AS2代码的BitmapData类平滑加载进来的图片,令其在缩放之后不产生马赛克效果;
11、用AS2代码的Tween类,在图片加载完毕时为其添加淡入动画效果;
12、用AS2代码的setInterval + updateAfterEvent()实现不依赖帧频的高频率渲染,让卡拉OK风格的LRC同步歌词条进度动画更加细腻平滑。
三、算法特色:
1、独创的简洁高效的LRC文本的解析方法:使用的LRC解析算法是自己根据对LRC文件的分析编写的,使用2个FOR循环嵌套,外层解析LRC文本行,内层解析一行中的所有时间标签。支持一行多时间标签的LRC文件,支持精确到秒和毫秒两种LRC规格。LRC解析过程总代码仅21行,如下所示:
-折叠
var lrcItem:Array;
var divsign:String = "|||";
function parseLRC(lrcStr) {
lrcItem = [];
var lrcLine:Array = lrcStr.split("\n");
for (var k:Number = 0; k<lrcLine.length; k++) {
for (var j:Number = 0; j<1000; j++) {
var subpos:Number = lrcLine[k].indexOf("]");
if (subpos<0) {
break;
} else {
var curTimeTag:String = lrcLine[k].slice(1, subpos);
curTimeTag.length == 5 ? curTimeTag += ".00" : null;
lrcItem[lrcItem.length] = curTimeTag+divsign+lrcLine[k].slice(lrcLine[k].lastIndexOf("]")+1);
lrcItem[lrcItem.length-1] = lrcItem[lrcItem.length-1].split("\r").join("").split("\n").join("");
lrcLine[k] = lrcLine[k].slice(subpos+1);
}
}
}
lrcItem.sort();
}
LRC原文件内容:
[ti:我们站在雨中]
[ar:]
[al:我们站在雨中]
[by:LRC制作者]
[00:03.25]《我们站在雨中》
[00:08.68]连城诀吴越版主题歌曲
[00:16.54]把双眼紧闭,剑要往哪里刺去
[00:24.97]谁说秋风最能懂落叶思绪
[00:32.21]致命的一击,真的对手是自己
[00:40.24]只剩下了无声的叹息
[00:48.69]酒逢知己,千杯也难尽兴
[00:56.27]英雄醉了也一样的慢慢倒下去
[01:04.45]望星辰转移,江湖又下起暴雨
[01:12.51]我们都站在雨里
[01:52.46][01:20.49]穿过刀光剑影之后,我还是那个我
[02:00.74][01:28.45]只是把受伤的心独自对着明月说
[02:08.38][01:36.77]天涯的尽头有没有一处安静的角落
[02:16.57][01:44.70]让浪迹的人在走累的时候躲一躲
[02:31.80][00:12.40]
解析之后输出的结果:
00:03.25|||《我们站在雨中》
00:08.68|||连城诀吴越版主题歌曲
00:12.40|||
00:16.54|||把双眼紧闭,剑要往哪里刺去
00:24.97|||谁说秋风最能懂落叶思绪
00:32.21|||致命的一击,真的对手是自己
00:40.24|||只剩下了无声的叹息
00:48.69|||酒逢知己,千杯也难尽兴
00:56.27|||英雄醉了也一样的慢慢倒下去
01:04.45|||望星辰转移,江湖又下起暴雨
01:12.51|||我们都站在雨里
01:20.49|||穿过刀光剑影之后,我还是那个我
01:28.45|||只是把受伤的心独自对着明月说
01:36.77|||天涯的尽头有没有一处安静的角落
01:44.70|||让浪迹的人在走累的时候躲一躲
01:52.46|||穿过刀光剑影之后,我还是那个我
02:00.74|||只是把受伤的心独自对着明月说
02:08.38|||天涯的尽头有没有一处安静的角落
02:16.57|||让浪迹的人在走累的时候躲一躲
02:31.80|||
al:我们站在雨中|||
ar:|||
by:LRC制作者|||
ti:我们站在雨中|||
2、LRC句子时间点与MP3播放时间的对比用字符串对比方式实现:与网上其他开发者制作歌词同步时用的LRC时间数组与播放时间的对比方式不同,我不比时间,而是比字符串,即把当前播放时间转换成字符串再和LRC时间数组中的字符串相对比,从而实现到时换句从上面的代码中可以看出,时间标签只保留了前面七个字符,也就是精确到0.1秒,这是为了降低对比的频率以降低CPU使用率,精确到0.1秒已经完全足够了,因为相邻两句歌词的时间间隔绝对不可能小于0.1秒。两次比较的时间间隔我设置为了30毫秒,也就是说确保时间头经过每一个LRC时间点时都会执行至少三次对比,以确保不会漏句跳句。
很低的CPU占用率(测试环境:2.4G单核赛扬四CPU)
3、制作歌词条卡拉OK进度效果不用遮罩,而是直接设置本底文本框的自动对齐属性(autoSize)和高亮文本框的宽度:
看到网上其他人的LRC歌词同步MP3播放器作品,几乎都是用的遮罩的方法来做卡拉OK的进度效果的,而我做的水平文字的LRC同步播放器完全不用遮罩(除了竖式的必须使用遮罩以外,见下图:)
水平文字卡拉OK风格歌词条的进度实现,我使用的是直接设置本底文本框的自动对齐属性(autoSize)和高亮文本框的宽度的方法。使用这种技术有一点要注意:滤镜一定不要加在文本框上,而是要将文本框放入一个剪辑,滤镜只能加到剪辑上。
四、MP3播放列表XML文件的结构:
五、《梦回AS2纯代码卡拉OK动态歌词同步配图MP3播放器》全部AS2代码:
/*****************第一部分:系统参数设置、显示尺寸及模式、右键菜单功能****************/
/*****************第一部分:系统参数设置、显示尺寸及模式、右键菜单功能****************/
System.useCodepage = false;
Stage.scaleMode = "NoScale";
Stage.align = "LT";
var stageResizeListener:Object = new Object();
stageResizeListener.onResize = fitmodels;
function fitmodels() {
lrcBar._x = Stage.width/2-350;
lrcBar._y = Stage.height-100;
mp3Cap._x = Stage.width-mp3Cap._width;
mp3Cap._y = Stage.height-35;
if (mp3Img._width) {
mp3Img._height = Stage.height-100;
mp3Img._xscale = mp3Img._yscale;
mp3Img._x = (Stage.width-mp3Img._width)/2;
}
}
Stage.addListener(stageResizeListener);
function switchDisplayMode() {
Stage["displayState"] == "normal" ? Stage["displayState"]="fullScreen" : Stage["displayState"]="normal";
}
function authorHomePage() {
System.setClipboard("http://dreamdesign./");
getURL("http://dreamdesign./", "_blank");
}
function authorQQTalk() {
System.setClipboard("http://wpa.qq.com/msgrd?v=3&uin=744123372&site=qq&menu=yes");
getURL("http://wpa.qq.com/msgrd?v=3&uin=744123372&site=qq&menu=yes", "_blank");
}
var rightMenu:ContextMenu = new ContextMenu();
rightMenu.hideBuiltInItems();
rightMenu.customItems.push(new ContextMenuItem("□ 全屏显示/返回窗口", switchDisplayMode, true, true, true));
rightMenu.customItems.push(new ContextMenuItem("○ 一叶扁舟(梦回轻狂)2013.08.18,QQ:744123372", authorQQTalk, true, true, true));
rightMenu.customItems.push(new ContextMenuItem("⊙ 一叶梦回XML-LRC单行卡拉OK风格歌词同步MP3列表播放器(一曲一图+全屏自适应)", authorHomePage, true, true, true));
this.menu = rightMenu;
/*****************第二部分:解析LRC文件的功能****************/
/*****************第二部分:解析LRC文件的功能****************/
var lrcItem:Array;
var divsign:String = "|||";
function parseLRC(lrcStr) {
lrcItem = [];
var lrcLine:Array = lrcStr.split("\n");
for (var k:Number = 0; k<lrcLine.length; k++) {
for (var j:Number = 0; j<1000; j++) {
var subpos:Number = lrcLine[k].indexOf("]");
if (subpos<0) {
break;
} else {
var curTimeTag:String = lrcLine[k].slice(1, subpos);
curTimeTag.length == 5 ? curTimeTag += ".00" : null;
lrcItem[lrcItem.length] = curTimeTag+divsign+lrcLine[k].slice(lrcLine[k].lastIndexOf("]")+1);
lrcItem[lrcItem.length-1] = lrcItem[lrcItem.length-1].split("\r").join("").split("\n").join("");
lrcLine[k] = lrcLine[k].slice(subpos+1);
}
}
}
lrcItem.sort();
}
/*****************第三部分:显示LRC歌词的功能****************/
/*****************第三部分:显示LRC歌词的功能****************/
import flash.filters.BevelFilter;
var lrcBarBevelFilter:BevelFilter = new BevelFilter(1);
import flash.filters.DropShadowFilter;
var lrcBarDropShadowFilter:DropShadowFilter = new DropShadowFilter(1);
lrcBarDropShadowFilter.blurX = lrcBarDropShadowFilter.blurY=3;
var lrcTextFomart:TextFormat = new TextFormat();
lrcTextFomart.size = 28;
lrcTextFomart.bold = true;
//lrcTextFomart.font = "楷体_GB2312";
var curLrcId:Number;
var lrcHander:Number;
function getTimeMS(timeStr:String):Number {
return (Number(timeStr.split(":")[0])*60+Number(timeStr.split(":")[1])+Number(timeStr.split(".")[1])/10)*1000;
}
function getTimeStr(theTime:Number):String {
var sec = Math.floor(theTime/1000)%60;
sec<10 ? sec="0"+sec : null;
var min = Math.floor(Math.floor(theTime/1000)/60);
min<10 ? min="0"+min : null;
return min+":"+sec;
}
var secondPos:String;
var klokStepLong:Number;
var lrcMotionMode:Boolean = true;
var lrcBarFilterMode:Boolean = true;
function updateLrcBar() {
var curTime:Number = mp3Sound.position+300;
var mp3Time:String = getTimeStr(curTime);
if (secondPos != mp3Time) {
secondPos = mp3Time;
mp3Cap.htmlText = "<b><font color='#ff0000'>"+mp3Time+" / "+getTimeStr(mp3Sound.duration/(mp3Sound.getBytesLoaded()/mp3Sound.getBytesTotal()))+"</font></b> "+mp3CtrlHtml;
}
mp3Time += "."+String(curTime).slice(String(curTime).length-3, String(curTime).length-2);
//for (var i:Number = curLrcId+1; i<curLrcId+5; i++) {
for (var i:Number = curLrcId+1; i<lrcItem.length; i++) {
if (lrcItem[i].slice(0, 7) == mp3Time) {
curLrcId = i;
break;
}
}
if (lrcBar.lrcBase.lrcText.text != lrcItem[curLrcId].split(divsign)[1] && lrcBar._alpha>0) {
lrcBar._alpha -= 10;
lrcMotionMode == true ? lrcBar.lrcBase._x=lrcBar.lrcOver._x=Math.ceil((100-lrcBar._alpha)/10)/10*lrcBar.lrcBase.lrcText._width*1.5 : null;
} else if (lrcBar.lrcBase.lrcText.text == lrcItem[curLrcId].split(divsign)[1] && lrcBar._alpha<100) {
lrcBar._alpha += 10;
lrcMotionMode == true ? lrcBar.lrcBase._x=lrcBar.lrcOver._x=-Math.ceil((100-lrcBar._alpha)/10)/10*lrcBar.lrcBase.lrcText._width*1.5 : null;
} else if (lrcBar._alpha<=0) {
lrcBar.lrcBase._x = lrcBar.lrcOver._x=0;
curLrcId>=0 ? setLrcText() : null;
}
lrcBar.lrcOver.lrcText._width<lrcBar.lrcBase.lrcText._width ? lrcBar.lrcOver.lrcText._width += klokStepLong : lrcBar.lrcOver.lrcText._width=lrcBar.lrcBase.lrcText._width;
//lrcBar.lrcOver.lrcText._width<lrcBar.lrcBase.lrcText._width ? lrcBar.lrcOver.lrcText._width=lrcBar.lrcBase.lrcText._width*((getTimeMS(mp3Time)-getTimeMS(lrcItem[curLrcId].slice(0,7)))/(getTimeMS(lrcItem[curLrcId+1].slice(0,7))-getTimeMS(lrcItem[curLrcId].slice(0,7)))) : lrcBar.lrcOver.lrcText._width=lrcBar.lrcBase.lrcText._width;
updateAfterEvent();
}
function setLrcText() {
lrcBar.lrcBase.lrcText.text = lrcBar.lrcOver.lrcText.text=lrcItem[curLrcId].split(divsign)[1];
lrcBar.lrcBase.lrcText.setTextFormat(lrcTextFomart);
lrcBar.lrcOver.lrcText.setTextFormat(lrcTextFomart);
lrcBar.lrcOver.lrcText._x = lrcBar.lrcBase.lrcText._x;
lrcBar.lrcOver.lrcText._width = 0;
klokStepLong = lrcBar.lrcBase.lrcText._width/((getTimeMS(lrcItem[curLrcId+1].slice(0, 7))-getTimeMS(lrcItem[curLrcId].slice(0, 7)))/(Stage["displayState"] == "fullScreen" ? 50 : 45));
}
function switchFilterMode() {
if (lrcBarFilterMode == true) {
lrcBarFilterMode = false;
lrcBar.filters = [];
} else {
lrcBarFilterMode = true;
lrcBar.filters = [lrcBarBevelFilter, lrcBarDropShadowFilter];
}
}
function switchMotionMode() {
lrcMotionMode = !lrcMotionMode;
}
/*****************第四部分:加载LRC文件的功能****************/
/*****************第四部分:加载LRC文件的功能****************/
var lrcLoader:LoadVars;
function loadLRC(lrcUrl:String) {
delete lrcLoader.onData;
lrcLoader = new LoadVars();
lrcLoader.onData = function(lrcStr) {
parseLRC(lrcStr);
trace(lrcItem.join("\n"));
curLrcId = -1;
clearInterval(lrcHander);
startLrcBarUpdate();
};
lrcLoader.load(lrcUrl);
}
function startLrcBarUpdate() {
lrcItem.length>0 ? lrcHander=setInterval(updateLrcBar, 30) : lrcBar.lrcBase.lrcText.text=lrcBar.lrcOver.lrcText.text="非常抱歉,此曲暂无歌词!";
}
/*****************第五部分:播放MP3列表的功能****************/
/*****************第五部分:播放MP3列表的功能****************/
var mp3List:Array;
var mp3Sound:Sound;
var mp3Id:Number = -1;
var mp3CtrlHtml:String;
function playmp3(disVar:Number) {
clearInterval(lrcHander);
mp3Sound = new Sound(this);
mp3Sound.onSoundComplete = function() {
playmp3(1);
};
mp3Playing = true;
mp3Id = makeMp3Id(disVar);
mp3Sound.loadSound(mp3List[mp3Id].attributes.url,true);
lrcBar.lrcBase.lrcText.text = lrcBar.lrcOver.lrcText.text="";
mp3CtrlHtml = "<b>("+String(mp3Id+1)+"/"+mp3List.length+") "+mp3List[mp3Id].attributes.cap+" <font color='#cc0000'><a href='asfunction:playmp3,-1'><上一首</a> <a href='asfunction:pausemp3,1'>暂停/播放</a> <a href='asfunction:playmp3,1'>下一首></a> <a href='asfunction:switchFilterMode'>倒角投影淡入开关</a> <a href='asfunction:switchMotionMode'>动效开关</a> <a target='_blank' href='"+mp3List[mp3Id].attributes.lrc+"'>LRC</a> </font></b>";
mp3Cap.htmlText = mp3CtrlHtml;
loadLRC(mp3List[mp3Id].attributes.lrc);
loadMp3Img(mp3List[mp3Id].attributes.img);
}
function makeMp3Id(disVar:Number) {
var theid:Number = mp3Id+Number(disVar);
theid>mp3List.length-1 ? theid=0 : null;
theid<0 ? theid=mp3List.length-1 : null;
return theid;
}
var mp3Playing:Boolean;
var mp3PausePos:Number;
function pausemp3() {
clearInterval(lrcHander);
if (mp3Playing == true) {
mp3Playing = false;
mp3PausePos = mp3Sound.position/1000;
mp3Sound.stop();
} else {
mp3Playing = true;
mp3Sound.start(mp3PausePos,1);
startLrcBarUpdate();
}
}
/*****************第六部分:加载MP3配图的功能****************/
-折叠
/*****************第六部分:加载MP3配图的功能****************/
import flash.display.BitmapData;
var imgbitmapdata:BitmapData;
import mx.transitions.Tween;
import mx.transitions.easing.*;
var fadestyle:Object = None.easeNone;
var fadeduration:Number = 0.5;
var fadeTween:Object;
var imgloader:MovieClipLoader = new MovieClipLoader();
var imgloadlistener:Object = new Object();
imgloader.addListener(imgloadlistener);
imgloadlistener.onLoadInit = function(target:MovieClip) {
imgbitmapdata = new BitmapData(target._width, target._height, true, 0xff0000);
imgbitmapdata.draw(target);
target.attachBitmap(imgbitmapdata,target.getNextHighestDepth(),"auto",true);
fadeTween = new Tween(mp3Img, "_alpha", fadestyle, mp3Img._alpha, 100, fadeduration, true);
fitmodels();
};
function loadMp3Img(imgUrl:String) {
fadeTween = new Tween(mp3Img, "_alpha", fadestyle, mp3Img._alpha, 0, fadeduration, true);
fadeTween.onMotionFinished = function() {
imgloader.unloadClip(mp3Img);
mp3Img._xscale = mp3Img._yscale=100;
imgloader.loadClip(imgUrl,mp3Img);
};
}
/*****************第七部分:创建LRC歌词条、播放控制链接等界面元素****************/
/*****************第七部分:创建LRC歌词条、播放控制链接等界面元素****************/
function createLrcCtrl() {
createEmptyMovieClip("mp3Img",getNextHighestDepth());
createEmptyMovieClip("lrcBar",getNextHighestDepth());
lrcBar.filters = [lrcBarBevelFilter, lrcBarDropShadowFilter];
lrcBar.createEmptyMovieClip("lrcBase",lrcBar.getNextHighestDepth());
lrcBar.createEmptyMovieClip("lrcOver",lrcBar.getNextHighestDepth());
lrcBar.lrcBase.createTextField("lrcText",lrcBar.lrcBase.getNextHighestDepth(),0,15,740,35);
lrcBar.lrcOver.createTextField("lrcText",lrcBar.lrcOver.getNextHighestDepth(),0,15,740,35);
lrcBar.lrcBase.lrcText.textColor = 0x3366ff;
lrcBar.lrcOver.lrcText.textColor = 0xff3300;
createTextField("mp3Cap",getNextHighestDepth(),0,65,740,20);
lrcBar.lrcBase.lrcText.autoSize = "center";
mp3Cap.autoSize = "right";
mp3Cap.html = true;
lrcBar._alpha = 0;
}
createLrcCtrl();
fitmodels();
/*****************第八部分:加载MP3列表的功能****************/
/*****************第八部分:加载MP3列表的功能****************/
var mp3Xml:XML = new XML();
mp3Xml.ignoreWhite = true;
mp3Xml.onLoad = function(success) {
if (success) {
mp3List = this.firstChild.firstChild.childNodes;
playmp3(Math.ceil(Math.random()*mp3List.length));
}
};
mp3Xml.load("http://dreamdesign.105./bbs/menber/yypz/mp3/lrc/mp3list.xml");