Accessibility

Table of Contents

Handling cue points for audio files in ActionScript 2.0 and ActionScript 3.0

SoundSync class in ActionScript 2.0

The code in this custom class must be saved in an external text file named SoundSync.as. According to recommended best practices, you should place this file inside nested folders that reflect the class's package structure. In this case, you'll need a folder named sound inside a folder named quip inside a folder named net that resides within a folder specified in your global classpath setting. (In Flash CS3, select the Edit > Preferences > ActionScript category > ActionScript 2.0 Settings button.) I personally keep one folder for ActionScript 2.0 classes and another for ActionScript 3.0 classes. When you encounter the ActionScript 3.0 version of this class later in the article, you'll repeat these first few steps, including the creation of nested package folders inside the root ActionScript 3.0 classpaths folder of your choice. In either case, the net.quip.sound package is based on my domain name by convention; feel free to personalize the package structure to suit your needs. For details, see "About setting and modifying the classpath" under "Learning ActionScript 2.0 in Adobe Flash" in the Flash CS3 documentation.

Creating the ActionScript 2.0 class file

Launch Flash CS3 Professional and select File > New > General tab > ActionScript File to begin (see Figure 1).

Starting a new ActionScript file in Flash CS3 

Figure 1. Starting a new ActionScript file in Flash CS3

Type the following ActionScript into your empty file:

import mx.events.EventDispatcher;
class net.quip.sound.SoundSync extends Sound {
  // PROPERTIES
  // CONSTRUCTOR
  // METHODS
  // EVENT HANDLERS
}

Save this file as SoundSync.as in the net/quip/sound folder structure described earlier.

The first line imports a native class, EventDispatcher, used later in the code. In ActionScript 2.0, the import statement is a convenience that allows you to reference a class without having to type its full package name.

Next, the class is declared as a derivative of the base Sound class. This is followed by a handful of commented placeholders for properties, the constructor, methods, and event handlers. While these comments are not essential to the class, I generally find it useful to block out the overall structure and fill in the details as I go.

Declaring the properties

Type the following ActionScript after the // PROPERTIES comment:

private var _cuePoints:Array;
private var _currentCuePoint:Number;
private var _interval:Number;
private var _intervalDuration:Number;
private var _secondOffset:Number;
// Event dispatcher
public var addEventListener:Function;
public var removeEventListener:Function;
private var dispatchEvent:Function;

By convention, private variable names begin with an underscore (_). This provides a quick visual reminder that the property in question is for internal use only (that is, inaccessible outside the class). Each of these properties will be explained as the class unfolds. Note, however, that the final three lines (highlighted) are required by EventDispatcher, which allows this class to dispatch events related to the goings-on of any SoundSync instance, including cuePoint events. The private variable in this last bunch is something of an oddball because it does not begin with an underscore. But every rule has its exception, and because we didn't write these functions, we'll roll with what we've been given.

The constructor

Type the following ActionScript after the // CONSTRUCTOR comment:

public function SoundSync(target:MovieClip) {
  super(target);
  init();
}

This function ensures that SoundSync can be used in the same way as any Sound instance. The super statement invokes the superclass version of the constructor (that is, the base class's constructor), which accepts an optional MovieClip reference. Anything the base class does to initialize itself now occurs here, too, thanks to this statement. This is followed by a custom init() method, described next, which sets up additional initialization.

The methods

This class requires a bit of its own preparatory housekeeping. You'll accomplish this with an init() method.

init()

Type the following ActionScript after the // METHODS comment:

private function init():Void {
  // Initialize properties
  _cuePoints = new Array();
  _currentCuePoint = 0;
  _intervalDuration = 50;
  _secondOffset = 0;
  // Initialize class instance as valid event broadcaster
  EventDispatcher.initialize(this);
}

Here, several of the class properties are initialized to their default values. The _cuePoints property is an Array instance that holds references to each cue point object, using _currentCuePoint as its index. The _intervalDuration property determines the span of time, in milliseconds, between repeated checks of the audio's current position. The _secondOffset property specifies the number of seconds that should pass before the audio starts to play (by default, this is zero).

The final line invokes the static EventDispatcher.initialize() method with a parameter of this, which establishes each instance of this class as an object capable of dispatching events.

addCuePoint()

At this point, you're already geared up for the addCuePoint() method. Type the following ActionScript after the init() method:

// 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);
}

