Managing Sounds with Commands

Sound management is very important for many types of Flash applications, such as interactive websites and games. As long as you want to deliver a rich interactive experience, you might want to consider making use of sound effects and background music. In this tutorial, I’ll present a minimalistic sound management framework that manages sounds into sound tracks. And I’ll show how to integrate the sound framework with the command framework from my previous tutorials.


Introduction

I’ve played games with incautious sound management, and that degrades user experience. Have you ever played a game, say, an action game, where the character’s exclamation voice plays before the previous voice ends, overlapping each other? That’s a result of bad sound management: there shouldn’t be more than one voice of the same character playing at a time. The sound management framework I’m about to cover will take care of this issue by managing sounds with sound tracks.

The examples in this tutorial make use of the command framework and scene management framework from my previous tutorial, Thinking in Commands (Part 1, Part 2), and the examples also use the data manager class from Loading Data with Commands. I highly recommend that you go through these tutorials first before going on. Also, you’ll need the GreenSock Tweening Platform to complete the examples.


Step 1: Theory Sound Tracks

The sound track we’re talking about here has nothing to do with game or movie sound tracks. A sound track is an imaginary “track” associated with a playback of a single sound. One single sound track does not allow more than one sound playing at a time. If a sound track is currently playing a sound, we say it’s occupied. If another sound is to be played on an occupied sound track, the currently playing sound is stopped, and then the new one is played on the track. It is thus reasonable to play a single character’s voices on a single sound track, so as to avoid the sound overlapping issue mentioned earlier. Also, in most cases, there should be only one track for background music.

Let’s take a look at some conceptual figures. A single application can have multiple sound tracks.

Each sound track can hold one single playing sound.

If a sound is to be played on an occupied track, the “old” sound is first stopped, and then the “new” sound is played on the track.


Step 2: Theory The Framework

The Sound Management framework consistes of two classes, the SoundManager class and the SoundTrack class. Each sound track is assigned a unique key string. An occupied sound track’s underlying playing sound is actually a native SoundChannel object obtained from the native Sound.play() method, and the SoundManager class manages sound tracks and organizes the playback of sounds.

Here are some quick previews of the usage of the framework. The following code plays a new sound on a sound track associated with the key string “music”, where MySound is a sound class from the library.

//play a sound on the "music" track
SoundManager.play("music", new MySound());

If the same line of code is executed again before the playback is finished, the original sound is stopped, and a new sound is played on the “music” track.

//stop the original sound on the "music" track and play a new one
SoundManager.play("music", new MySound());

The SoundManager.stop() method stops a sound track associated with a specified key string.

//stop the "music" sound track
SoundManager.stop("music");

In order to transform the sound, like to adjust the volume, we’ll need to obtain a reference to a sound track’s underlying sound channel. The reference can be obtained by accessing the SoundTrack.channel property.

var channel:SoundChannel = SoundManager.getSoundTrack("music").channel;
var transform:SoundTransform = channel.soundTransform;
transform.volume = 0.5;
channel.soundTransform = transform;

Step 3: Classes The SoundTrack Class

Enough theory. Let’s get down to the coding. We are going to use different key strings to distinguish different sound tracks. Here’s the SoundTrack class, which essentially represents a key-channel pair. Details are described in comments.

package sounds {
	import flash.media.SoundChannel;

	/**
	 * A sound track represents a key-channel pair.
	 */
	public class SoundTrack{

		//read-only key value
		private var _key:String;
		public function get key():String { return _key; }

		//read-only sound channel reference
		private var _channel:SoundChannel;
		public function get channel():SoundChannel { return _channel; }

		public function SoundTrack(key:String, channel:SoundChannel) {
			_key = key;
			_channel = channel;
		}

		/**
		 * Stops the underlying sound channel.
		 */
		public function stop():void {
			_channel.stop();
		}
	}
}

Step 4: Classes The SoundManager Class

And here’s the SoundManager class. Note that the key-track association is handled by using the Dictionary class. A track is emptied automatically if a playing sound has reached its end.

