Skip to main content

Using AS3's computeSpectrum

·11 mins

Hey there, since I’ve got this blog anyway, I might as well share some of my knowledge. This is a very basic tutorial on how to build a simple spectrum analyzer using SoundMixer.computeSpectrum, which is new to actionscript 3. This is also my first tutorial ever, so feedback is welcome! πŸ™‚

This is what we will be creating:

Spectrum analyzer screengrab

Who is this meant for?

Well, if you’re familiar with AS3, this probably isn’t for you. My aim is to introduce AS3 to people familiar with AS1 or AS2. If you’re not at all familiar with actionscript, I’m not sure this is for you, but you might follow along, and see how simple it actually is (for a rather complex example).

What do I need to know?

Basic knowledge of flash is required and some knowledge in oop and actionscript will help, but I’ll go thru every aspect anyway, so you’ll get it to work with copy-paste, if nothing else 😜 Still, if you want to learn something new, I suggest you read what I’m saying here, look at the examples, and do it yourself. Some knowledge of trigonometry & math will help you understand what I’m doing with the data (how i output it visually).


Alright, enough with the chit chat, let’s start!

First of all you’ll need the flash 9 alpha preview to compile, so if you don’t have it, go get it from here.

Like with any project planning in advance helps a lot in the future, so let’s write down a couple points on what we want to do. I was thinking of a circle where spikes come out depending on the music. If it’s quiet it’s nice and smooth, if there’s loadsa action it’s all spiky. Sounds good? Oh, and let’s make it respond to beats! Mm, yeah, that oughta be nice. You can of course easily modify it later on, once you get a hang of how this stuff works.

  1. Start by creating a .fla file. Give it a black background color, a framerate of approx. 30fps, and save it somewhere with the name spectrum.fla. We’ll come back to this later on. Place some mp3 called song.mp3 in the same folder.

  2. Let’s create a class for our spectrum analyzer. Create a new .as file and save it as SpectrumAnalyzer.as in the same place as the .fla file is. Please note that normally you’d want to have all you classes in a central place (for structure and code reusability), but for the sake of simplicity we’ll just put the .as file in the same folder.

  3. Inside SpectrumAnalyzer.as add this:

// SpectrumAnalyzer.as
 
package {
    import flash.display.*; // needed for Sprite
    class SpectrumAnalyzer extends Sprite {
    }
}

This defines our class inside a new package that extends Sprite (Sprite is basically a MovieClip, without the timeline stuff we don’t need anwyay).

  1. Currently we’ve got a class that has a visual appearance (just doesn’t show anything yet), so let’s go back to the .fla file and create an instance of that class. We’ll also pass along what mp3 to play, and what size the visualization should be.
// spectrum.fla
 
import SpectrumAnalyzer;
var visualization:SpectrumAnalyzer = new SpectrumAnalyzer("song.mp3", 550, 400);
addChild(visualization);

This imports the class, adds a new instance of it, and creates a child of that instance, placing it on the stage. addChild is similar to createEmptyMovieClip() and/or attachMovie() in AS 1 & 2.

  1. Alright, we’ve got everything set up pretty good now, let’s start getting in some data.
// SpectrumAnalyzer.as
 
package {
    import flash.display.*;
    import flash.media.*; // needed for Sound, SoundMixer
    import flash.net.*; // needed for URLRequest
    class SpectrumAnalyzer extends Sprite {
        private var music:Sound = new Sound;
        private var __width:uint;
        private var __height:uint;
        function SpectrumAnalyzer(mp3:String, _width:uint, _height:uint) {
            __width = _width;
            __height = _height;
            x = __width/2;
            y = __height/2
            music.load(new URLRequest(mp3));
            music.play(0, 999);
        }
    }
}

Let’s start playing music. We create the contructor function that gets executed automatically when a class instance is created, and pass on mp3, _width and _height to it (which we sent from spectrum.fla, remember?). We store _width and _height as variables for future usage. Then we load the mp3. The syntax here is new to AS3 with the URLRequest class, but it’s still pretty straight forward. We’ll also offset the SpectrumAnalyzer with Β½ of the height & width, so that it’s in the center.

If you now publish spectrum.fla, you should hear that song playing.6. Ok, but this isn’t an mp3-player, let’s get some visual feedback!

If you look at the docs, you’ll notice computeSpectrum, which is the key function here, wants a byteArray as an argument, so let’s first create one, then make an onEnterFrame alΓ‘ AS3.