This method accepts two parameters, cuePointName and cuePointTime, which represent the name and temporal position of a given cue point. These are used as properties of a generic Object instance stored in the _cuePoints array via the Array.push() method; the curly braces, {}, are shorthand for invoking the new Object() constructor. In addition, each cue point object needs a type property, which is always set to the string "cuePoint". Finally, a target property refers to the SoundSync instance (this) that dispatched the event.

As each cue point object is added, the _cuePoints array is sorted numerically by the cuePoint.time property. In this way, any cue points added out of sequence will still be positioned correctly.

getCuePoint()

For the sake of symmetry, let's provide a way to retrieve cue points after they've been added. Type the following ActionScript after the addCuePoint() method:

// 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;
}

This method accepts a single parameter, either the cue point's name or time, which means the parameter might be a string or a number. To cover either possibility, the parameter is typed as Object. Inside the method, a simple counter variable, whose purpose is to step through the _cuePoints array, is declared and set to zero. In a while() loop, the nameOrTime parameter is checked for its specific type with the typeof operator. If it is a string, nameOrTime is compared with the current cue point's name property; if it is a number, it is compared with the cue point's time property. Because counter is incremented with each loop, the first occurrence of a given name or time is returned, even if there are duplicates. If no match is found, the method returns null.

getCurrentCuePointIndex() and getNextCuePointIndex()

These methods are "behind the scenes" functions that are only accessed inside the class by other methods. Their purpose is to return the current or next cue point's position (index) in the _cuePoints array, used respectively to remove cue points and determine which cue point to focus on next.

Type the following ActionScript after the getCuePoint() method:

// Get Current Cue Point Index
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;
}
// Get Next Cue Point Index
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;
}

The first method, getCurrentCuePointIndex(), accepts a cue point object as its only parameter. Employing the same while() mechanism used earlier, this method checks the name property of the parameter object against the name property of each cue point object in the _cuePoints array. If a match occurs, the method returns the number value of the current index in the array; otherwise, null.

The second method, getNextCuePointIndex(), is a little more interesting. The purpose of this method is to determine which cue point to look for next, depending on the specified time. Imagine three cue points exist at 10, 20, and 30 seconds, respectively. If the audio has been playing for 22 seconds, the next pertinent cue point is the one set at 30 seconds — that is, the third cue point in this example, or index 2 (because arrays start at zero).

This method is called by two others in the class, start() and pollCuePoints(), discussed later in this article. Two circumstances pose a challenge here:

  • It's entirely possible that the seconds parameter may not be provided, in which case its value would be null
  • A cue point's time property is stored in milliseconds but the nature of the base Sound class in ActionScript 2.0 requires a parameter in seconds

Fortunately, both challenges are easy to resolve. First, the conditional operator (?:) checks the value of seconds in the parentheses preceding the question mark. If the value is nonzero, the value is simply set to itself (it stays the same); otherwise, a default value of zero is assigned.

Next, a while() loop once again steps through the _cuePoints array. Each cue point's time property is compared against the seconds parameter multiplied by 1,000 (thus, seconds are converted to milliseconds). Because this loop starts at zero, the first seconds time slot equal to or greater than a given cue point's time property represents the next relevant cue point.

removeCuePoint() and removeAllCuePoints()

The next two methods allow you to remove an existing cue point or all cue points directly. Type the following ActionScript after the getNextCuePointIndex() method:

// Remove Cue Point
public function removeCuePoint(cuePoint:Object):Void {
  _cuePoints.splice(getCurrentCuePointIndex(cuePoint), 1);
}
// Remove All Cue Points
public function removeAllCuePoints():Void {
  _cuePoints = new Array();
}

The first method, removeCuePoint(), accepts a cue point object as its only parameter. The Array.splice() method is invoked on the _cuePoints array to remove the specified element. The index of this element is determined by the return value of the getCurrentCuePointIndex() method outlined above. Because only a single element should be removed, 1 is supplied as the second parameter to Array.splice().

The second method, removeAllCuePoints(), simply replaces the existing _cuePoints array with a new, empty Array instance.

Overriding the start(), stop(), and loadSound() methods

