译者:雪の猫
Macromedia Flash Professional 8 和 Adobe Flex Builder 2 都支持直接在Flash video (FLV)文件中使用提示点。使用Flash 8(ActionScript 2.0),完成这工作只需要一个FLVPlayback组件的实例和简单的设置。可以在组件检查面板中定义或者调用FLVPlayback.addAsCuePoint()方法;使用Flex Builder 2,需要的是对应VideoDisplay实例指定的CuePointManager类的实例。两种情况下,接下来都是设置一个cuePoint事件的监听器对象和定义该对象一个特定的响应函数。
而对音频文件,实现提示点要困难些。虽然使用Flash8能用如MediaController, MediaDisplay, 或MediaPlayback这些媒体组件来装载MP3文件,但是要注意这些组件是为了兼容Flash Player 6 and 7。这些组件的结构比FLVPlayback旧,这样,使用它们会使生成的swf文件多出至少55KB。使用FLVPlayback仅使之增加33KB,可是它不能装载MP3文件。类似的,Flex Builder 2的VideoDisplay类只能装载video。可以暂且做几个只有声音的FLV文件(不包含视频内容)。不过,这看来并不广泛适用。特别在一个团队中,你并不能决定选择哪些外部资源。而最通常的音频格式是MP3。幸运的是,用一点ActionScript就可以弥补这个不足了。
这篇文章将讲述一个特别解决方案的两个版本:用一个自定义类SoundSync来支持内部声音资源或者是外部的MP3文件。代码有ActionScript 2.0(Flash Professional 8 )和ActionScript 2.0(Flex Builder 2)两种。
不管使用那种语言,方法是一样的。ActionScript 2.0和ActionScript 3.0 都提供了Sound类来代表嵌入或装载的声音文件。两种语言都允许播放,从开头播放或从音频的某个时刻播放,暂停,循环。我们当然想不重造轮子!那些已经有的功能都是有用的。缺少的只是对提示点的支持,所以看来应该保留原有功能而extend这个Sound类。
自定义类SoundSync要继承Sound并添加下面的新公有方法:
addCuePoint(): 添加一个提示点
getCuePoint(): 通过提示点的名字或者时间返回该提示点
removeCuePoint(): 通过提示点的名字或者时间删除该提示点
removeAllCuePoints(): 删除所有的提示点
此外,还添加了几个私有方法。并根据使用语言不同重写一些原有的方法。简而言之,SoundSync类使用一个timer不断对照当前的播放时间检查当前提示点对象的time属性。我们先从ActionScript 2.0开始再移植到ActionScript 3.0。
需要环境
为了充分学习,需要如下软件和文件:
Flash Professional 8
Flex Builder 2
In Labs: Flash Professional 9 ActionScript 3.0 Preview (可选)
示例文件
sound_sync.zip (ZIP, 3MB)
准备知识
对ActionScript 2.0的中等掌握和对Flex Builder 2及ActionScript 3.0的介绍性了解。
ActionScript 2.0版的SoundSync类
自定义类必须保存到一个外部文本文件中,命名为SoundSync.as。按照最佳实践,应该把这个文件放在符合包结构的路径下的文件夹中。这里,应该放到相对于全局classpath设置的(选择Edit > Preferences > ActionScript category > ActionScript 2.0 Settings 按钮,中文的按钮……我不是中文版)路径为net/quip/sound这个文件夹下。为了方便按照我的域名给包起名为net.quip.sound。请随便把它改成你喜欢的吧。有关包的细节,请参考Flash8帮助文档中”……我又不是中文版–b”节。
创建ActionScript 2.0 类文件
启动Flash Professional 8,选择File > New > ActionScript File 图一
---
|图1 |
| |
---
输入如下的ActionScript代码:
/**************代码*1***********/
import mx.events.EventDispatcher;
import mx.utils.Delegate;
class net.quip.sound.SoundSync extends Sound {
// PROPERTIES 属性
// CONSTRUCTOR 构造
// METHODS 方法
// EVENT HANDLERS 事件处理
}
头两行import了两个类,后面的代码会用到他们。在ActionScript 2.0中,import语句可以方便以后输入类名的时候不必再输入其完整的包名。
接下来,是声明这个类继承自基类Sound。然后写上属性,构造函数,方法和事件的注释占个位置。对一个类的代码,这些不是必须的,但是他们使类的结构一目了然。下面把细节添上。
声明属性:
在// PROPERTIES 属性 的注释下面输入:
private var _cuePoints:Array;
private var _currentCuePoint:Number;
private var _interval:Number;
private var _intervalDuration:Number;
private var _secondOffset:Number;
// Event dispatcher
public var dispatchEvent:Function;
public var addEventListener:Function;
private var removeEventListener:Function;
为了方便,私有变量的名字用下划线(_)开头。这样就容易辨认出这些变量只能用在该类的内部(即,类外不可以访问)。一会儿再解释每个属性。先注意最后三个,为了使用EventDispatcher,它们是必须的,EventDispatcher可以使这个类的实例发送事件给监听器,包括cuePoint事件。
构造函数
在// CONSTRUCTOR 构造 的注释下面输入:
public function SoundSync(target:MovieClip) {
super(target);
init();
}
这个函数使SoundSync可以像Sound一模一样的使用。super语句调用父类(基类)的构造函数,它接受一个可选的MovieClip引用作参数。因为有这个语句,基类在这里完成其初始化。然后是调用init()方法,进一步完成SoundSync的初始化,下面介绍它:
方法
这个类包含下面一些方法,我们从init开始讲解:
init()
在// METHODS 方法 的注释下面输入:
private function init():Void {
// Initialize properties 初始化属性
_cuePoints = new Array();
_currentCuePoint = 0;
_intervalDuration = 50;
_secondOffset = 0;
// Initialize class instance as valid event broadcaster
// 把类的实例作为EventDispatcher的广播者
EventDispatcher.initialize(this);
}
好了,我们给几个属性赋值为默认的值。cuePoints是一个数组对象,元素是控制点对象的引用,用currentCuePoint作数组当前的索引。_intervalDuration 属性定义检查的周期,就是两次检查音频当前位置的时间间隔,单位是毫秒。_secondOffset属性用来指定播放开始后多少秒才开始检查(默认是0)。
最后一行调用EventDispatcher.initialize()方法,参数是this,这样每一个SoundSync的实例都可以发送事件了。
addCuePoint()
可以添加addCuePoint()方法了. 在init()之后输入:
// Add Cue Point 添加提示点
public function addCuePoint(cuePointName:String, cuePointTime:Number):Void {
_cuePoints.push(
{
type: “cuePoint”,
name: cuePointName,
time: cuePointTime,
target: this
}
);
_cuePoints.sortOn(”time”, Array.NUMERIC);
}
这个方法需要两个参数:cuePointName 和 cuePointTime。分别表示一个提示点的名字name和时刻temporal position。然后我们把两个参数的值赋给一个object对象的两个属性,用 Array.push()方法添加到cuePoints数组;花括号{}是new Object()的一个简记。此外,每一个提示点对象还需要一个字符串类型的type属性。最后,提示点对象还要一个target属性来引用发送事件的那个SoundSync的实例(this)。
每当添加一个提示点对象,_cuePoints数组要根据cuePoint.time属性重新排列。这样保证了提示点位置的正确。
getCuePoint()
为了对称, 应该提供一个方法检索已经被添加的提示点。在addCuePoint()方法后输入:
// Get Cue Point 得到提示点
public function getCuePoint(nameOrTime:Object):Object {
var counter:Number = 0;
while (counter < _cuePoints.length) {
if (typeof(nameOrTime) == “string”) {
if (_cuePoints[counter].name == nameOrTime) {
return _cuePoints[counter];
}
} else if (typeof(nameOrTime) == “number”) {
if (_cuePoints[counter].time == nameOrTime) {
return _cuePoints[counter];
}
}
counter++;
}
return null;
}
这个方法需要一个参数,可以是提示点的名字或时刻,这意味着参数类型可能是字符串或者可能是数值型。为了应对两种可能,声明参数类型是Object。在方法内,定义一个计数器,用来遍历_cuePoints数组。–b 在while循环里边用typeof检查参数nameOrTime的类型,如果是字符串,nameOrTime 比照当前提示点的name属性;如果是数值型,比照提示点的time属性。因为计数器是递增的,函数返回的是第一个name属性或time属性匹配nameOrTime的提示点,即使有重复的话也是如此。如果没有匹配,返回null。
getCurrentCuePointIndex() 和 getNextCuePointIndex()
这两个方法是”幕后英雄”,它们只被该类内部的其他方法访问。它们返回当前的或者下一个提示点在_cuePoints数组中的位置(索引),这是用来移去当前的提示点并选定下一个提示点。
在getCuePoint() 方法之后输入:
// 得到当前提示点的索引
private function getCurrentCuePointIndex(cuePoint:Object):Number {
var counter:Number = 0;
while (counter < _cuePoints.length) {
if (_cuePoints[counter].name == cuePoint.name) {
return counter;
}
counter++;
}
return null;
}
// 得到下一个提示点的索引
private function getNextCuePointIndex(seconds:Number):Number {
seconds = (seconds) ? seconds : 0;
var counter:Number = 0;
while (counter < _cuePoints.length) {
if (_cuePoints[counter].time >= seconds * 1000) {
return counter;
}
counter++;
}
return null;
}
第一个方法,getCurrentCuePointIndex(),要求一个提示点对象作为参数。使用一个刚刚那样的循环,这个方法检查每一个_cuePoints数组中提示点的name属性是不是和参数的name匹配。如果是,返回匹配像的索引,否则返回null。
第二个方法, getNextCuePointIndex(), 有趣一些 O_O||| 。 这个方法是要通过指定的时间来确定那一个提示点是”下一个”。假设有三个提示点在10秒,20秒和30秒处。如果音频播放了22秒,下一个可用的提示点就是30秒处的那个——在这个假设中是第三个,索引是2(因为数组下标是0开头)。
这个方法会被该类中的start()和pollCuePoints()两个方法调用,在后边一点讨论。现在有两个必须要解决的小问题:
参数seconds很有可能不能被提供,那样函数的返回值会是null。然后提示点保存的time是以毫秒为单位的,而基类的那些方法都要求其参数的单位是秒。这两个问题都很简单。第一个,我们用条件运算符(?:)来看看seconds参数的值,如果是合法的把它赋值给自己,否则把它赋值为0。
第二个,还是while循环遍历_cuePoints数组。比较每一个提示点的time属性和1000与参数的乘积(因为1秒是1000毫秒)。因为循环从0开始,第一个大于等于给定提示点time属性值的提示点是”下一个”提示点。啊~~这一句要怎么翻
removeCuePoint() 和 removeAllCuePoints()
这两个方法用来删除某一个提示点或删除全部提示点。在getNextCuePointIndex()方法之后输入
// 删除提示点
public function removeCuePoint(cuePoint:Object):Void {
_cuePoints.splice(getCurrentCuePointIndex(cuePoint), 1);
}
// 删除所有提示点
public function removeAllCuePoints():Void {
_cuePoints = new Array();
}
方法removeCuePoint(), 需要一个提示点对象作为参数。调用Array.splice() 方法来删除_cuePoints数组中指定的元素。用getCurrentCuePointIndex()方法的返回值确定要删除元素的索引。因为只删除一个元素,Array.splice()的第二个参数应该传入1。
方法removeAllCuePoints(), 是简单的把_cuePoints替换成一个新的空数组.
重写start(), stop(), 和loadSound() 方法
基类Sound有两个方法可以开始播放:Sound.start() 和 Sound.loadSound()。不管用那一个,SoundSync 都要开始不断的轮询音频的位置,和每一个提示点的time属性比较。同样的,调用了Sound.stop() 之后轮询要停止。
为了实现这一点, SoundSync重写了这三个方法,调用基类的方法完成基本功能之外执行附加的命令。
在removeAllCuePoints() 方法之后输入:
// Start
public function start(secondOffset:Number, loops:Number):Void {
super.start(secondOffset, loops);
dispatchEvent({type:”onStart”, target:this});
// Reset current cue point
_secondOffset = secondOffset;
_currentCuePoint = getNextCuePointIndex(secondOffset);
// Poll for cue points
clearInterval(_interval);
_interval = setInterval(Delegate.create(this, pollCuePoints), _intervalDuration);
}
// Load Sound
public function loadSound(url:String, isStreaming:Boolean):Void {
super.loadSound(url, isStreaming);
clearInterval(_interval);
_interval = setInterval(Delegate.create(this, pollCuePoints), _intervalDuration);
}
// Stop
public function stop(linkageID:String):Void {
if (linkageID) {
super.stop(linkageID);
} else {
super.stop();
}
dispatchEvent({type:”onStop”, target:this});
// Kill polling
clearInterval(_interval);
}
一开始, start()方法调用super.start()并传入两个可选参数。然后,发送onStart事件,提示音频的播放已经开始了。
两个重要的私有属性在这里根据可选参数secondOffset被赋值。第一个_secondOffset只是简单的copy以备用。第二个_currentCuePoint,以secondOffset作为getNextCuePointIndex()的参数计算得到。请注意,这里secondOffset 是一个可选参数(可能是null),所以getNextCuePointIndex()的确应该做那一检查。
最后,如果setInterval()的循环已经在运行,要用clearInterval()取消并开始新的轮询。静态方法Delegate.create()让轮询的作用域是这个类本身而不是setInterval()这个函数。
当然,开发人员可能使用loadSound()而绕过了start()。这种情况SoundSync的loadSound()是调用super.loadSound()并传入其期望的参数,然后启动轮询。
要节约处理器周期,一个好办法是当调用stop()之后停止轮询。因为linkageID在基类Sound中是一个可选参数,SoundSync的该函数要检查这个参数是否存在再调用合适的super.stop()。之后发送一个 onStop 事件并取消轮询。
这个类的灵魂: pollCuePoints()
把前面的一切整合起来! 在stop() 方法之后输入:
private function pollCuePoints():Void {
// If current position is near the current cue point’s time …
// 如果当前位置接近提示点的time
var time:Number = _cuePoints[_currentCuePoint].time;
var span:Number = (_cuePoints[_currentCuePoint + 1].time) ? _cuePoints[_currentCuePoint + 1].time : time + _intervalDuration * 2;
if (position >= time && position <= span) {
// Dispatch event 发送事件
dispatchEvent(_cuePoints[_currentCuePoint]);
// Advance to next cue point … 下一个
if (_currentCuePoint < _cuePoints.length) {
_currentCuePoint++;
} else {
_currentCuePoint = getNextCuePointIndex(_secondOffset);
}
}
}
pollCuePoints()方法有点繁重但是很容易理解。这个方法每50毫秒调用一次,检查正在播放的音频的Sound.position属性,以比较当前和下一个提示点对象的time属性。如果position大于等于当前的提示点的time,但是小于等于下一个提示点time,那就把当前的提示点作为事件发送出去。
注意两个局部变量:time(时间) 和 span (间隔)。第一个是当前提示点time属性的值。而第二个可能是下一个提示点time属性的值——如果当前提示点的后面还有另外一个——或者当前提示点的time值加上轮询时间间隔的两倍。这个乘法是武断的,不过事实证明还很好用,何况只有到数组的最后一个提示点的时候才会用到。
cuePoint事件一发出,如果还有更多的提示点的话,_currentCuePoint就加1。没有了的话,音频就播放到结束,可能又要接着从头播放了,这样_currentCuePoint又要根据secondOffset被重置为最开始的那个提示点。
利用基类的事件处理
即使没有直接调用stop(),音频自己也会停止。这时轮询应该停止,请在// EVENT HANDLERS 事件处理 的注释后面写:
// onSoundComplete
public function onSoundComplete():Void {
// Kill polling
clearInterval(_interval);
// Reset current cue point
_currentCuePoint = 0;
// Dispatch event
dispatchEvent({type:”onSoundComplete”, target:this});
}
不幸的是,这样占去了一个Sound类很有用的事件(在AS2.0中有两种”事件处理”一种是比如onEnterFrame的回调函数,另一种是EventDispatcher这样使用AddEventListener注册监听器来处理的事件,这里的onSoundComplete函数是前者,而后面的dispatchEvent({type:”onSoundComplete”, target:this});属于后者,原文并没有区分,翻译的人解释道)。作为一个解决办法,SoundSync在clearInterval()中断轮询,_currentCuePoint重置为0之后,发送一个onSoundComplete来替代。这样一来,还是在音频播放完后触发动作的。
使用ActionScript 2.0版本的SoundSync
SoundSync 类使用简单。在需要音频提示点的FLA文件或其他ActionScript类中,实例化SoundSync来代替Sound类。之后要调用addCuePoint(),并创建监听对象来处理cuePoint 事件:
import net.quip.sound.SoundSync;
var ss:SoundSync = new SoundSync();
ss.addCuePoint(”first”, 1000);
ss.addCuePoint(”second”, 2000);
ss.addCuePoint(”third”, 3000);
ss.loadSound(”sample.mp3″, true);
var listener:Object = new Object();
listener.cuePoint = function(evt:Object):Void {
trace(evt.name + “, ” + evt.time);
}
ss.addEventListener(”cuePoint”, listener);
观看基于timeline的高级演示,请解压文章附带的ZIP文件,打开green_presidents.fla 。确认附带的音频文件green_presidents.mp3在和FLA文件相同的文件加下。并确认ActionScript 2.0版的SoundSync.as文件在相对全局classpath设置的目录为net/quip/sound文件夹下。
ActionScript 3.0的SoundSync
啊!准备好再来一次了吗?SoundSync 的ActionScript 3.0 版和其ActionScript2.0版非常接近。当然有一点差异,但是并不吓人。
创建ActionScript 3.0 的类文件
启动Flex Builder 2 菜单选择 File > New > ActionScript Project 见图2
---
|图2 |
| |
---
给工程起名叫SoundSyncExample 然后点Finish(完成)按钮。这样就用 Flex Builder 2创建了一个新工程,工程包括bin和html-template文件夹,Flex和一个新的SoundSyncExample.as类文件将在发布时用到这两个文件夹,而SoundSyncExample将作为新应用程序的入口。SoundSyncExample将实例化ActionScript 3.0 版的S SoundSync;这对应的是上一节”使用 ActionScript 2.0 “的Flash 8的桢代码
看到 Flex Builder 2 自动的生成了包和类的声明,见图三,我们一会来添加内容。暂时保存和关闭它。
---
|图3 |
| |
---
在工程中创建net.quip.sound 包的文件夹,选择File > New > Folder 。在New Folder(新文件夹)对话框,选择SoundSyncExample 文件夹在Folder Name(文件夹名)文本框中输入路径net/quip/sound。
最后,选择File > New > ActionScript Class。在New ActionScript Class(新ActionScript类) 对话框中,确认Package 文本框中是net.quip.sound 。在Name 项中输入SoundSync并在Superclass 项中输入flash.media.Sound 。选中Generate Constructor from Superclass (由父类生成构造函数)选项。见图4
---
|图4 |
| |
---
和前一版一样,SoundSync 继承Sound 类。点 Finish(完成) 按钮。看到Flex Builder 2 又自动的生成了包和类的声明,并在SoundSync的构造函数中调用了super。
在ActionScript 3.0中,import语句不仅是为了方便,它们是必须的。作为代码自动生成功能的一部分,Flex Builder 2 会自动写好需要的import语句。不过,为了完成教程的学习,请把代码以及注释手工的添加到相应的位置。
package net.quip.sound
{
import flash.events.Event;
import flash.events.TimerEvent;
import flash.media.Sound;
import flash.media.SoundChannel;
import flash.media.SoundLoaderContext;
import flash.media.SoundTransform;
import flash.net.URLRequest;
import flash.utils.Timer;
public class SoundSync extends Sound
{
// PROPERTIES
// CONSTRUCTOR
public function SoundSync(stream:URLRequest=null, context:SoundLoaderContext=null)
{
super(stream, context);
init();
}
// METHODS
// EVENT HANDLERS
}
}
不要忘记构造函数中是要调用init()的!
声明属性
在// PROPERTIES 注释之后输入:
private var _cuePoints:Array;
private var _currentCuePoint:uint;
private var _timer:Timer;
private var _timerInterval:uint;
private var _startTime:Number;
private var _loops:uint;
private var _soundChannel:SoundChannel;
这里 _cuePoints 和 _currentCuePoint 与ActionScript 2.0版的那两个完全一样。 _timer 和 _timerInterval 属性 对应于那一版的_interval 和 _intervalDuration,而_startTime对应于之前的_secondOffset。最后,_loops 和 _soundChannel 是新的属性,将在后面逐步讲解。
注意到一些数值属性是新的数据类型uint。在ActionScript 3.0里,我们已经熟悉的Number表示的是双精度浮点数,表示Number要使用多达53???位 -_o。新的int类型是32为有符号整数(正数或者负数)而uint是32位无符号整数(只有正数)。Flash Player 处理int和uint比Number更加有效,所以如果不是必须使用浮点数应该选择新的数据类型。
构造函数
构造函数已经更新过了, 但是还是来回顾一下:
// CONSTRUCTOR
public function SoundSync(stream:URLRequest = null, context:SoundLoaderContext = null) {
super(stream, context);
init();
}
看起来很熟悉吧,因为这个和ActionScript 2.0版本非常相似。和以前一样,在父类初始化之后进行了这个类自己的初始化。不过注意到,在 ActionScript 3.0中可选的参数要给定一个默认值(这里两个都是null)
方法
大多数都熟悉了.
init()
在 // METHODS 注释之后输入:
// init
private function init():void {
_cuePoints = new Array();
_currentCuePoint = 0;
_timerInterval = 50;
_startTime = 0.0;
}
在这里用默认值进行了一些私有属性的初始化。注意函数返回类型的那个小写的”v”,区别于ActionScript 2.0中Void类型的大写V
addCuePoint()
这个方法是离开ActionScript 2.0的第一个里程碑 –b要怎么翻这句。在ActionScript 3.0 事件处理进行了很大变动,变得一致,易于控制和使用。为了通过类型检查,必须把_cuePoints数组中的一般Object对象替换成一个自定义类型CuePointEvent的对象,而CuePointEvent继承自新的Event类。
选择File > New > ActionScript Class。在New ActionScript Class (新ActionScript类)对话框,确认Package (包)文本框是 net.quip.sound;在Name(类名) 文本框中 输入CuePointEvent 并在Superclass(父类) 文本框中输入Event。选中Generate Constructor from Superclass (由父类生成构造函数)选项然后点Finish(完成) 按钮。把代码修改成和下面的一致:
package net.quip.sound
{
import flash.events.Event;
public class CuePointEvent extends Event
{
// PROPERTIES
public static const CUE_POINT:String = “cuePoint”;
public var name:String;
public var time:uint;
// CONSTRUCTOR
public function CuePointEvent(type:String, cuePointName:String, cuePointTime:uint, bubbles:Boolean = false, cancelable:Boolean = false)
{
super(type, bubbles, cancelable);
this.name = cuePointName;
this.time = cuePointTime;
}
// METHODS
// Clone
public override function clone():Event
{
return new CuePointEvent(type, name, time, bubbles, cancelable);
}
}
}
这个类有三个公有属性,其中第一个是常量:字符串”cuePoint”。使用这个属性的原因是,按照最佳实践推荐,这样做使事件的类型可以在外部的代码中使用CuePointEvent.CUE_POINT而不是字符串来指定。第二个和第三个属性用于保存提示点的name和time。
修改完代码之后,留意一下构造函数的参数。CuePointEvent 需要type, cuePointName 和 cuePointTime 然后是一组用于Event类的可选参数。调用super的时候按照Event的构造函数的需要提供参数。其他的赋值给对应的属性。
最后重写Event。clone()方法来返回一个派生类CuePointEvent的实例而不是原本的Event类的。
现在要写SoundSync.addCuePoint()方法了。回到SouncSync.as文档并在其init()方法之后输入:
// Add Cue Point
public function addCuePoint(cuePointName:String, cuePointTime:uint):void {
_cuePoints.push(new CuePointEvent(CuePointEvent.CUE_POINT, cuePointName, cuePointTime));
_cuePoints.sortOn(”time”, Array.NUMERIC);
}
除了要求使用Event的派生类,这个方法和ActionScript 2.0 版的几乎一样。
getCuePoint()
比起之前的版本只有一个变化是局部变量的计数器使用uint类型代替Number类型:
// Get Cue Point
public function getCuePoint(nameOrTime:Object):Object {
var counter:uint = 0;
while (counter < _cuePoints.length) {
if (typeof(nameOrTime) == “string”) {
if (_cuePoints[counter].name == nameOrTime) {
return _cuePoints[counter];
}
} else if (typeof(nameOrTime) == “number”) {
if (_cuePoints[counter].time == nameOrTime) {
return _cuePoints[counter];
}
}
counter++;
}
return null;
}
更多的相同
下面的四个方法getCurrentCuePointIndex(), getNextCuePointIndex(), removeCuePoint()
和removeAllCuePoints()。这些只是代码移植中非常一般的变化。一些原来是Number类型的用uint替换,Void换成void然后解决getNextCuePoint() 方法中两个小问题的办法有一些变化。这一次传入的参数已经是以毫秒为单位的了然后要用isNaN()函数来检查可能传进的null值,因为ActionScript3.0对数值型的检查更加严格。
在 getCuePoint() 方法之后输入:
// Get Current Cue Point Index
private function getCurrentCuePointIndex(cuePoint:CuePointEvent):uint {
var counter:uint = 0;
while (counter < _cuePoints.length) {
if (_cuePoints[counter].name == cuePoint.name) {
return counter;
}
counter++;
}
return null;
}
// Get Next Cue Point Index
private function getNextCuePointIndex(milliseconds:Number):uint {
if (isNaN(milliseconds)) {
milliseconds = 0;
}
var counter:uint = 0;
while (counter < _cuePoints.length) {
if (_cuePoints[counter].time >= milliseconds) {
return counter;
}
counter++;
}
return null;
}
// Remove Cue Point
public function removeCuePoint(cuePoint:CuePointEvent):void {
_cuePoints.splice(getCurrentCuePointIndex(cuePoint), 1);
}
// Remove All Cue Points
public function removeAllCuePoints():void {
_cuePoints = new Array();
}
play()
和原来的类有两个主要的不同。在ActionScript 3.0中,既没有start() 也没有loadSound() 方法。最接近的是play() 和 load()。两者中,只有前者能开始播放音频。这就是说只要重写play()方法就够了,但是要多解释一下。
在removeAllCuePoints() 方法之后输入:
// Play
public override function play(startTime:Number = 0.0, loops:int=0, sndTransform:SoundTransform=null):SoundChannel {
_soundChannel = super.play(startTime, loops, sndTransform);
_soundChannel.addEventListener(Event.SOUND_COMPLETE, onSoundComplete);
dispatchEvent(new Event(”play”));
// Reset current cue point
_startTime = startTime;
_loops = 0;
_currentCuePoint = getNextCuePointIndex(startTime);
// Poll for cue points
_timer = new Timer(_timerInterval);
_timer.addEventListener(TimerEvent.TIMER, pollCuePoints);
_timer.start();
return _soundChannel;
}
注意函数声明中的override关键字,这里是必须的。在 ActionScript 3.0中Sound.play()方法返回一个SoundChannel 类的实例,使用它来控制正在播放音频的声音效果。自定义类SoundSync的私有成员 _soundChannel 将引用这个实例,然后立刻使用它来监听表示播放完毕的soundComplete事件。这样也使在合适时取消轮询变得可能。
发送一个自定义的play事件。接着,保存一下 play()方法的两个可选参数startTime和loops以备用。事实上,私有属性_loops并不总和loops参数一致,后者表示了音频应该重复的次数。在这个类中,_loops会被 pollCuePoints() 累加,_loops是为了完成一些必须的算法。在ActionScript 2.0里,当循环的音频播放完毕而重复时Sound.position属性会置0,这样就方便了重置提示点。在ActionScript 3.0里,对应的属性是SoundChannel.position,可是SoundChannel.position不会自动置0。于是,当音频需要重复是_loops就被设计用来乘以SoundChannel.position的值。
然后,_currentCuePoint属性和之前的一样由 getNextCuePointIndex()来设定。使用新的Timer类代替setInterval()函数来实现轮询。这里的_timer保存一个对Timer 类实例的引用并负责监听TimerEvent.TIMER事件来触发pollCuePoints()方法。最后返回_soundChannel满足重写的Sound.play()方法。
stop()
和原来的版本类似,这个方法在明确被调用而停止音频的播放时取消轮询。注意这个方法并不是override的, 因为ActionScript 3.0中停止一个声音是由SoundChannel类控制的。在play()方法的下面输入:
// Stop
public function stop():void {
_soundChannel.stop();
dispatchEvent(new Event(”stop”));
// Kill polling
_timer.stop();
}
pollCuePoints()
ActionScript 3.0 中的”类的灵魂”和ActionScript 2.0的非常类似。记住SoundChannel.position对循环播放的音频并不会置0,所以如果循环播放要用内部的 _loops 属性乘以SoundChannel.position。此外的都是以前一样:检查当前提示点和下一个提示点的时间属性。如果音频的position 在当前提示点和下一个提示点之间,发送cuePoint事件。当没有下一个提示点时,检查一个武断的值,它是当前提示点的time值加上轮询时间间隔的两倍。
在stop()方法之后输入:
// Poll Cue Points
private function pollCuePoints(event:TimerEvent):void {
var time:Number = _cuePoints[_currentCuePoint].time + (length * _loops);
var span:Number = 0;
if (_cuePoints[_currentCuePoint + 1] == undefined) {
span = time + _timerInterval * 2;
} else {
span = _cuePoints[_currentCuePoint + 1].time + (length * _loops);
};
if (_soundChannel.position >= time && _soundChannel.position <= span) {
// Dispatch event
dispatchEvent(_cuePoints[_currentCuePoint]);
// Advance to next cue point …
if (_currentCuePoint < _cuePoints.length - 1) {
_currentCuePoint++;
} else {
_currentCuePoint = getNextCuePointIndex(_startTime);
_loops++;
}
}
}
利用同样的基类事件
和以前一样,要在音频文件自然结束时取消轮询。并发送soundComplete 来弥补已经用掉了一个。
在// EVENT HANDLERS 注释之后输入:
// onSoundComplete
public function onSoundComplete(event:Event):void {
// Reset current cue point
_currentCuePoint = 0;
// Kill polling
_timer.stop();
// Dispatch event
dispatchEvent(new Event(Event.SOUND_COMPLETE));
}
使用ActionScript 3.0版本的SoundSync
还记得SoundSyncExample.as文件吧? 现在该打开它了。
把代码改成:
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.net.URLRequest;
import net.quip.sound.CuePointEvent;
import net.quip.sound.SoundSync;
public class SoundSyncExample extends Sprite
{
// CONSTRUCTOR
public function SoundSyncExample()
{
var ss:SoundSync = new SoundSync();
ss.load(new URLRequest(”green_presidents.mp3″));
ss.addCuePoint(”Benjamin Franklin”, 20100);
ss.addCuePoint(”Ulysses S. Grant”, 16479);
ss.addCuePoint(”Alexander Hamilton”, 9431);
ss.addCuePoint(”Andrew Jackson”, 13278);
ss.addCuePoint(”Thomas Jefferson”, 2439);
ss.addCuePoint(”Abraham Lincoln”, 5480);
ss.addCuePoint(”George Washington”, 0);
ss.load(new URLRequest(”green_presidents.mp3″));
ss.play();
ss.addEventListener(CuePointEvent.CUE_POINT, onCuePoint);
ss.addEventListener(Event.SOUND_COMPLETE, onSoundComplete);
}
// On Cue Point
private function onCuePoint(event:CuePointEvent):void {
trace(”Cue point: ” + event.name + “, ” + event.time);
}
// On Sound Complete
private function onSoundComplete(event:Event):void {
trace(”Audio finished: ” + event.type);
}
}
}
解压文章附带的ZIP文件后,把green_presidents.mp3放在Flex Builder 2 工程的文件夹下。以windows XP为例,默认的路径是C:\Documents and Settings\<用户名>\My Documents\Flex Builder 2\<工程名>
选择Run > Debug SoundSyncExample。 将启动默认浏览器来播放包含应用程序的SWF文件。当SWF文件在浏览器中运行时,切换到Flex Builder 2 观察Console (控制台)面板,当达到提示点时发现trace()的输出。看,虽然这几为总统先生按照姓氏字母的先后被排近来,它们还是按照时间顺序被触发。
如果有Flash Professional 9 ActionScript 3.0 Preview public alpha 发行版 –b,也可以用来测试。确认全局classpath设置指向含有package(net/quip/sound)的那个文件夹,然后打开一个新的FLA文件。确认发布设置是 ActionScript 3.0 然后设置Document Class(文档类)文本框中是SoundSyncExample。保存FLA文件到Flex Builder 2 文件夹的SoundSyncExample的子文件夹。测试SWF文件,注意观察Ouput(输出)面版。
进一步的学习
在本教程中讲述了使用ActionScript 2.0 及 3.0创建自定义SoundSync类。代码分别使用良种语言扩展了内建的类Sound,向其添加了提示点功能来回放音频文件。教程介绍了ActionScript两个版本的一些不同,有新的数据类型和相似的对象的结构上的变化。
代码升级/移植是叫人难受的,但是我希望这个练习可以减少些盲目的恐惧。ActionScript 3.0的前景被大大的拓宽了,但是它还是ActionScript。伴随你的足迹,探险会更激动人心。
学习更多关于 ActionScript 2.0,请参考下面的资源:
Learning ActionScript 2.0 in Flash (LiveDocs)
ActionScript 2.0 best practices
Flash ActionScript 2.0 Language Reference (LiveDocs)
Debugging ActionScript 2.0 code: Lifting the blindfold
学习更多关于 ActionScript 3.0, 包括移植到新语言的技巧, 请参考下面的资源:
ActionScript 3.0 overview (LiveDocs)
Tips for learning ActionScript 3.0
ActionScript 2.0 migration (LiveDocs)
Programming ActionScript 3.0 (LiveDocs)
Flex 2 Language Reference (LiveDocs)
关于作者
David Stiller是一位职业多媒体程序员/设计师。曾在NASA,Adobe和美国其他的主要汽车制造及造船公司任职。他喜欢立体摄影艺术,精致的木玩具船,还有土耳其咖啡。David自学成材,乐于和人分享在Flash 及ActionScript 论坛上学习,讨论和帮助的快乐。他是Community MX站点的驻站作者,该站是一个针对adobe产品的开发培训网站。David现在和他迷人的妻子Dawn还有可爱的女儿Meridian居住在维吉尼亚。