Advertisement

Beginning Audio Programming in AS3

by

Flash Player 10 introduced new low-level APIs for manipulating audio with AS3. In this tutorial, you'll learn about these APIs and how they work, and use them to create a simple app that can play MP3s in reverse.

Click here to view a preview of the SWF we'll be building in this tutorial. Click on the "Play" button to play the sound. You can't really tell by looking at or listening to it, but this isn't just an MP3 loaded and then played normally; the MP3 is being used as a sound source and samples are fed dynamically to the Sound engine. To help prove it, the "Reverse" button will play the same sound, just in reverse. There is no sleight of hand here: there is only one MP3 loaded and the reversal effect is computed on the fly.


Step 1: Playing Sound: The Olde Way

First, let's make sure we're up to speed on the pre-Flash 10 method of loading and playing an MP3. That is to say, this method still works in Flash 10 and up, but we'll be exploring a more advanced and more powerful method in the remainder of the tutorial. If you are reading this tutorial for the specifics of the more advanced method, and feel you have a solid understanding of the traditional method, feel free to skip ahead to the step "Playing Sound: The Neue Way."

The general method for this process is as follows:

  1. Create a Sound object.
  2. Load an MP3 file using the load method of the Sound object.
  3. Once the MP3 loads, start playback by calling the play method.
  4. When you call play, be sure to store the SoundChannel object you'll get returned.
  5. When/If you need to stop the sound, or adjust the volume or panning, call the appropriate methods on the SoundChannel object.

To start, let's create a basic project.

  • Create a new ActionScript 3 FLA and save it to a project folder.
  • Create a document class to go along with the FLA. That is, create a new ActionScript 3.0 file, and save it as "TraditionalSound.as" in the same folder as your FLA.
  • If you need some class boilerplate, enter this into your new class file:

    package { 
        import flash.display.*; 
        import flash.events.*; 
        import flash.net.*; 
        import flash.media.*; 
        public class TraditionalSound extends Sprite { 
            public function TraditionalSound() { 
            } 
        } 
    }

Next, we'll walk through the five steps (outlined above) involved in loading and playing a sound.


Step 2: Creating a Sound Object

Create an instance property of type Sound in the class file:

 
package { 
 
    import flash.display.*; 
    import flash.events.*; 
    import flash.net.*; 
    import flash.media.*; 
 
    public class TraditionalSound extends Sprite { 
 
        private var _sound:Sound; 
 
        public function TraditionalSound() { 
        } 
    } 
}

And then instantiate it in the constructor.

 
public function TraditionalSound() { 
    _sound = new Sound(); 
}

Step 3: Load an MP3 File

To load a file, call the load method of the Sound object and pass in a URLRequest that indicates the MP3 file to load.

 
public function TraditionalSound() { 
    _sound = new Sound(); 
    _sound.load(new URLRequest("clip.mp3")); 
    _sound.addEventListener(Event.COMPLETE, onSoundLoad); 
} 
 
private function onSoundLoad(e:Event):void { 
    trace("sound loaded"); 
}

While we're loading the sound, we may as well add the "complete" event listener. You can test the movie now, and assuming you're pointing the load method at a valid MP3 file, you should see the trace as soon as it loads:


Also note that ideally you'll hook up an IOErrorEvent.IO_ERROR event listener, as well, for the situation where the MP3 file fails to load.


Step 4: Start Playback

It is possible to start the playback of a Sound before it has fully loaded, but for our purposes we'll stick with waiting until it has. So, in the onSoundLoad method, tell the Sound to play.

 
private function onSoundLoad(e:Event):void { 
    _sound.play(); 
}

Test the movie now, and you should hear your sound.


Step 5: The SoundChannel Object

Now that you have sound playing, presumably you would like to further control it: stop it, adjust the volume, etc. If this is the first time you've worked with audio in flash, you may be surprised to learn that there is no stop method on the Sound object, nor is there any way to control the volume or pan once the Sound has started playing. Instead, there is a SoundChannel object that takes care of these functions. You create a SoundChannel object by calling Sound.play, which not only starts the playback of the audio but also returns the SoundChannel object that is the playing audio. So, first, we need a property in which to store the SoundChannel object:

 
public class TraditionalSound extends Sprite { 
 
    private var _sound:Sound; 
    private var _channel:SoundChannel;

Then we need to alter our playback code to store the SoundChannel object:

 
private function onSoundLoad(e:Event):void { 
    _channel = _sound.play(); 
}

Now we can control the playing sound. For example, let's make a click on the stage stop the audio (most of the class code is listed below for reference; the new code is highlighted).

 
public class TraditionalSound extends Sprite { 
 
    private var _sound:Sound; 
    private var _channel:SoundChannel; 
 
    public function TraditionalSound() { 
        _sound = new Sound(); 
        _sound.load(new URLRequest("clip.mp3")); 
        _sound.addEventListener(Event.COMPLETE, onSoundLoad); 
        stage.addEventListener(MouseEvent.CLICK, onClick); 
    } 
 
    private function onSoundLoad(e:Event):void { 
        _channel = _sound.play(); 
    } 
 
