Back to Posts

Panning sounds in FlashPunk

Konstantinos Egkarchos / October 14, 2011

I tend to create helper classes for easier organisation and here is one handling each and every sound. The main purpose was to add a panning system which would play the sound 2D, by evaluating the source of the sound. If e.g. an enemy alien died at the left of the stage the sound would be higher from the left speaker than the right. As I noticed in ISS that was not obvious in the user but instictively the user new where the particular sound was coming from. In ISS where the player shoots consecutively, and aliens die one by one in a particular small time, this isn’t very helpful for the user but again, its a nice addition :).

EDIT: SoundSystem now has a repository with bug fixes. Find it here.

So without further ado, here’s the code:

package
{
    import net.flashpunk.FP;
    import net.flashpunk.Sfx;
    import net.flashpunk.tweens.sound.Fader;
    /**
     * ...
     * @author Konstantinos Egarhos
     */
    public class SoundSystem
    {
        private static var mute:Boolean;
        private static var volume:Number; // From 0 to 1.
        private static var snd:Sfx;
        private static var fader:Fader;

        // Gets-Sets
        public static function get muteG():Boolean
        {
            return mute;
        }

        /**
         * Constructor of a static class.
         */
        public function SoundSystem()
        {

        }

        /**
         * Sets mute to false, and volume to 0.7.
         */
        public static function reset():void
        {
            mute = false;
            volume = 0.7;
            setVolume();
            fader = new Fader(resetVolume);
        }

        /**
         * Resets volume.
         */
        public static function resetVolume():void
        {
            FP.volume = volume;
        }

        /**
         * Adds the fader(in case the world is changed).
         */
        public static function addFader():void
        {
            FP.world.addTween(fader);
        }

        /**
         * Sets the volume, or if it's muted it zeroes it.
         */
        public static function setVolume():void
        {
            if (mute)
            {
                FP.volume = 0;
            }else
            {
                FP.volume = volume;
            }
        }

        /**
         * Returns the pan value according to the entity x position in the stage.
         * @param   xPos The x position where the sound is comming.
         * @return The pan value.
         */
        public static function panSound(xPos:Number):Number
        {
            if (xPos > FP.halfWidth)
            {
                xPos -= FP.halfWidth;
                return xPos / FP.halfWidth;
            }else if(xPos < FP.halfWidth)
            {
                return (xPos/FP.halfWidth)-1;
            }else
            {
                return 0;
            }
        }

        /**
         * Plays sounds.
         * @param   snd The sound you want to hear.
         * @param   xPos The source of the sound in the x axis for panning.
         */
        public static function play(snd:Sfx, xPos:Number = 320):void
        {
            if (!mute)
            {
                snd.play(volume, panSound(xPos));
            }
        }

        /**
         * Loops sounds.
         * @param   snd The sound you want to hear.
         * @param   xPos The source of the sound in the x axis for panning.
         */
        public static function loop(snd:Sfx, xPos:Number = 320):void
        {
            if (!mute)
            {
                snd.loop(volume, panSound(xPos));
            }
        }

        /**
         * Pauses the sound from playing.
         * @param   snd The sound you want to pause.
         */
        public static function pause(snd:Sfx):void
        {
            snd.stop();
        }

        /**
         * Mutes or unmutes the sounds.
         */
        public static function reverseMute():void
        {
            if (mute)
            {
                mute = false;
            }else
            {
                mute = true;
            }
            setVolume();
        }

        /**
         * Increases/decreases gradually to targetVolume all the sounds.
         * @param   targetVolume The volume to be set.
         * @param   time The duration the change will take.
         */
        public static function fadeOut(targetVolume:Number, time:Number):void
        {
            fader.fadeTo(targetVolume, time);
        }

    }

}

The class is considered as “static”. Of course no such thing exists in actionscript, but it's easier to describe it that way. Every function is static in order to be called from other parts of the code with ease. e.g.

SoundSystem.reset(); // Initializes. Just once in your code.

SoundSystem.play(soundExplosion, this.centerX); // where this.centerX is

The function panSound() is called from every other function except from the one that plays the background sound. It’s rather simple, though I’m a little proud for that little masterpiece :D. The pan value can be a float number from -1(left) to +1(right). So in the function we have to first find if the sound source is past the center or not, and that’s what the if-else if-else do. The division of a value to its full returns a value from 0 to 1, and that’s saficient for the first if which takes place if the source is in the right hand of the stage. As for the left this was rather tricky but simple. We take the same result but subtract it with 1, in order to have the exact same result in its minus form!

As for the reverseMute() function its here for the settings only. The user can choose to mute or unmute the sound, and by this function it is done.

You’ll have to call addFader() in each world initialization if you want to fade the sound in that world. Careful here, if you forget it probably you won’t have the result you want, and the sound will still play without giving you an error.

The only thing you have to care about this class is the default value of the xPos. Your game maybe will have different dimensions than mine, so though it's a very rare occasion to be exactly in the middle, you probably won’t want to miss that.