package sounds {
	import flash.events.Event;
	import flash.media.Sound;
	import flash.media.SoundChannel;
	import flash.media.SoundTransform;
	import flash.utils.Dictionary;

	/**
	 * This class allows you to manage sounds in terms of sound tracks.
	 */
	public class SoundManager{

		//a dictionary that keeps tracks of all sound tracks
		private static var _soundTracks:Dictionary = new Dictionary();

		//a dictionary that maps a sound channel to its corresponding key for playback completion handling
		private static var _soundKeys:Dictionary = new Dictionary();

		/**
		 * Plays a sound and returns a corresponding sound track object.
		 */
		public static function play(key:String, sound:Sound, startTime:int = 0, loops:int = 0, transform:SoundTransform = null):SoundTrack {

			//if the sound track is occupied, stop the current sound track
			if (isPlaying(key)) stop(key);

			//play the sound, creating a new sound channel
			var channel:SoundChannel = sound.play(startTime, loops, transform);

			//listen for the complete event of the sound channel
			channel.addEventListener(Event.SOUND_COMPLETE, onSoundComplete);

			//create a new sound track
			var soundTrack:SoundTrack = new SoundTrack(key, channel);

			//add the sound track to the dictionary
			_soundTracks[key] = soundTrack;

			//add the channel-key mapping relation
			_soundKeys[channel] = key;

			return soundTrack;
		}

		/**
		 * Returns a reference to the sound track corresponding to the provided key string.
		 */
		public static function getSoundTrack(key:String):SoundTrack {
			return _soundTracks[key];
		}

		/**
		 * Determines if a sound track is currently playing.
		 */
		public static function isPlaying(key:String):Boolean {
			return Boolean(_soundTracks[key]);
		}

		/**
		 * Stops a sound track.
		 */
		public static function stop(key:String):void {
			var soundTrack:SoundTrack = _soundTracks[key];

			//check if the sound track exists
			if (soundTrack) {

				//stop the sound track
				soundTrack.stop();

				//and remove it from the dictionary
				delete _soundTracks[key];

				//along with the channel-key relation
				delete _soundKeys[soundTrack.channel];
			}
		}

		/**
		 * Removes a sound track when the sound playback is complete
		 */
		private static function onSoundComplete(e:Event):void {

			//cast the event dispatcher to a sound channel object
			var channel:SoundChannel = SoundChannel(e.target);

			//remove the event listener
			channel.removeEventListener(Event.SOUND_COMPLETE, onSoundComplete);

			//extract the corresponding key
			var key:String = _soundKeys[channel];

			//remove the sound track
			stop(key);
		}
	}
}

Step 5: Example Sound Manager Testdrive

Now let’s test our sound management framework. We’re going to compare the outcome of repeated requests to play a sound with and without using the sound manager.


Step 6: Example New Flash Document

Create a new Flash document (duh).


Step 7: Example Create Buttons

Create two buttons on the stage. You can draw your own and convert them to symbols, or you can, as in my case, drag two Button components from the Components panel. Name them “boing_btn” and “managedBoing_btn”.


Step 8: Example Import the Sound

Import the sound we’re going to play to the library. You can find the “Boing.wav” file in the example source folder.


Step 9: Example The Document Class

Finally, create an AS file for the document class. The code is rather straightforward, so I just explain everything in the comments.

package  {
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	import flash.media.Sound;
	import sounds.SoundManager;

	public class BoingPlayer extends Sprite {

		public function BoingPlayer() {

			//add the click listeners for both buttons
			boing_btn.addEventListener(MouseEvent.CLICK, onBoing);
			managedBoing_btn.addEventListener(MouseEvent.CLICK, onManagedBoing);
		}

		//play the sound directly by invoking the Sound.play() method.
		private function onBoing(e:MouseEvent):void {
			var sound:Sound = new Boing();
			sound.play();
		}

		//play the sound with the sound manager on the "boing" sound track
		private function onManagedBoing(e:MouseEvent):void {
			var sound:Sound = new Boing();
			SoundManager.play("boing", sound);
		}
	}
}

Step 10: Example Test Drive

We’re done. Press Ctrl+Enter to test the movie, and try rapidly clicking the buttons (remember to turn on your speakers). For the “Boing!” button, multiple sounds are overlapping when played. As for the “Managed Boing!” button, which makes use of the sound manager, one sound is forced to stop before the next one is played, so you won’t hear sounds mixed up together.


Step 11: Framework Integration

Commands, commands, commands. It’s always nice to integrate your work with your previous ones, right? Now we’re going to integrate the sound management framework with the command framework, along with the scene management framework. Again, if you’re not familiar with the command framework and the scene management framework, you’d better check them out in my previous tutorials (Part 1, Part 2) before going on.


Step 12: Framework PlaySound Command

The name of this command is pretty self-explanatory: it plays a sound with the sound manager.