    private function onClick(e:MouseEvent):void { 
        _channel.stop(); 
    } 
}

As another example, we'll make moving the mouse around control the volume and pan. The pan will be adjusted depending where the mouse is on the horizontal axis, and the volume will be on the vertical axis, with the bottom being muted. We just need to add a mouse move listener. In the constructor:

 
public function TraditionalSound() { 
    _sound = new Sound(); 
    _sound.load(new URLRequest("clip.mp3")); 
    _sound.addEventListener(Event.COMPLETE, onSoundLoad); 
    stage.addEventListener(MouseEvent.CLICK, onClick); 
    stage.addEventListener(MouseEvent.MOUSE_MOVE, onMove); 
}

And the listener itself:

 
private function onMove(e:MouseEvent):void { 
    var volume:Number = 1 - (e.stageY / stage.stageHeight); 
    var pan:Number = (e.stageX / (stage.stageWidth/2)) - 1; 
    _channel.soundTransform = new SoundTransform(volume, pan); 
}

The math is a little wild, but it just turns the mouse position into numbers that can be used for volume and pan. Volume can be from 0 to 1, and pan from -1 to 1. Those get fed into a SoundTransform object, which can then be set in the SoundChannel.

Now, you may be wondering why you need one object to play the sound and another to control the sound. It is a little confusing at first, but the setup allows you to have a single Sound object, that is, an audio source, and play it multiple times in multiple ways. For simply playing back a song, that's not overly useful, but imagine providing sounds for a game. If you had a sound for hitting an enemy, this sound might need to be played back many times, perhaps overlapping itself, and with separate volume and pan depending on where on the screen the hit happened. By playing back a single Sound into multiple SoundChannels, you have this kind of control, but without the need to re-create the audio source every time.

For our purposes, we won't be getting that involved, but you do need to understand the relationship between Sound and SoundChannel objects.


Step 6: Playing Sound: The Neue Way

Now that we've had a crash course in how to load and play a sound the standard way, we'll start looking at how to play a sound with the new method introduced in Flash Player 10. Our approach will first include a bit of Flash folklore, and then involve some general digital audio theory. Next we'll create the skeleton of the project for us to work in, and then spend the rest of our time on an incremental approach to writing the code, with generous explanation woven in.

A bit of history. The geniuses behind the hobnox audiotool*, namely André Michelle and Joa Ebert, were going where no Flash-based audio application had gone before, and required a more dynamic method of creating audio on the fly. While they managed to make happen in Flash Player 9, they were fed up with the less-than-powerful audio implementation in Flash. They founded a site called make-some-noise.info that was a community-powered plea to get Adobe to add audio features to Flash.

Amazingly, not only did Adobe listen, but they delivered in a matter of months, and released some previously-unexpected features with Flash Player 10. The good news is that we now have some fairly advanced tools available to us for audio manipulation. The bad news is that it really is just a small set of fairly low-level APIs that, while powerful, are just the building blocks upon which you'll need to add your own ingenuity and elbow grease. But that's what this tutorial is about, so you're covered.

(The Adobe Make Some Noise campaign is no longer around, but you can still read the posts on make-some-noise.info, as well as a series of blog posts by Tinic Uro, who is a developer at Adobe working on the Flash Player. In these posts he responds to Adobe Make Some Noise on behalf of Adobe, and provides some interesting insight to the situation as well as a look some of the first code samples. There are three posts, adding up to a good dose of information, but not too lengthy of a read. Part 1, Part 2, and Part 3.)

* Just a personal aside: The audiotool is by far the most impressive Flash piece I have ever seen.


Step 7: A Quick Explanation of Samples

To fully understand what's going on with this new technique, you need to be sure you understand the basics of digital audio. If you feel like you already have a good grasp on how digital audio works, specifically the nature of samples (that's "samples" as in 44,100 samples per second on a CD, not as in sampling a bass riff for your song), then feel free to skip ahead to the next step.

Also note that what follows is a very brief and cursory introduction to digital audio. Please understand that there is a wealth of information to be learned on this subject, and all I'm trying to do is pull out the absolute essentials as they relate to our purpose in Flash. For more information, feel free to do more research, possibly starting with this Audiotuts+ article, and perhaps including this Wikipedia article and this page of the Audacity manual, and if that's not enough, culminating with an internet search

The sounds that we hear, or that are picked up by a microphone, are changes in pressure in the air. In the case of the ear, these pressure changes hit our ear drums, and ultimately the vibrations get to our brain. In the case of a microphone, these pressure changes hit the diaphragm of the mic and are accordingly translated into changes in an electrical current, which can then be routed down a cable and into other audio devices.

We're interested in one of those other audio devices: the A/DC, or Analog-to-Digital Converter. Ultimately the audio will need to be converted into 1's and 0's so that computers and other digital audio devices can do their stuff. And the job of the A/DC is to take that analog electrical current, which pretty closely resembles the air pressure changes, and turn it into a stream of binary numbers, which really don't resemble the original sound at all.

The way it works, then, is that at a regular interval of time, the A/DC takes a sample of the analog audio. In then measures the strength of the audio signal at the moment in time, or amplitude, and records that value as a binary number. It does this sampling at regular intervals, known as the sample rate of the conversion. The faster this rate, the higher the quality of the conversion, but the more storage space and processing power is required. CD audio has a sampling rate of 441.kHz, or 44,100 samples per second.


As this audio signal continues its journey in the digital realm, a stream of samples fly by at the speed of the sample rate. These samples may be altered along the way, but they remain samples; a snapshot of the audio at a specific point in time, which can then be reassembled into an analog audio signal by a reverse process.

If this cursory explanation did not leave you with a firm grasp on the idea of samples, then please refer to the links mentioned at the beginning of this step. To fully understand what we are doing in this tutorial, as well as forthcoming Flash audio tutorials, then please do the due diligence and get up to speed.


Step 8: Set Up the Project

