Sound Manager

Introduction

The SoundManager allows for different levels of control for sound in your game, from simple play/pause/resume/stop controls with sound names, to control over individual playing sound instances.

Using the sound manager

Registering a sound into the sound manager

a sound needs to be registered into the sound manager so you can easily play it later anywhere in your game by “calling” it by name (a string identifier).

you can register sounds as flash Sound objects, classes that would create a flash sound object, or with a string “url” (in which case the sound manager will load the sound when necessary - or preload if you manually ask for it.

the basic way to register sounds is in your document class or the class that extends *CitrusEngine , outside of any state so you don't register the same sound every time - and so they will be ready when you start your first state. the following examples will assume you are in Main.as and Main.as extends a CitrusEngine class.

//adding a sound with a url
sound.addSound("loop", { sound:"assets/sounds/loop.mp3" };
 
//adding a sound with a Class that extends the flash Sound Object (usefull if your sound is embedded 
sound.addSound("loop", { sound:MySoundClass };
 
//adding a sound using the starling asset manager is also easy since it returns a flash sound object
sound.addSound("loop", { sound:starlingassetmanager.getSound("loop") };

adding sounds this way, creates a CitrusSound inside the SoundManager. A CitrusSound just wraps a flash Sound object basically.

a Sound added via url can be preloaded with

sound.getSound("loop").load();

or you can preload all of them with

sound.preloadAllSounds();

an event is dispatched when one sound is done loading, and when all sounds are done loading as well. more info on events later in this article.

Warning By default sounds added from url are streaming, so you don't really need to wait for them to load. although once they are loaded you have less lag when you first play it as its already loaded and the streaming process doesn't need to come into action, that's useful for sound effects.

Playing/pausing/resuming and stopping a sound

Now that we've registered our sounds into the sound manager, we can use them. in a citrus engine state, or citrus object, you have access to the sound manager using the

_ce.sound

shortcut.

you have the following functions to play/pause/resume/stop CitrusSounds :

_ce.sound.playSound("loop");
_ce.sound.pauseSound("loop");
_ce.sound.resumeSound("loop");
_ce.sound.stopSound("loop");

Now pauseSound/resumeSound/stopSound will pause/resume/stop all playing sounds with the same id.

if you've been playing “loop” more than once and they overlap, they will all be affected. We'll see later how to stop only one of them.

you can also check if a CitrusSound already has at least one playing sound or has at least one paused sound with

_ce.sound.soundIsPlaying("loop");
_ce.sound.soundIsPaused("loop");

sound properties

Alright, we've been calling our sound example “loop” but it doesn't actually loop, it plays only once.

Now that you understand the “registering” process for sounds, I will explain all properties you have available on a CitrusSound.

to make the sound loop, you can set its “loops” parameter to -1 or int.MAX_VALUE

sound.addSound("loop", { sound:"assets/sounds/loop.mp3" , loops:-1 };
//or
sound.addSound("loop", { sound:"assets/sounds/loop.mp3" , loops:int.MAX_VALUE };

sound events are dispatched when a sound completes and starts a new loop, but that feature only exists if loops is a positive value. In fact, the sound manager uses event based looping when loops is positive, if its negative it means you want the sound to loop infinitely and we use the loop feature of flash (which is said to leave less silent gap between loops) but with that, we have no idea when the sound loops one more time so no events are used in that case.

We just left the choice to you , most of the time for simple uses, you will just use loops:-1 .

Now our sound loops.

here are the other default properties you can set :

  • volume : the initial volume (Number from 0 to 1)
  • panning : Number between -1 and 1
  • mute : defaults to false, whether to start the sound muted or not.
  • permanent : by default set to false. if set to true, this sound cannot be forced to be stopped - and cannot overlap
  • group : the groupID of a group, no groups are set by default.

volume, panning and mute talk for themselves, permanent is something to expand on :

In a 'sound heavy' game where you're always at risk of playing too many sounds, If you try to play a new sound and there's no channel available, by default the sound manager will stop the first played sound in the list of playing sounds.

If a sound is set to permanent:true however, it will never be forced to stop by another sound.

What happens when no sound channels are available is defined by the static 'setting'

CitrusSoundInstance.onNewChannelsUnavailable;

its default is CitrusSoundInstance.REMOVE_FIRST_PLAYED .

you can decide that the last sound played needs to be forced to stop, or simply that the new sound will not be played at all if no sound channels are available setting onNewChannelsUnavailable to CitrusSoundInstance.REMOVE_LAST_PLAYED or CitrusSoundInstance.DONT_PLAY.

We don't know for sure how many soundChannels are made available in the first place - at least CitrusEngine doesn't want to make this assumption.

CitrusSoundInstance, when created will check how many channels are available at the moment of its creation and this will be the definitive maximum number of sounds you'll be able to play.

Background music, looping sounds or environmental sound effects might need to be set to permanent = true so if there's no channel available for the next sound, the permanent ones won't be stopped.

sound.addSound("BGM", { sound:"assets/sounds/backgroundMusic.mp3" ,permanent:true, loops:-1 };

permanent also means it can't be played more than once.

Sound groups

In games, you often want to provide your user with the option of controlling volumes of background music, sound effects, ui sound independently.

We provide that with CitrusSoundGroup.

there are three default ones :

CitrusSoundGroup.BGM
CitrusSoundGroup.SFX
CitrusSoundGroup.UI

and you can add a CitrusSound to a group by setting its group property.

here we setup our background music, two sound effects and a button click sound for our ui.

sound.addSound("BGM", { sound:"assets/sounds/backgroundMusic.mp3" ,permanent:true, loops:-1 , group:CitrusSoundGroup.BGM};
 
sound.addSound("sfx1", { sound:"assets/sounds/sfx1.mp3", group:CitrusSoundGroup.SFX};
sound.addSound("sfx2", { sound:"assets/sounds/sfx2.mp3", group:CitrusSoundGroup.SFX};
 
sound.addSound("buttonSound", { sound:"assets/sounds/click.mp3", group:CitrusSoundGroup.UI};

You can then , whenever you want, control sound volumes by group, even dynamically in the middle of the game :

Here I will be muting all the sound effects and lowering the volume of my ui sounds :

_ce.sound.getGroup(CitrusSoundGroup.SFX).mute = true;
_ce.sound.getGroup(CitrusSoundGroup.UI).volume = 0.2;

note that even if a CitrusSound's volume is 1, it will be multiplied by its group's volume to have the final volume you can hear.

The soundmanager itself has a masterVolume and masterMute property which also determines the final volume of all sounds :

//mute all sounds...
_ce.sound.masterMute = true;
//change the final volume output
_ce.sound.masterVolume = 0.2;

CitrusSoundGroups doesn't only let you organize you sounds into volume groups, you can get all sounds registered in a group to a vector of CitrusSounds

var soundEffects:Vector.<CitrusSound> = _ce.sound.getGroup(CitrusSoundGroup.SFX).getAllSounds();

or get a random sound from the group and play it,

var randomSound:CitrusSound = _ce.sound.getGroup(CitrusSoundGroup.SFX).getRandomSound();
if(randomSound)
   randomSound.play();

or preload all sounds from a specific group

sound.getGroup(CitrusSoundGroup.SFX).addEventListener(CitrusSoundEvent.ALL_SOUNDS_LOADED, function(e:CitrusSoundEvent):void
{
e.currentTarget.removeEventListener(CitrusSoundEvent.ALL_SOUNDS_LOADED,arguments.callee);
trace("All the SFX group preloaded.");
});
 
_ce.sound.getGroup(CitrusSoundGroup.SFX).preloadSounds();

You can even set a group to play only one voice at a time by setting its 'polyphonic' property to false ! which is handy so you don't have to worry which previously playing sound to stop in a group that should just be “one voice” or play one track at any time anyway.

sound.getGroup(CitrusSoundGroup.BGM).polyphonic = false;
The CitrusSoundGroup doesn't get all events from the CitrusSound as its not a “parent” of it so the events don't “bubble through it”, unlike the SoundManager.

CitrusSoundGroup only listens to SOUND_LOADED events on the sounds in categorizes and dispatches an ALL_SOUNDS_LOADED event when necessary.

CitrusSoundInstance

A CitrusSoundInstance basically wraps a flash soundchannel and lets you control individual playing sounds.

if you were to play a CitrusSound more than once and there was two overlapping sounds playing then changing the volume of the CitrusSound will change the volume of both playing sounds.

if you want to change the volume of only one of them, you need to get a reference to it.

As you already know, you can play a sound with

_ce.sound.playSound("soundId");

but you can also do the following :

var citrusSound:CitrusSound = _ce.sound.getSound("soundId");
if(citrusSound)
var soundInstance:CitrusSoundInstance = citrusSound.play();

and with the second solution, if that “soundId” was already playing, you have a reference to the soundInstance you just played, and can dynamically change its volume and panning without affecting any other “soundId” sounds that are playing.

var citrusSound:CitrusSound = _ce.sound.getSound("soundId");
if(citrusSound)
var soundInstance:CitrusSoundInstance = citrusSound.play();
 
soundInstance.volume = 0.2;
soundInstance.panning = -1;

Gaps in looping sounds

As we've seen earlier, we loop sounds either based on events (when the loops property is positive) or using the loops argument of the flash Sound's play method when loops is negative.

In both cases, you might experience gaps in loops due to mp3 encoding adding a bit of silence in the beginning of sounds.

We provide a solution for that through a static property in CitrusSoundInstance :

CitrusSoundInstance.startPositionOffset = 80;

all sounds will basically offset by that amount in milliseconds when playing, the default value is 0 of course.

mp3 encoding and added silence is not the only cause of gaps, you have to take into account event processing when you're using the event based loop system, and whatever processing time flash uses internally as well.

Also know that this only makes sense if you encoded all your sounds with the same encoder - so that the amount of 'silence added' in all of your sounds is the same and has been clearly identified.

Events

a CitrusEventDispatcher is available with a pseudo hierarchy but no weak references (so be careful when adding listeners, remove them explicitly when done.) the SoundManager, CitrusSound and CitrusSoundInstance are connected CitrusEventDispatchers.

CitrusSound is responsible for the following events :

public static const SOUND_ERROR:String = "SOUND_ERROR";
public static const SOUND_LOADED:String = "SOUND_LOADED";
public static const ALL_SOUNDS_LOADED:String = "ALL_SOUNDS_LOADED";

CitrusSoundInstance is responsible for these :

public static const SOUND_START:String = "SOUND_START";
public static const SOUND_PAUSE:String = "SOUND_PAUSE";
public static const SOUND_RESUME:String = "SOUND_RESUME";
public static const SOUND_LOOP:String = "SOUND_LOOP";
public static const SOUND_END:String = "SOUND_END";
public static const NO_CHANNEL_AVAILABLE:String = "NO_CHANNEL_AVAILABLE";
public static const FORCE_STOP:String = "FORCE_STOP";
public static const SOUND_NOT_READY:String = "SOUND_NOT_READY";

some of these you might never use at all. but there are there just in case. Due to the hierarchy linking SoundManager, CitrusSound and CitrusSoundInstance,

listening to the SOUND_START event on the sound manager will work (you don't have to add that event listener to the specific sound instance that just played… SO you don't need to dig deep into the sound manager system , you can use all the high level stuff.

_ce.sound.addEventListener(CitrusSoundEvent.SOUND_START, function(soundEvent:CitrusSoundEvent):void
{
	trace(
"Sound named",
soundEvent.soundName,
"just started via the CitrusSoundInstance",
soundEvent.soundInstance,
"and will be looping",
soundEvent.loops,
"times. the corresponding citrus sound has a default volume of",
soundEvent.sound.volume);
});
 
_ce.sound.playSound("anysound"); //will dispatch the event from the citrus sound instance up to the sound manager if it was able to play.

Its also not necessary to get the event as a listener argument.

_ce.sound.addEventListener(CitrusSoundEvent.SOUND_START, function():void
{
	trace("some sound just started.");
});

The events will pass through CitrusSoundInstance and CitrusSound, so that same listener will work on any of them.

Advanced use of the sound manager

The first thing you might want, is for each of your game objects to know about which sound they are playing, and update volume and panning according to the object's position relative to the camera to get a richer sound environment for the game.

the advanced sound example : https://github.com/alamboley/Citrus-Engine-Examples/tree/master/src/advancedSounds

uses this idea easily and you can implement it using the work in progress CitrusSoundSpace and CitrusSoundObject.

its only taking advantage of the events to track down playing sounds and “connect” their volume and panning to a citrus object's position in space.

  citrus/sound.txt · Last modified: 2015/05/15 05:17 by 88.172.141.162
 
Powered by DokuWiki