package commands.sounds {
	import commands.Command;
	import flash.media.Sound;
	import flash.media.SoundTransform;
	import sounds.SoundManager;

	/**
	 * This command plays a sound.
	 */
	public class PlaySound extends Command {

		public var key:String;
		public var sound:Sound;
		public var startTime:int;
		public var loops:int;
		public var transform:SoundTransform;

		public function PlaySound(key:String, sound:Sound, startTime:int = 0, loops:int = 0, transform:SoundTransform = null) {
			this.key = key;
			this.sound = sound;
			this.startTime = startTime;
			this.loops = loops;
			this.transform = transform;
		}

		override protected function execute():void {

			//tell the sound manager to play the sound
			SoundManager.play(key, sound, startTime, loops, transform);

			//complete the command
			complete();
		}
	}
}

Step 13: Framework StopSound Command

This is basically the evil cousin of the previous command. This command stops a sound track by using the sound manager.

package commands.sounds {
	import commands.Command;
	import sounds.SoundManager;

	/**
	 * This command stops a sound track corresponding to a given key.
	 */
	public class StopSound extends Command {

		public var key:String;

		public function StopSound(key:String) {
			this.key = key;
		}

		override protected function execute():void {

			//tell the sound manager to stop the sound track, how evil >:]
			SoundManager.stop(key);

			//complete the command
			complete();
		}
	}
}

Step 14: Framework SoundLoad Command

This command loads an external MP3 file into a Sound object. Not until the loading is complete will the command’s complete() method be called. This allows us to easily chain together the command with other commands, without having to worry about handling the loading completion.

package commands.loading {
	import commands.Command;
	import flash.events.Event;
	import flash.media.Sound;
	import flash.net.URLRequest;

	/**
	 * This command loads a sound.
	 */
	public class SoundLoad extends Command {

		public var sound:Sound;
		public var url:URLRequest;

		public function SoundLoad(sound:Sound, url:URLRequest) {
			this.sound = sound;
			this.url = url;
		}

		override protected function execute():void {

			//add the complete listener
			sound.addEventListener(Event.COMPLETE, onComplete);

			//begin loading
			sound.load(url);
		}

		private function onComplete(e:Event):void {

			//remove the complete listener
			sound.removeEventListener(Event.COMPLETE, onComplete);

			//complete the command
			complete();
		}
	}
}

Integration complete. Get prepared for our final example!


Step 15: Example Managing Sounds with Commands

In this example, we’re going to allow users to play two music on the same sound track. If a sound is to be played when the sound track is occupied, the original music is first faded out, and then the new music is played. The fading-out is handled by the TweenMaxTo command, which internally uses the special property volume provided by the TweenMax class from GreenSock Tweening Platform. The two musics are external MP3 files loaded during run-time.

Note that we’re going to use the scene management framework. If you want to refresh your memory, go check it out here.


Step 16: Example Copy Flash Document

Make a copy of the FLA file used in the previous example. Rename the buttons to “music1_btn” and “music2_btn”. You can also change the button labels to “Music 1? and “Music 2?. And add an extra button named “stop_btn”, which is for stopping the music.


Step 17: Example Copy the MP3 Files

The MP3 files can be found in the source folder. Copy them to the same folder as the FLA file.


Step 18: Example Document Class

Create a new AS file for the document class of the new FLA file. Instantiate a scene manager, and initialize it to a loading state, where the two MP3 files are loaded.

package {
	import flash.display.Sprite;
	import scenes.SceneManager;

	public class MusicPlayer extends Sprite {

		public function MusicPlayer() {

			//instantiate a scene manager object
			var sceneManager:SceneManager = new SceneManager();

			//set a loading scene as the initial scene
			sceneManager.setScene(new LoadingScene(this));
		}
	}
}

Step 19: Example The Loading Scene

The loading scene instantiates two Sound objects for loading the two MP3 files. The buttons are set invisible at the beginning, and will be set visible again when the loading is finished. When the loading is complete, the scene immediately instructs the scene manager to transit to the main scene, as written in the overridden onSceneSet() method. Further details are described in the comments.