// SpectrumAnalyzer.as
 
package {
    import flash.display.*;
    import flash.media.*;
    import flash.net.*;
    import flash.utils.ByteArray; // needed for ByteArray
    import flash.events.*; // needed for Event
    class SpectrumAnalyzer extends Sprite {
        private var music:Sound = new Sound;
        private var ba:ByteArray = new ByteArray();
        function SpectrumAnalyzer(mp3:String) {
            __width = _width;
            __height = _height;
            x = __width/2;
            y = __height/2
            music.load(new URLRequest(mp3));
            music.play(0, 999);
            addEventListener(Event.ENTER_FRAME, processSound);
        }
        private function processSound(ev:Event):void {
            SoundMixer.computeSpectrum(ba, true, 0);
        }
    }
}

You don’t have mc.onEnterFrame in AS3 anymore, so initially this may seem confusing. Still, when you get the hang of it, it’s a lot more logical. Anyway, here we’re now listening to the Event.ENTER_FRAME event which is a constant of Event, equivalent to the string “enterFrame”. The SpectrumAnalyzer broadcasts it automatically every frame because it extends Sprite, which eventually extends EventDispatcher, and executes a function called processSound every time. This way we’re constantly processing the sound, which will result in a visualizer that changes all the time. A visualizer that doesn’t update would be pretty boring, huh ? ;) In the processSound function we add the SoundMixer.computeSpectrum to analyze the music that’s playing and output it’s values to a ByteArray which we’ve here called ba.

  1. That’s it really! We’re now getting the music as a stream into ba, that gets updated every frame. If you look at the computeSpectrum docs, you’ll notice that the ByteArray contains 512 floating point values, 256 for the left channel and 256 for the right channel. Now the real fun starts!

Let’s draw the circle with our data, where we’re putting the volume as the size of the circle at a certain point. This will make the circle look spiky since it’s quite unlikely the volume at all frequencies is the same (unless it’s totally silent) πŸ˜‰

We’ll also add a couple variables so that we can easily later on change the appearance of the circle.

// SpectrumAnalyzer.as
package {
    import flash.display.*;
    import flash.media.*;
    import flash.net.*;
    import flash.utils.ByteArray;
    import flash.events.*;
    class SpectrumAnalyzer extends Sprite {
        // Settings
        private var lineThickness:Number = 2;
        private var lineColor:Number = 0x993300;
        private var circleSize:Number = 75;
        //
        private var music:Sound = new Sound;
        private var ba:ByteArray = new ByteArray();
        private var __width:uint;
        private var __height:uint;
        function SpectrumAnalyzer(mp3:String, _width:uint, _height:uint) {
            __width = _width;
            __height = _height;
            x = __width/2;
            y = __height/2
            music.load(new URLRequest(mp3));
            music.play(0, 999);
            addEventListener(Event.ENTER_FRAME, processSound);
        }
        private function processSound(ev:Event):void {
            SoundMixer.computeSpectrum(ba, true, 0);
            graphics.clear();
            graphics.moveTo(0, -circleSize);
            graphics.lineStyle(lineThickness, lineColor);
            for (var i:uint = 0; i <512; i++) {
                var lev:Number = ba.readFloat();
                graphics.lineTo(-Math.sin(i/256*Math.PI)*circleSize*(lev+1), Math.cos(a/256*Math.PI)*circleSize*(lev+1));
            }
        }
    }
}

Ok, some more code here. This is pretty much the same you’d do in AS1/2, with the exception of graphics, which now is a part of the Sprite (you can’t directly just use mc.lineTo anymore). First we clear whatever has been drawn from the previous frame, then we move the pointer up by the size of the circle and define the lineStyle according to the variables defined earlier. Then we loop thru 512 values in the ByteArray. This may seem a bit odd, but when you do ba.readFloat(); it returns the float value (0 and up), and moves on to the next value in the ByteArray. This way we can get all the frequencies very easily with a simple for loop.