To get started with actual coding, we'll create our basic project. This is general boilerplate stuff, but it needs to be done. You're welcome to skip this step by simply using the advanced-sound-start project included in the download package. The sub-steps outlined in this step will simply create those file, or else serve as an explanation to those files.

  1. Create a folder for our project to reside in. Mine is named \advanced-sound-start\.
  2. Create a new Flash file:
    • Make it ActionScript 3.0.
    • Save it as AdvancedSound.fla in our project folder.
  3. Create a UI in the Flash file:
    • Draw some artwork to be a "Play" button. Turn it into a symbol, and name the instance "play_mc"
    • Draw some artwork to be a "Stop" button. Turn it into a symbol, and name the instance "stop_mc"
    • Draw some artwork to be a "Reverse" button. Turn it into a symbol, and name the instance "reverse_mc"
  4. Create a new ActionScript file to serve as our document class.

    • Save it as AdvancedSound.as in our project folder.
    • Enter the following typical document class template, if your editor does not provide that facility:

       
      package { 
          import flash.display.*; 
          import flash.events.*; 
          import flash.geom.*; 
          public class AdvancedSound extends Sprite { 
              public function AdvancedSound() { 
                  // constructor code 
              } 
          } 
      }
  5. We'll be putting our audio code into its own class, which will be contained in a package. Create the following folders:
    • [project root]
      • com
        • activetuts
          • audio
  6. Inside the audio folder, create another ActionScript file named ReversibleSound.as

    • Enter the following class template:

       
      package com.activetuts.audio { 
          import flash.events.*; 
          import flash.media.*; 
          import flash.net.*; 
          import flash.utils.*; 
          public class ReversibleSound { 
              public function ReversibleSound() { 
                  // constructor code 
              } 
          } 
      }
  7. Make sure to have an MP3 file accessible at the project root level. You can copy the clip.mp3 file provided in the download package (look in the advanced-sound-start folder) or use an MP3 of your own. If you use your own, you can rename it "clip.mp3", or else you can be on the lookout for the occurrence of "clip.mp3" in the following step and change it accordingly.

That's a lot of micro-tasks, but all in all it just adds up to having files ready for us to work in for the following steps.


Step 9: Load an MP3 File

Now let's load the MP3 file. The mechanics of the loading will be identical to what we covered earlier. However, we'll wrap that up in a load method on the ReversibleSound class. We'll also need to add and set up a property for the Sound object. Start by declaring the property, right after the class opens and before the constructor:

 
public class ReversibleSound { 
 
    private var _soundData:Sound; 
 