package {
	import commands.Command;
	import commands.data.RegisterData;
	import commands.loading.SoundLoad;
	import commands.ParallelCommand;
	import commands.SerialCommand;
	import commands.utils.SetProperties;
	import flash.events.Event;
	import flash.media.Sound;
	import flash.net.URLRequest;
	import scenes.Scene;

	public class LoadingScene extends Scene {

		//a reference to the document root container
		private var container:MusicPlayer;

		public function LoadingScene(container:MusicPlayer) {
			this.container = container;
		}

		override public function createIntroCommand():Command {

			//create two sound objects to load the two MP3 files
			var music1:Sound = new Sound();
			var music2:Sound = new Sound();

			var command:Command =
				new ParallelCommand(0, 

					//hide the buttons
					new SetProperties(container.music1_btn, {alpha:0, visible:false}),
					new SetProperties(container.music2_btn, {alpha:0, visible:false}),
					new SetProperties(container.stop_btn, {alpha:0, visible:false}), 

					//register the two sound objects to the data manager
					new RegisterData("music1", music1),
					new RegisterData("music2", music2), 

					//begin the loading of the MP3 files
					new SoundLoad(music1, new URLRequest("Music1.mp3")),
					new SoundLoad(music2, new URLRequest("Music2.mp3"))
				);

			return command;
		}

		override public function onSceneSet():void {

			//tell the scene manager to switch to the main scene directly after the intro command is complete
			sceneManager.setScene(new MainScene(container));
		}
	}
}

Step 20: Example The Main Scene

The main scene brings the hidden buttons back to visible, and registers the playMusic() method and the stopMusic() method as listeners for the click event. In the playMusic() method, a serial command is executed if the “bgm” sound track is occupied. The command first temporarily removes the click listeners, fades out the current music, stops the current music, plays the new music on the now-empty “bgm” sound track, and then finally re-adds the click listeners. The stopMusic method does basically the same thing, only that there’s no new music playback. This complex series of actions is carried out in only a few lines of clean code. Pretty neat, huh?

Note that adding and removing the listeners are common actions that are present in both the playMusic() method and the stopMusic() method. So they are factored out as two private properties, addListeners and removeListeners, initialized in the constructor.

package {
	import commands.Command;
	import commands.events.AddEventListener;
	import commands.events.RemoveEventListener;
	import commands.greensock.TweenMaxTo;
	import commands.ParallelCommand;
	import commands.SerialCommand;
	import commands.sounds.PlaySound;
	import commands.sounds.StopSound;
	import data.DataManager;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.media.Sound;
	import flash.utils.Dictionary;
	import scenes.Scene;
	import sounds.SoundManager;
	import sounds.SoundTrack;

	/**
	 * The main scene is displayed when the loading is complete.
	 */
	public class MainScene extends Scene {

		//a reference to the document root container
		private var container:MusicPlayer;

		private var btn1:Sprite;
		private var btn2:Sprite;
		private var btn3:Sprite;
		private var dataKeys:Dictionary = new Dictionary();

		private var addListeners:Command;
		private var removeListeners:Command;

		public function MainScene(container:MusicPlayer) {
			this.container = container;

			btn1 = container.music1_btn;
			btn2 = container.music2_btn;
			btn3 = container.stop_btn;

			//data keys used to retrieve sound objects from the data manager in the playMusic() method
			dataKeys[btn1] = "music1";
			dataKeys[btn2] = "music2";

			//this command adds all listeners
			addListeners =
				new ParallelCommand(0,
					new AddEventListener(btn1, MouseEvent.CLICK, playMusic),
					new AddEventListener(btn2, MouseEvent.CLICK, playMusic),
					new AddEventListener(btn3, MouseEvent.CLICK, stopMusic)
				);

			//this command removes all listeners
			removeListeners =
				new ParallelCommand(0,
					new RemoveEventListener(btn1, MouseEvent.CLICK, playMusic),
					new RemoveEventListener(btn2, MouseEvent.CLICK, playMusic),
					new RemoveEventListener(btn3, MouseEvent.CLICK, stopMusic)
				);
		}

		override public function createIntroCommand():Command {

			var command:Command =
				new SerialCommand(0, 

					//fade in the buttons
					new ParallelCommand(0,
						new TweenMaxTo(btn1, 1, {autoAlpha:1}),
						new TweenMaxTo(btn2, 1, {autoAlpha:1}),
						new TweenMaxTo(btn3, 1, {autoAlpha:1})
					), 

					//add click listeners
					addListeners
				);

			return command;
		}

		/**
		 * Plays the music.
		 */
		private function playMusic(e:Event):void {

			//retrieve the sound object corresponding to a data key
			var music:Sound = DataManager.getData(dataKeys[e.target]);

			//check if the BGM sound track is already playing
			if (SoundManager.isPlaying("bgm")) {

				//retrieve the playing sound track
				var soundTrack:SoundTrack = SoundManager.getSoundTrack("bgm");

				var command:Command =
					new SerialCommand(0,
						//temporarily remove click listeners
						removeListeners, 

						//fade out the current sound track
						new TweenMaxTo(soundTrack.channel, 1, {volume:0}), 

						//and then stop the sound track
						new StopSound("bgm"), 

						//play a new sound on the same sound track
						new PlaySound("bgm", music, 0, int.MAX_VALUE), 

						//re-add click listeners
						addListeners
					);

				command.start();
			} else {

				//just play the sound if the sound track is idle
				SoundManager.play("bgm", music, 0, int.MAX_VALUE);
			}
		}

		/**
		 * Stops the music that is currently playing.
		 */
		private function stopMusic(e:Event):void {

			//check if the BGM sound track is already playing
			if (SoundManager.isPlaying("bgm")) {

				//retrieve the playing sound track
				var soundTrack:SoundTrack = SoundManager.getSoundTrack("bgm");

				var command:Command =
					new SerialCommand(0,
						//temporarily remove click listeners
						removeListeners, 

						//fade out the current sound track
						new TweenMaxTo(soundTrack.channel, 1, {volume:0}), 

						//and then stop the sound track
						new StopSound("bgm"), 

						//re-add click listeners
						addListeners
					);

				command.start();
			}
		}
	}
}