Then some basic trigonometry (remember high-school?). Sinus & cosinus of 2 * pi is a full circle, and anything in between will go to that point. Sinus of pi is Β½ etc. We then divide i with 256 to get a value from 0-2, depending on where we are in the for loop, and then multiply that with pi so that it’ll draw a full circle when the for loop completes. We then multiply that with circleSize so that we get a bit bigger circle than 1px πŸ˜‰ Then we multiply it with the level. At total silence this will be 0, and everybody knows that x * 0 = 0 -> we want to add 1 to make the minimum size of the circle circleSize, so that it multiplies the result of the sinus/cosinus with 1 or more, not 0. Pretty simple, but if you didn’t get it (probably due to my messy explanations), just play around with it to see what happens.

  1. Ok, so we’ve now got it acting to the music, but not quite the way we want it to. One side of the circle is upside down, which doesn’t look as cool. The reason for this is that it continues around the circle, and since we’re changing the channel, the first values (of the final 256) will be similar to first of the whole 512. Flipping the other side will fix that easily. I’ll leave out all the excess actionscript, because otherwise this page will eventually be reaaally long. Let’s modify the processSound function a bit:
private function processSound(ev:Event):void {
    SoundMixer.computeSpectrum(ba, true, 0);
    graphics.clear();
    graphics.moveTo(0, -circleSize);
    graphics.lineStyle(lineThickness, lineColor);
    for (var i:uint = 0; i <512; i++) {
        var lev:Number = ba.readFloat();
        var a:uint = i;
        if (i <256) a += 256;
        if (i == 256) graphics.moveTo(0, -circleSize);
        graphics.lineTo(-Math.sin(i/256*Math.PI)*circleSize*(lev+1), Math.cos(a/256*Math.PI)*circleSize*(lev+1));
    }
}

So, what’s happening here? We store i in a temporary variable called a. If we modified i directly, it’d mess up the for loop, which we don’t want to do. We’ll want to flip the left side, and since a & i will start from 0 in the left channel, and 256 in the right channel, all we need to do is to add 256 to a, which is exactly what we do. This will fix the issue with the flipped Β½ circle. We also want to move the pointer up to the original starting point (center-up) when we change from the left channel to the right channel, otherwise there will be an ugly line in the middle when the line draws from the bottom to the top.

If you now try it, you’ll see it working correctly. Sweet!

  1. Beat detection. We wanted beat detection. Ok, beat detection might be the wrong word, since we’re done with all the sound analysis. Still let’s collect the entire volume of the computeSpectrum snapshot, see if it’s over a certain level, and if it is, scale the spiky thingy up a bit. Shouldn’t be too difficult, but will add a cool effect.

First lets define a couple variables, so that we’ve got all the settings in the same place for easy manipulation later on

// Settings
private var lineThickness:Number = 2;
private var lineColor:Number = 0x993300;
private var circleSize:Number = 75;
private var scaleOnBeat:Number = 1.1; // 110%
private var reactToBeat:Number = 30;

Alright we’ve added two settings, one which is how much we’ll scale it up, and a second when we’ll scale it up. 30 is just a value i got by testing, you might get better results with another number. You could trace out vol, which we’re defining next, and determine a suitable level for the song you’ve chosen. The song i had here originally was Prodigy - Voodoo People (Pendulum Remix). Obviously I can’t include it due to copyright issues etc.. And bandwidth isn’t free either πŸ˜‰

private function processSound(ev:Event):void {
    SoundMixer.computeSpectrum(ba, true, 0);
    graphics.clear();
    graphics.moveTo(0, -circleSize);
    graphics.lineStyle(lineThickness, lineColor);
    var vol:Number = 0;
    for (var i:uint = 0; i <512; i++) {
        var lev:Number = ba.readFloat();
        vol += lev;
        var a:uint = i;
        if (i <256) a += 256;
        if (i == 256) graphics.moveTo(0, -circleSize);
        graphics.lineTo(-Math.sin(i/256*Math.PI)*circleSize*(lev+1), Math.cos(a/256*Math.PI)*circleSize*(lev+1));
    }
    if (vol> reactToBeat) {
        scaleX = scaleY = scaleOnBeat;
    } else {
        scaleX = scaleY = 1;
    }
}

Should be pretty straight forward. It adds up all the floats, and checks if it’s over the value you defined. If it is, it’ll scale the whole thingy to whatever you said, and if not, scale it (back) to 1. You could of course add several values with it, make it rotate at beat or whatever. Sky’s the limit!

  1. Well, we’re pretty much done here. The next step really is to play around, use your imagination and make some cool visualizers. Using BitmapData and/or filters will easily make it look a lot better. This tutorial, however, won’t go into that, just read the docs or check some examples/tutorials on google and you’ll get the hang of it.

Download source files

The file included with the zip above is DiscoFruity by Osnoff.

That’s it! Hope you learned something new!