There are two ways to begin playing audio with the base Sound class in ActionScript 2.0: Sound.start() and Sound.loadSound(). When either of these occurs, SoundSync must begin continuously polling the audio's position to compare it against the time property of each cue point object. Likewise, polling should stop when the Sound.stop() method is called.

To accomplish this feat, SoundSync provides its own version of these methods, invoking super to let the base class manage its own functionality, and then following with additional instructions.

Type the following ActionScript after the removeAllCuePoints() method:

// 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(this, "pollCuePoints", _intervalDuration);
}
// Load Sound
public function loadSound(url:String, isStreaming:Boolean):Void {
  super.loadSound(url, isStreaming);
  clearInterval(_interval);
  _interval = setInterval(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);
}

Right off the bat, the start() method calls super.start() and passes it the two optional parameters available to that method. Next, an onStart event is dispatched to alert you that audio has started playing.

Two important private properties are set based on the value of the optional secondOffset parameter. The first is simply a copy of the parameter's value for later use; the second, _currentCuePoint, is derived from getNextCuePointIndex(), with secondOffset as its parameter. Remember that secondOffset is not a required parameter (it may be null) so it's a good thing getNextCuePointIndex() accounts for that!

Finally, if a setInterval() cycle is already in play, it's cancelled by clearInterval() and a new cycle of cue point polling begins. There are two ways to use the setInterval() function, and the usage here—where the first parameter is this—sets the scope of this polling to the class itself rather than to the setInterval() function.

Of course, the developer might bypass the start() method altogether by using loadSound(). In this case, the SoundSync version calls super.loadSound() and passes it the parameters it expects. Then, as before, polling is set in motion.

To save processor cycles, it's a good idea to stop polling if the developer invokes the stop() method. Because the linkageID parameter is optional in the base Sound class, the SoundSync version tests to see if it's present and invokes super.stop() as appropriate. An onStop event is dispatched and polling is cancelled with clearInterval().

The brains of the class: pollCuePoints()

Here's where it all comes together! Type the following ActionScript after the stop() method:

private function pollCuePoints():Void {
  // If current position is near the current cue point's 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);
    }
  }
}

The pollCuePoints() method is a little densely packed but straightforward. Called every 50 milliseconds (approximately 20 times a second), this method compares the Sound.position property of the playing audio with the current and next cue point objects' time properties. If position is greater than or equal to the current cue point's time and less than or equal to the next cue point's time, the current cue point object is dispatched as an event.

Note the two local variables: time and span. The first is simply set to the current cue point's time value. The second is set either to the next cue point's time value — if another cue point follows — or to the current object's time value plus twice the time span in which polling is invoked. This multiplication is an arbitrary formula but it bears out fairly well and is used only when the final cue point in the array is encountered.

As soon as a cuePoint event is dispatched, _currentCuePoint is incremented if additional cue points remain. If not, because the audio has reached an end and may be looped, then _currentCuePoint is reset to the earliest relevant cue point based on your optional secondOffset preference.

Commandeering a base class event

Even if you do not explicitly invoke stop(), the audio will eventually stop on its own. Polling should be cancelled when this happens, so type the following ActionScript after the // EVENT HANDLERS comment:

// onSoundComplete
public function onSoundComplete():Void {
  // Kill polling
  clearInterval(_interval);
  // Reset current cue point
  _currentCuePoint = 0;
  // Dispatch event
  dispatchEvent({type:"onSoundComplete", target:this});
}

Unfortunately, this hijacks a useful event of the Sound class. As a consolation, however, SoundSync dispatches a replacement onSoundComplete event after clearInterval() halts the polling and _currentCuePoint is reset to zero. In this way, you are still allowed to trigger actions based on the audio's completion.

Using the ActionScript 2.0 version

The SoundSync class is easy to use. Wherever audio cue points are required in a FLA file or another ActionScript class, instantiate SoundSync in place of Sound. After invoking addCuePoint() as necessary, set up a listener object to handle cuePoint events:

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);

To see a demonstration that advances the timeline based on cue points, open green_presidents_as2.fla from the ZIP file that accompanies this article (this file opens in Flash 8 as well as Flash CS3). Make sure the companion audio file, green_presidents.mp3, is in the same folder as the FLA file. Also, make sure that the ActionScript 2.0 version of SoundSync.as is in a folder named "sound" inside a folder named "quip" inside a folder named "net" that is accessible to your global classpaths setting.