Step 21: Example Test the Movie

We’re ready to test the movie. Press CTRL+ENTER to test it. When you click a button, a music begins to play. After clicking another, the music fades out, and then a new one starts from the beginning.


Step 22: Extra Code Jockey Version

It’s the end of the tutorial, I know. But I just couldn’t resist from showing this to you. If you’re a code jockey, I bet you’ve already noticed that there are lots of similarities in the playMusic() method and the stopMusic() method. Why not refactor them into a single one? If you’re not interested in this code jocky version of music player, you may skip to the summary section. Otherwise, just go on reading!

First, replace all the playMusic and stopMusic in the source code with handleMusic, our new event listener. Next, delete the playMusic and the stopMusic method, and add the following handleMusic() method in the main scene class.

/**
 * Plays or stops the music. Code jockey version.
 */
private function handleMusic(e:Event):void {

	var music:Sound = DataManager.getData(dataKeys[e.target]);

	if (SoundManager.isPlaying("bgm")) {

		var soundTrack:SoundTrack = SoundManager.getSoundTrack("bgm");

		var command:Command =
			new SerialCommand(0,
				removeListeners,
				new TweenMaxTo(soundTrack.channel, 1, {volume:0}),
				new StopSound("bgm"), 

				//determine if we're going to play another music
				(music)?
				(new PlaySound("bgm", music, 0, int.MAX_VALUE)):
				(new Dummy()), 

				addListeners
			);

		command.start();
	} else {
		if (music) SoundManager.play("bgm", music, 0, int.MAX_VALUE);
	}
}

You’ll notice that the only major difference between this method and the original listeners, is the following chunk of code:

(music)?
(new PlaySound("bgm", music, 0, int.MAX_VALUE)):
(new Dummy()),

What the hell is this anyway? This is actually the ?: conditional operator. It’s a ternary operator, meaning that it requires three operands, A, B, and C. The statement “A?B:C” evaluates to B if A is true, or C otherwise. The music variable is supposed to hold a reference to a Sound object, so that the variable evaluates to true. However, if the event dispatcher target is the “stop_btn” button, the variable contains a null value, which evaluates to false in the ternary operator. So, if the two music buttons are clicked, the above code chunk is regarded as the single line of code below.

new PlaySound("bgm", music, 0, int.MAX_VALUE)

Otherwise, if the stop button is clicked, the code chunk is regarded as a dummy command, which simply does nothing.

new Dummy()

Just one other thing to notice. The following line of code

SoundManager.play("bgm", music, 0, int.MAX_VALUE);

is changed to

if (music) SoundManager.play("bgm", music, 0, int.MAX_VALUE);

This is for the handling the exception that the sound track is currently empty. If you can understand the code chunk above, I’m pretty sure you could figure out what this line is all about.

Test the movie by pressing Ctrl+Enter, you’ll see the exact same result as the last example. You can regard it as a fulfillment of a code jockey’s coding vanity.


Summary

In this tutorial, you have learned how to manage sounds with sound tracks. One sound track allows only one sound being played at a time, therefore ideal to represent a single character’s voice or background music. Also, you’ve seen how to integrate the sound management framework with the command framework, which gives you a huge maintainability and flexibility boost on your applications.

This is the end of the tutorial. I hope you enjoyed it. Thank you very much for reading!

Leave a Reply

Your email address will not be published. Required fields are marked *