原理:动态遮罩(本文将使用到AactionScript2定时器),以下是其完整源码(含测试用例):
import wargrey.util.Timer; import wargrey.util.Map; import mx.transitions.Tween; import mx.transitions.easing.*; /** * @author Yoshua */ class wargrey.util.SoundCaption extends MovieClip{ public static var LIB:Number=0; public static var LOCAL:Number=1; public static var NETWORK:Number=2;
private var masker:MovieClip; private var maskee:MovieClip; private var exMask:MovieClip; private var maskeeFormator:TextFormat; private var exMaskFormator:TextFormat; private var song:Sound; private var words:Array; private var mapping:Map; private var times:Array; private var timer:Timer;
public function set songWord(word:String):Void{ exMask.exMaskTextFeild.text=word; maskee.maskeeTextFeild.text=word; exMask.exMaskTextFeild.setTextFormat(exMaskFormator); maskee.maskeeTextFeild.setTextFormat(maskeeFormator); }
public function SoundCaption(){ words=new Array(); mapping=new Map(); times=new Array(); timer=new Timer(5); } public function initialize(font:String,size:Number,bgc:Number,fgc:Number){ font=(font.length>0)?font:"Times New Roman"; size=(size>0)?size:12; bgc=(bgc>0)?bgc:0x2CEA15; fgc=(fgc>0)?fgc:0xff0000; exMask=createEmptyMovieClip("exMask",5); maskee=exMask.duplicateMovieClip("maskee",10); masker=createEmptyMovieClip("masker",15); maskee.createTextField("maskeeTextFeild",0,0,0,100,22); exMask.createTextField("exMaskTextFeild",0,0,0,100,22); maskee.maskeeTextFeild.autoSize=true; exMask.exMaskTextFeild.autoSize=true; maskeeFormator=new TextFormat(font,size,fgc,true); exMaskFormator=new TextFormat(font,size,bgc,true); masker.beginFill(0x0000ff); masker.moveTo(0,0); masker.lineTo(24,0); masker.lineTo(24,22); masker.lineTo(0,22); masker.lineTo(0,0); masker.endFill(); maskee.setMask(masker); } public function setSong(name:String,words:Array,src:Number):Void{ song=new Sound(this); switch (src){ case SoundCaption.LOCAL: song.loadSound(name,false); break; case SoundCaption.NETWORK: song.loadSound(name,true); break; default: song.attachSound(name); } song.onSoundComplete=this.terminate; timer.addEventListener(Timer.TIMECYCLE,this); setWords(words); song.start(0,1); timer.start(); } private function setWords(wordArray:Array):Void{ var start:Number; var end:Number; for (var i:Number=0;i<wordArray.length;i++){ var wordLine:String=wordArray[i]; var lineSplit:Array=wordLine.split("]"); var len:Number=lineSplit.length; if (len==0)break; words.push(lineSplit[len-1]); for (var j:Number=0;j<len-1;j++){ var timeString:String=lineSplit[j].substr(1); var timeSplit:Array=timeString.split(":"); end=((new Number(timeSplit[0]))*60+(new Number(timeSplit[1])))*1000; mapping.put(end,i); } if (i==0){ start=end; }else{ var last:Number=end-start-1; times.push(last); start=end; } } }
private function terminate():Void{ timer.stop(); } private function timecycle():Void{ var pos:Number=song.position; pos=Math.floor(pos/100)*100; if (mapping.get(pos)==null)return; var line:Number=Number(mapping.get(pos)); songWord=words[line]; masker._height=maskee._height; new Tween( masker,"_width",Regular.easeInOut,0,maskee._width,times[line]/1000,true ).start(); } private function onLoad() : Void{ initialize("华文彩云",32); var words : Array = new Array(); words.push("[00:01]火柴天堂 熊天平 《一个人流浪》"); words.push("[02:36][00:14]走在寒冷下雪的夜空"); words.push("[02:41][00:19]卖着火柴温饱我的梦"); words.push("[02:46][00:25]一步步冰冻一步步寂寞"); words.push("[02:51][00:29]人情寒冷冰冻我的手"); words.push("[02:59][00:39]一包火柴燃烧我的心"); words.push("[03:04][00:44]寒冷夜里挡不住前行"); words.push("[03:09][00:49]风刺我的脸雪割我的口"); words.push("[03:14][00:54]拖着脚步还能走多久"); words.push("[03:22][01:02]有谁来买我的火柴"); words.push("[03:27][01:07]有谁将一根根希望全部点燃"); words.push("[03:32][01:12]有谁来买我的孤单"); words.push("[03:37][01:18]有谁来实现我想家的呼唤"); words.push("[03:45][01:26]每次点燃火柴微微光芒"); words.push("[03:50][01:31]看到希望看到梦想"); words.push("[03:52][01:33]看见天上的妈妈说话"); words.push("[03:54][01:35]她说你要勇敢你要坚强"); words.push("[03:57][01:38]不要害怕不要慌张"); words.push("[03:59][01:40]让你从此不必再流浪"); words.push("[04:02][01:43]每次点燃火柴微微光芒"); words.push("[04:05][01:46]看到希望看到梦想"); words.push("[04:07][01:48]看见天上的妈妈说话"); words.push("[04:09][01:50]她说你要勇敢你要坚强"); words.push("[04:12][01:53]不要害怕不要慌张"); words.push("[04:14][01:55]让你从此不必再流浪"); words.push("[04:19][01:59]妈妈牵着你的手回家"); words.push("[04:24][02:02]睡在温暖花开的天堂"); words.push("[02:15](music:march heaven 《一个人流浪》)"); words.push("[02:22] "); words.push("[04:32]天堂"); words.push("[04:38]天堂"); words.push("[04:42]......"); words.push("[04:50]"); setSong("mh.mp3",words,SoundCaption.LIB); } } |
该字幕类SoundCaption其实就是一个空的MovieClip实例,初始化时会动态生成3个空的MovieClip,分别用于充当遮罩层masker、未唱到的歌词层maskee和已唱到歌词层exMask,三个层按顺序重叠在一起。这样一来,在歌曲开始播放时将歌词显示在歌词层,并且根据歌唱时间合理的移动遮罩层并可以实现字模的显示效果。下面是本例重要方法的说明:
initialize所做的正是上述过程的准备工作。该方法有四个参数,依次为字体(String)、字号(Number)、未唱到的歌词颜色(Number)和已唱到的歌词颜色(Number)。四个参数均可省略,默认值可以参考initialize方法的开始处。
setWords:将歌词转化成方便处理的信息。该方法有一个参数,即存放所有歌词的字符串数组,该参数下面详细介绍。这里涉及到一个集合类Map,由于ActionScript2默认的集合框架比较简陋,再加上做这个工具时比较仓促,就随便写了一个基本的Map类,代码如下:
class wargrey.util.Map { private var keys:Array; private var values:Array;
public function Map(){ keys=new Array(); values=new Array(); } public function put(key:Object,value:Object):Void{ for (var i:Number=0;i<keys.length;i++){ if (keys[i]==key){ values[i]=value; return ; } } keys.push(key); values.push(value); return ; } public function get(key:Object):Object{ for (var i:Number=0;i<keys.length;i++) if (keys[i]==key)return values[i]; return null; } public function del(key:Object):Object{ var result:Object=null; for (var i:Number=0;i<length;i++){ if (keys[i]==key){ result=values[i]; keys[i]=keys[length-1]; values[i]=values[length-1]; keys.pop(); values.pop(); break; } } return result; } public function get length():Number{ return keys.length; } public function toString():String{ var s:String=""; for (var i:Number=0;i<length;i++) s=s+"\n"+keys[i]+"="+values[i]; return s; } } |
setSong:该方法的任务比较重,一方面要找到歌曲文件,一方面还要负责歌词歌曲的处理和同步,其实都不是它自己在做。该方法有三个参数,依次为:
歌曲实例名(String):即完整合法的歌曲源名,该源可以来自库,本地文件和网络流。
歌词(Array):即包含所有歌词的字符数组,该参数将被传递给setWords方法处理。歌词的写法与lrc(歌词文件)相同,不过只包含主体部分,诸如歌手、专题等信息不在本例处理范围内。
源类型(Number):即歌曲源的类型,可选的取值有三种,分别为SoundCaption.LIB(库实例)、SoundCaption.LOCAL(本地文件)和SoundCaption.NETWORK(网络流),默认为SoundCaption.LIB。
本例只是一个基本设计,如果可以结合一些图片类的字体便可以做出很多更有趣的字幕效果。不过本例还是有缺点的,比如并不能正真做到词曲同步,目前只能通过细化歌词来达到此目的。