    public function ReversibleSound() {

Then set up it up in the constructor:

 
public function ReversibleSound() { 
    _soundData = new Sound(); 
    _soundData.addEventListener(Event.COMPLETE, onSoundLoad); 
}

Next we'll write a load method, which will receive a String url and forward it to the Sound's load method. Just after the constructor, but before the class's closing curly brace:

 
public function load(url:String):void { 
    _soundData.load(new URLRequest(url)); 
}

And finally let's add the onSoundLoad method, the COMPLETE event handler we set up in the constructor:

 
private function onSoundLoad(e:Event):void { 
    trace("sound loaded"); 
}

To test this we can hop back over the AdvancedSound class and make sure we create a ReversibleSound object and call load on it. First import the class:

 
package { 
 
    import flash.display.*; 
    import flash.events.*; 
    import flash.geom.*; 
    import com.activetuts.audio.ReversibleSound;

Next declare a property to hold the object:

 
public class AdvancedSound extends Sprite { 
 
    private var _sound:ReversibleSound; 
 
    public function AdvancedSound() {

Then instantiate it and call load in the constructor:

 
public function AdvancedSound() { 
    _sound = new ReversibleSound(); 
    _sound.load("clip.mp3"); 
}

At this point, we can test it. You won't hear any sound yet, but you should see "sound loaded" pop up in the Output panel, indicating that our objects are wired up properly and that our MP3 file is being correctly loaded.



Step 10: The Second Sound Object

Things will start to get a little confusing, so bear with me if I get a little explanation-happy.

We have our Sound object, and it's loading an MP3 file. We could simply call play and be done with it. In fact, if you're tired of this tutorial, I suggest you do just that, and stop reading, and then go get a glass of milk and a Fruit Rollup™, followed by a nap. No one will judge you.

Still here? Good, because you're about to do some wicked cool Flash programming. To do that, we need to create and store another Sound object. Here's where things may not make sense right away (and you'll be glad you skipped the milk). In a nutshell, we need one Sound to provide the audio source, and the other Sound to provide a playback mechanism. The Sound we already created, named "_soundData", is the audio source. It loads the MP3 and, in a future step, provides the samples to be played. We will now create the Sound that will play those samples.

In ReversibleSound, create another property, right below the first property we created:

 
public class ReversibleSound { 
 
    private var _soundData:Sound; 
    private var _soundPlayer:Sound; 
 
    public function ReversibleSound() {

Now in the constructor, instantiate it:

 
public function ReversibleSound() { 
    _soundData = new Sound(); 
    _soundData.addEventListener(Event.COMPLETE, onSoundLoad); 
 
    _soundPlayer = new Sound(); 
}

These isn't much to test right now, although as always, you can just run the movie to see if any errors pop up.


Step 11: Providing Sample Data

We're finally going to get into the key technique of this tutorial. As mentioned in the last step, the _soundPlayer object will provide playback, while the _soundData object provides the audio data. To make this happen, we need to add a specific event to the _soundPlayer object. We'll do that right after we create it:

 
public function ReversibleSound() { 
    _soundData = new Sound(); 
    _soundData.addEventListener(Event.COMPLETE, onSoundLoad); 
 
    _soundPlayer = new Sound(); 
    _soundPlayer.addEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData); 
}

When you add this event listener to a Sound object, you are setting it up for dynamic audio generation. The event handler (which we'll write in the next step) will get called while the Sound is playing. Once playback starts, the Sound object needs samples to play (remember that this is not a "traditional" sound with sample data inherently part of the object in the form of an MP3 file). So the SAMPLE_DATA event fires, which is basically the Sound object saying, "May I please have some samples to play?" Through this event, you can feed samples into the Sound object, and then the Sound will play them back.

As playback progresses, it will eventually run out of samples to play from the first time you supplied them. "Eventually" here means "a fraction of a second," as typically you provide between 2048 and 8192 samples - that is, between 0.0464399093 and 0.1857596372 seconds at 44.1 kHz. When the buffer of samples run low, the Sound object will fire the SAMPLE_DATA event again, at which point you provide more samples. The Sound continues playing them, and then will fire the event as the samples run low again. This process continues until you stop providing samples, or call stop on the SoundChannel.

Sidenote: If you read Tinic Uro's posts responding to Adobe Make Some Noise, you may have noticed that in Part 2 he stated that you can provide between 512 and 8192 samples. When he wrote that post, Flash Player 10 had not been released and the the dynamic sound API had not been finalized. Unfortunately, it isn't true anymore; 512 samples would be a rather low-latency option for sound applications where responsive audio is more important than conserving CPU cycles. Feel free to try it; you'll simply get no audio if you plug in an invalid number.


Step 12: Getting Samples From the Source Audio

We'll need to write that event handler now, so we can continue our discussion of the SAMPLE_DATA event.

 
private function onSampleData(e:SampleDataEvent):void { 
    var bytes:ByteArray = new ByteArray(); 
    _soundData.extract(bytes, 2048); 
    e.data.writeBytes(bytes); 
}

And this is how we feed samples into the Sound. Let's first be clear that the end result of this will be to simply play back the MP3 file that was loaded with the other Sound. To that end, this method is rather convoluted, but it helps us focus on the fundamental technique.

Let's walk through the lines of the method.

First there is the method signature, which isn't surprising, but it's worth noting the type of the event object parameter: SampleDataEvent. We'll be using this event object within the method, so we should make sure we're specific, and not use the generic Event type.

Next we create a ByteArray object. If you haven't used ByteArrays before, they can be a little confusing to work with, especially if you expect them to be pretty similar to regular Arrays. I won't get terribly in-depth with how ByteArrays work, just enough to help make us with our sound tutorial, but you should also know that the seed for a future tutorial on ByteArrays has been planted.

Suffice it to say that we create an empty ByteArray object. This is a key ingredient in the next two steps: both steps need a ByteArray as a parameter to methods we will call. It will be this ByteArray.

The second line of the method body calls extract on our source Sound object; that is, the one that we used to load the MP3 file. This is where we get samples from that Sound. This method requires a ByteArray (told ya!) as the first parameter, and the ByteArray passed in will get sample data stored in it as a result of this process. It seems weird that instead of returning a ByteArray we pass one in and have data written to it, but this is actually a common technique in C-based languages. In this case, it's more efficient to have the extract method simply put data into an existing ByteArray than it is to create ByteArray, write to it, and return it. To that end, we can make our onSampleData method more efficient by re-writing it like this:

 
public class ReversibleSound { 
 
    private var _soundData:Sound; 
    private var _soundPlayer:Sound; 
    private var _bytes:ByteArray; 
 
    public function ReversibleSound() { 
        _soundData = new Sound(); 
        _soundData.addEventListener(Event.COMPLETE, onSoundLoad); 
 
        _soundPlayer = new Sound(); 
        _soundPlayer.addEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData); 
 
        _bytes = new ByteArray(); 
    } 
 
    public function load(url:String):void { 
        _soundData.load(new URLRequest(url)); 
    } 
 
    private function onSoundLoad(e:Event):void { 
        trace("sound loaded"); 
    } 
 
    private function onSampleData(e:SampleDataEvent):void { 
        _bytes.clear(); 
        _soundData.extract(_bytes, 2048); 
        e.data.writeBytes(_bytes); 
    } 
}

This way we create a single ByteArray object, store it, and then call clear on it, rather than creating a whole ByteArray object each time we need more samples. As a rule, creating new objects is an expensive action, and it's usually more efficient to reuse an existing object and simply resetting its attributes to a neutral setting. This is a technique known as Object Pooling and that's all the more I'll say about it in this tutorial.

The second parameter of extract is the number of samples to get from the Sound. This can be one of three values: 2048, 4096, or 8192. Not that 2048 * 2 = 4096, and that 2048 * 4 = 8192. Which you choose depends on your needs. I've found that the lower the number, the more intense Flash needs to be as it is firing the SAMPLE_DATA event more frequently (because you gave it fewer samples to work last time and so it needs more samples sooner). At the same time, higher values can have an odd side effect of not being updated quickly enough. The sound itself will play fine, but sound adjustments made during playback will be deferred until the next SAMPLE_DATA event, and as you increase the time between events, the more noticeable this deference becomes. For now, it won't much matter, but I've chosen to go with the performance-hogging 2048.

Now, you may be wondering, which 2048 samples are taken from the source audio? How does Flash know where to start harvesting data? The answer is in the third parameter to extract. The third parameter specifies at which sample to start, and working forward from there. If left out, however, the Sound object does some work for you by keeping track of a pointer internally that starts at 0 and advances automatically to the first sample after your extraction. So by leaving it off, as we do here, we automatically ask for all of the samples of the MP3 in the correct order.

Finally, the final line of onSampleData takes that ByteArray, which is now chock full of sample data, and feeds it to the playback Sound by way of the data property's writeBytes method, found on the SampleDataEvent object passed in to the event handler. There isn't a way to write samples directly to the Sound; you have to go through this method. Once we pass along the samples we're done (until the next time the event fires), and the playback Sound will produce output.

We would be ready to test this, but we still need to actually tell the playback Sound to play, so let's hook up that big "play" button in the next step.


Step 13: The Play Button

This step will be a cakewalk after that last one. We just need some MouseEvent handling.

In AdvancedSound (the document class), add a CLICK listener to the play_mc button.

 
public function AdvancedSound() { 
    _sound = new ReversibleSound(); 
    _sound.load("clip.mp3"); 
 
    play_mc.addEventListener(MouseEvent.CLICK, onPlayClick); 
}

And in that event listener, call the as-yet-unwritten play method on the ReversibleSound:

 
private function onPlayClick(e:MouseEvent):void { 
    _sound.play(); 
}

Now hop back over the ReversibleSound and write the play method:

 
public function play():void { 
    _channel = _soundPlayer.play(); 
}

And of course we haven't written that _channel property yet, so declare that alongside the rest of the properties:

 
public class ReversibleSound { 
 
    private var _soundData:Sound; 
    private var _soundPlayer:Sound; 
    private var _bytes:ByteArray; 
    private var _channel:SoundChannel; 
 
    public function ReversibleSound() {

And we should be good to go. Test the movie, and click on the "play" button. If all goes well, you should hear your MP3 file play back through Flash.


Step 14: Checkpoint

For reference, here is the full code of our two classes at this point:

AdvancedSound

 
package { 
 
    import flash.display.*; 
    import flash.events.*; 
    import flash.geom.*; 
    import com.activetuts.audio.ReversibleSound; 
 
    public class AdvancedSound extends Sprite { 
 
        private var _sound:ReversibleSound; 
 
        public function AdvancedSound() { 
            _sound = new ReversibleSound(); 
            _sound.load("clip.mp3"); 
 
            play_mc.addEventListener(MouseEvent.CLICK, onPlayClick); 
        } 
 
        private function onPlayClick(e:MouseEvent):void { 
            _sound.play(); 
        } 
    } 
}

ReversibleSound

 
package com.activetuts.audio { 
 
    import flash.events.*; 
    import flash.media.*; 
    import flash.net.*; 
    import flash.utils.*; 
 
    public class ReversibleSound { 
 
        private var _soundData:Sound; 
        private var _soundPlayer:Sound; 
        private var _bytes:ByteArray; 
        private var _channel:SoundChannel; 
 
        public function ReversibleSound() { 
            _soundData = new Sound(); 
            _soundData.addEventListener(Event.COMPLETE, onSoundLoad); 
 
            _soundPlayer = new Sound(); 
            _soundPlayer.addEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData); 
 
            _bytes = new ByteArray(); 
        } 
 
        public function load(url:String):void { 
            _soundData.load(new URLRequest(url)); 
        } 
 
        private function onSoundLoad(e:Event):void { 
            //trace("sound loaded"); 
        } 
 
        private function onSampleData(e:SampleDataEvent):void { 
            _bytes.clear(); 
            _soundData.extract(_bytes, 2048); 
            e.data.writeBytes(_bytes); 
        } 
 
        public function play():void { 
            _channel = _soundPlayer.play(); 
        } 
    } 
}

Step 15: A Word About Efficiency

As you might be able to imagine, this dynamic audio stuff can be pretty intense for Flash to execute. It's pretty sturdy, but that doesn't mean it's hard to tax the system. You should do your best to keep the SAMPLE_DATA event listener as lean as you can. We did this two steps ago when we refactored the method from using a new ByteAray every time to using the ByteAray object, only clearing it every time.

Another example will be to steer clear of traces in that method. Throwing a few in just for debugging is fine, but it's easy to trace too much. Single traces here and there are fine, but they are actually somewhat of a taxing process themselves, and sticking them in repeating events, like ENTER_FRAME or fast TIMERs, can be detrimental to your Flash movie's performance. Likewise, this SAMPLE_DATA method gets called up to 22 times a second, and is itself a performance-sensitive routine, so once you're done with your trace, go ahead and comment it out or delete it altogether.


Step 16: Stopping the Sound

The last thing we accomplished was to hook up the "play" button to playing the sound; let's now wire up the "stop" button.

So we'll add a CLICK event listener to the stop_mc movie clip in AdvancedSound.as:

 
public function AdvancedSound() { 
    _sound = new ReversibleSound(); 
    _sound.load("clip.mp3"); 
 
    play_mc.addEventListener(MouseEvent.CLICK, onPlayClick); 
    stop_mc.addEventListener(MouseEvent.CLICK, onStopClick); 
}

And then write the onStopClick method:

 
private function onStopClick(e:MouseEvent):void { 
    _sound.stop(); 
}

Next we need to write the stop method on the ReversibleSound class:

 
public function stop():void { 
    _channel.stop(); 
}

And at this point, we should be able to start and stop the sound by clicking the "play" and "stop" buttons respectively.

If you play, then stop, then play again, you should notice that your sound will pick up where it left off. This is because of the Sound.extract method, and its behavior when the third parameter (position) is omitted. If you recall, not specifying a parameter means that the Sound object will use it's own internal pointer, which starts at 0 and remembers where to start again the next time its called. So we get "pausing" for free. It would be simple enough to rename the button to "pause" and make a "stop" button stop the sound and reset the position to 0, but that's a tangent I'll resist.


Step 17: Reversing the Sound

So far, all we've done is make it a lot of extra work to play an MP3 file, which Flash has been able to do for years. But the real point of dynamic sound generation is to be able to process the sound in various ways on the fly, and hear the results in real-time, without having to load multiple variations of an MP3.

For the purposes of our tutorial, we'll keep the actual processing to something simpler and enable the option of playing the sound in reverse. To do this, it's conceptually rather simple: just extract the samples from the source Sound in reverse, and feed them to the playback Sound.

However, saying "just extract the samples in reverse" and doing it are two different things. This will take a little work to get functional, but we'll take it one step at a time.

The first effort will be to hook up the button. We'll set up the button as a toggle. It's off by default, but when clicked we'll enable reverse playback. Clicking again will turn it off.

To do this, we'll add a property and the associated setter/getter to indicate reverse playback should be/is in effect. In ReversibleSound.as, add a private property in the same area as the rest of the properties:

 
public class ReversibleSound { 
 
    private var _soundData:Sound; 
    private var _soundPlayer:Sound; 
    private var _bytes:ByteArray; 
    private var _channel:SoundChannel; 
    private var _isReversing:Boolean; 
 
    public function ReversibleSound() {

Then add a pair of getter/setter methods for it:

 
public function get isReversing():Boolean { 
    return _isReversing; 
} 
public function set isReversing(reverse:Boolean):void { 
    _isReversing = reverse; 
}

Now hop back to AdvancedSound.as and hook up a CLICK event for the reverse_mc button:

 
public function AdvancedSound() { 
    _sound = new ReversibleSound(); 
    _sound.load("clip.mp3"); 
 
    play_mc.addEventListener(MouseEvent.CLICK, onPlayClick); 
    stop_mc.addEventListener(MouseEvent.CLICK, onStopClick); 
    reverse_mc.addEventListener(MouseEvent.CLICK, onReverseClick); 
}

And then write the handler:

 
private function onReverseClick(e:MouseEvent):void { 
    _sound.isReversing = !_sound.isReversing; 
    if (_sound.isReversing) { 
        reverse_mc.transform.colorTransform = new ColorTransform(1, 1, 1, 1, -20, -20, -20, 0); 
    } else { 
        reverse_mc.transform.colorTransform = new ColorTransform(); 
    } 
}

Most of this should be fairly straight-forward. We'll come back to that isReversing setter, but for now it's just a simple property. The CLICK handler simply reverses the Boolean currently found in the ReversibleSound object, so it acts as a toggle. Then there is a simple check to see if we're reversing or not, and we visually affect the button simply as an indicator for whether reverse is in effect.

Go ahead and try it out. You won't be able to reverse the sound just yet, but you can click on the button and toggle it between a normal state and a slightly darker state (toggled on, and reverse playback would be in effect).


Step 18: Moving Playback Position to the End

The next phase will be to keep track of where we are in the sound while in reverse. We don't have the convenience of just leaving off that third parameter to extract. Instead, we'll have to do a bit of math to keep track of the position ourselves, and feed that value to the extract method.

Let's start by declaring a property in ReversibleSound.as to keep track of our position.

 
private var _position:int;

Next, head to your set isReversing setter and we'll set this position there:

 
public function set isReversing(reverse:Boolean):void { 
    _isReversing = reverse; 
    if (_isReversing) { 
        _position = (_soundData.length / 1000) * 44100; 
    } 
}

If we are enabling reverse playback, then we'll set the _position. How we set that, though, requires some explanation.

Since we're going to reverse playback, we need to start at the end of the sound. And since our _position property keeps track of the sample we're on, we need to initially set _position to the very last sample of the sound.

To do that, we have to open up a can of mathematics. It would be great if we could easily get the number of samples in a given Sound, but instead the closest we can get is the length property, which gives us how many milliseconds long the sound is. If we divide that number by 1000, we can get how many seconds long the sound is. And since we're going to assume that the MP3 is encoded with a 44.1 kHz sampling rate, we can multiply the number of seconds by 44100 to get the number of samples.

This, of course, could be simplified to:

 
_position = _soundData.length * 44.1;

But it's less obvious what that does; it was easier to explain using the longer version. Use whichever version you think makes you cooler.

Assuming 44.1 kHz is a fair assumption; Flash can only play back MP3 files (using the traditional method) if their sampling rates are 44.1 khz, 22.05 kHz, or 11.025 kHz. And when using dynamic sound generation, the Sound requesting the sample data expects the samples to be provided at 44.1 kHz. My programmer's intuition wants to make that 44100 and turn it into a property that can be configured, and there's nothing wrong with that, but it also could be dangerous to mis-configure the property. Keep this in mind, though, if you're loading a sound that is not 44.1 kHz. If you get errors (because you've over-estimated how many samples there are) or if your playback is at half-speed (because you've underestimated), then try re-encoding your MP3 to be 44.1 kHz.

There isn't a whole lot of point to testing the movie yet; we haven't really made any noticeable changes so far. But if you like, compile to make sure there aren't any compiler errors, and then move on.


Step 19: Tracking Playback Position While in Reverse

Now, if we are in reverse, we need to update that _position property appropriately as sample data is requested. We're going to first stick our sample buffer size into a property, so that it's easier to use consistently in the multiple places we're going to need it. Still in ReversibleSound.as, create a private property to hold our buffer size. Also create three public static constants to hold the valid values for the buffer:

 
public class ReversibleSound { 
 
    private var _soundData:Sound; 
    private var _soundPlayer:Sound; 
    private var _bytes:ByteArray; 
    private var _channel:SoundChannel; 
    private var _isReversing:Boolean; 
    private var _position:int; 
    private var _bufferSize:int; 
 
    public static const BUFFER_SMALL:int = 2048; 
    public static const BUFFER_MEDIUM:int = 4096; 
    public static const BUFFER_LARGE:int = 8192; 
 
    public function ReversibleSound() {

And then we'll create an optional parameter for the constructor, which will default to "small," and set the _bufferSize property.

 
public function ReversibleSound(bufferSize:int = BUFFER_SMALL) { 
    _soundData = new Sound(); 
    _soundData.addEventListener(Event.COMPLETE, onSoundLoad); 
 
    _soundPlayer = new Sound(); 
    _soundPlayer.addEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData); 
 
    _bytes = new ByteArray(); 
 
    _bufferSize = bufferSize; 
}

This way you have the option of changing the buffer size, but have a handy default.

Now, we need to revisit the onSampleData method. Update it to look like the following:

 
private function onSampleData(e:SampleDataEvent):void { 
    _bytes.clear(); 
    if (_isReversing) { 
        _position -= _bufferSize; 
    } else { 
        _soundData.extract(_bytes, _bufferSize); 
    } 
    e.data.writeBytes(_bytes); 
}

What we're doing is checking to see if we're reversing or not. If not (that would be the else block), we do exactly what we were doing before: simply extract the samples and feed them to the Sound. However, if we are reversing, we need to update our internal position by subtract from it the buffer size. This way we'll decrement the position by the size of the buffer, so that we can extract samples by chunks, only backwards.

Note that we've also updated the forward-playing extract method to use _bufferSize instead of the hard-coded 2048.


Step 20: Extracting Samples in Reverse

When it comes to extracting the samples, there isn't a whole lot of difference between going forward or backward. The main difference is that when going in reverse, we need to supply that third parameter; the starting sample argument.

Fortunately, we've already got that value: it's the _position we've been working on. To extract our samples, we just need to add the following highlighted line:

 
private function onSampleData(e:SampleDataEvent):void { 
    _bytes.clear(); 
    if (_isReversing) { 
        _position -= _bufferSize; 
        _soundData.extract(_bytes, _bufferSize, _position); 
    } else { 
        _soundData.extract(_bytes, _bufferSize); 
    } 
    e.data.writeBytes(_bytes); 
}

And feel free to test it (click on "Reverse" then on "Play"); you'll get reversed audio. Kind of. It definitely sounds reversed. But it also sounds kind of jittery. We'll address that in the next step.


Step 21: Reversing the ByteArray

The reason the audio sounds a bit jerky is because while we're extracting samples in reverse, we're only extracting the chunks of samples, 2048 at a time, in reverse. The chunks (the ByteArray) themselves are in forward order. So we've reversed the chunks but individual samples inside those chunks are still in forward order. This is like take a deck of cards, grabbing five from the top and placing them to the side, then grabbing the next five from the top and placing them on top of the new stack, and so on. We'll have mixed up the order of the cards, and kind of reversed them so the cards that were at the top of the deck are now at the bottom, and vice versa. But they are not truly reversed.

To get true reverse playback, we need to next reverse the contents the of the ByteArray after we extract the samples. We'll create a new method for this purpose. Place this somewhere in ReversibleSound.as:

 
private function reverseBytes(inBytes:ByteArray):ByteArray { 
    _reversedBytes.clear(); 
 
    for (var i:int = inBytes.length - 4; i >= 0; i -= 4) { 
        inBytes.position = i; 
        _reversedBytes.writeFloat(inBytes.readFloat()); //4 bytes each time. 
    } 
 
    return _reversedBytes; 
}

This method needs another ByteArray object to do the reversing, and rather than create a new ByteArray each time this method is called, we can save some overhead by creating one in a property, and then clearing it each time. That's the _reversedBytes in use in this method. We need to declare and instantiate that property:

 
public class ReversibleSound { 
 
    private var _soundData:Sound; 
    private var _soundPlayer:Sound; 
    private var _bytes:ByteArray; 
    private var _channel:SoundChannel; 
    private var _isReversing:Boolean; 
    private var _position:int; 
    private var _bufferSize:int; 
    private var _reversedBytes:ByteArray; 
 
    public static const BUFFER_SMALL:int = 2048; 
    public static const BUFFER_MEDIUM:int = 4096; 
    public static const BUFFER_LARGE:int = 8192; 
 
    public function ReversibleSound(bufferSize:int = BUFFER_SMALL) { 
        _soundData = new Sound(); 
        _soundData.addEventListener(Event.COMPLETE, onSoundLoad); 
 
        _soundPlayer = new Sound(); 
        _soundPlayer.addEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData); 
 
        _bytes = new ByteArray(); 
        _reversedBytes = new ByteArray(); 
 
        _bufferSize = bufferSize; 
    }

Getting back to the reverseBytes method, the core logic of it simply loops over the input ByteArray in reverse and reads numbers out of it to write into the reversed ByteArray. Again, I'm not going to dwell of the mechanics of the ByteArray here, but I will point out that the samples stored in the ByteArray, both from the extract method and from what the playback Sound expects, are written as floats: 32-bit floating point numbers. So we can read individual samples with readFloat and store them with writeFloat. Since the position of a ByteArray is based on bytes, and there are 4 bytes in a 32-bit number (8 bits = 1 byte), we can traverse the input ByteArray by steps of 4 and land on the proper starting point for the next (or previous, depending on how you look at it) sample.

When all it said and done, the method returns a perfect reversal of the input ByteArray.


Step 22: Supplying the Reversed Samples to the Sound

Let's now use this method in onSampleData:

 
private function onSampleData(e:SampleDataEvent):void { 
    _bytes.clear(); 
    if (_isReversing) { 
        _position -= _bufferSize; 
        _soundData.extract(_bytes, _bufferSize, _position); 
        _bytes = reverseBytes(_bytes); 
    } else { 
        _soundData.extract(_bytes, _bufferSize); 
    } 
    e.data.writeBytes(_bytes); 
}

We just need to pass in _bytes after extraction, and then we can store the result back into _bytes so it can be used to write the samples to the playback Sound.

Go ahead and try it out. Click the "Reverse" button, then "Play", and...oh, nuts, you might have heard the very beginning of some reversed sound but then you ended up with nothing (Or, depending on your sound, you may not hear anything, if the ending has some silence as part of the MP3). What the deuce?

At this point, we need to be careful of how we're managing the ByteArrays. Our attempt to save some overhead by creating the ByteArrays ahead of time and reusing them for each method is actually coming back to bite us. But there is a way around the problem, and it involves, yes, creating another ByteArray.

This time, we simply need to declare a property (or a method variable, even, although we can - again - save some processing by declaring it once as a property rather than multiple times as a method variable). Let's do that, up where the rest of the properties are declared:

 
private var _samplesToUse:ByteArray;

And let's put it to use in the onSampleData method:

 
private function onSampleData(e:SampleDataEvent):void { 
    _bytes.clear(); 
    if (_isReversing) { 
        _position -= _bufferSize; 
        _soundData.extract(_bytes, _bufferSize, _position); 
        _samplesToUse = reverseBytes(_bytes); 
    } else { 
        _soundData.extract(_bytes, _bufferSize); 
        _samplesToUse = _bytes; 
    } 
    e.data.writeBytes(_samplesToUse); 
}

We're adding two lines of logic, one for each case of the if/else. We're just assigning _samplesToUse to either _bytes (for normal playback) or to the return of reverseBytes (for reversed playback). Then we use _samplesToUse, not _bytes, when sending samples to the Sound object with writeBytes.

That much might make sense, and it may be unclear why we're doing this at all.

I don't want to derail the discussion too much from our core topic, but this has to do with object references, and the way we pass ByteArrays around to be worked on by various methods. I'll try to sum it up:

  • When we pass _bytes into reverseBytes, we use it as the source of data to be reversed. Because we need to loop over that source data, and write it in reverse into another ByteArray, we have the second ByteArray property, _reversedBytes.
  • Both _bytes and _reversedBytes are being held onto for efficiency purposes. reverseBytes then returns _reversedBytes, and then we set that to _bytes on line 49, in onSampleData. After one call, we've actually pointed both _bytes and _reversedBytes to the same ByteArray object.
  • The second time around, we're actually passing in that double-referenced ByteArray object into reverseBytes, and now the same object is the source data as well as the ByteArray intended to store the reverse samples.
  • At this point, Flash has trouble keeping the universe from imploding and simply stops thinking about it ("New mission: Refuse this mission!"). So we sidestep the issue by introducing the third ByteArray property - but understand that it's not a third ByteArray object, it's simply a property used for reference. We don't care which ByteArray gets stored in it, so we use that one as a pointer to whichever one we want to use and pass that into writeBytes.

Now you should now hear perfectly reversed audio!


Step 23: Knowing When to Stop

We need to consider what would happen as we approach the beginning of the sound. As _postiion gets decremented, at some point it will become negative. That's not a valid position, but it is below 0 and will cause extract to start at the beginning and move forward automatically, like we do if we're not in reverse.

So every time we decrease _position, we need to check to make sure it's 0 or higher. If not, then we need to consider the audio as having run its course, and we can stop. Update the onSampleData method to this:

 
private function onSampleData(e:SampleDataEvent):void { 
    _bytes.clear(); 
    if (_isReversing) { 
        _position -= _bufferSize; 
        if (_position < 0) { 
            stop(); 
            isReversing = true; 
            return; 
        } 
        _soundData.extract(_bytes, _bufferSize, _position); 
    } else { 
        _soundData.extract(_bytes, _bufferSize); 
    } 
    e.data.writeBytes(_bytes); 
}

If the _position property goes negative, then we can call stop() to make sure sound has stopped. Then we can reset the _position property to the end of the sound again, so it's ready to start again if play() should happen to be called next. We do that by simply setting the isReversing property to true again. It's already true, but when it's set to true it also sets the _position property to the be at the end. Finally, we return early so that no samples are extracted, or sent to the sound.


Step 24: I Knew When to Stop

This tutorial was packed to the brim with audio information: first we learned (or reviewed) the traditional method for loading and play MP3 files. Then we learned how to load an MP3 and use that to feed samples into a sound buffer. Finally we figured out how to take those samples and reverse them so we could feed them into the buffer to get a reversed sound, while learning some fundamentals of digital audio along the way.

I hope you had fun with this one. I'll be back with another tutorial or two on Flash audio, so if you liked this article, stay tuned.

Advertisement
Related Posts