26.3 Developing the Jukebox Application
In the next sections, you will create both the SoundController
component and the main jukebox movie code that is responsible for
making this application work. All the subsequent code is contained
within a single Flash document. Therefore, the first step in this
process is to create a new Flash document
named
jukebox.fla and save it.
26.3.1 Adding the Sound Controller
The JukeboxController component
is an essential part of the jukebox application. It is required to
play the songs. The component uses two SoundController components so
that one song can be queued while another is playing. Here are the
steps to follow to complete the component:
The jukebox controller requires the SoundController component from
Recipe 13.16. If you completed that
exercise, you should have a Sound Controller menu option in the
Components panel, which contains a SoundController component.
Otherwise, you can download and install the completed SoundController
component from http://www.person13.com/ascb/components/soundController.zip. Once you have the SoundController component installed, create a copy
of it in your jukebox movie's Library by dragging an
instance onto the Stage and deleting the instance. The symbol remains
in the Library, even after the instance is deleted. Create a new movie clip symbol named
JukeboxController. Open the linkage properties for the
JukeboxController symbol using the Library
panel's pop-up Options menu. Select the Export for ActionScript and Export on First Frame
checkboxes. Give the symbol a linkage identifier of
JukeboxControllerSymbol. Click OK to close the Linkage Properties dialog box. Edit the JukeboxController symbol. Add the following code to the first frame of the default layer: // Make sure to enclose the code in #initclip/#endinitclip so that this code
// executes before any code on the main timeline.
#initclip
// In the JukeboxController constructor, create two SoundController instances and
// store references to them in the currentPlayer and queuePlayer properties. Hide
// the second player instance by setting its _alpha to 0.
function JukeboxController ( ) {
this.attachMovie("SoundControllerSymbol", "player2", _root.getNewDepth( ));
this.attachMovie("SoundControllerSymbol", "player1", _root.getNewDepth( ));
this.player2._alpha = 0;
this.currentPlayer = this.player1;
this.queuePlayer = this.player2;
}
// Make sure that JukeboxController subclasses MovieClip.
JukeboxController.prototype = new MovieClip( );
// The loadSong( ) method uses the SoundController's setTarget( ) method to set
// currentPlayer's target sound and tell it to play once it is loaded.
JukeboxController.prototype.loadSong = function (snd) {
this.currentPlayer.setTarget(snd, true);
};
// The loadQueue( ) method sets the target sound for queuePlayer without playing
// the sound immediately.
JukeboxController.prototype.loadQueue = function (snd) {
this.queuePlayer.setTarget(snd);
};
// The startNext( ) method starts the next song.
JukeboxController.prototype.startNext = function ( ) {
// Use the custom fade( ) method (from MovieClip.as in Chapter 7) to fade
// the currentPlayer sound down and the queuePlayer sound up.
this.currentPlayer.fade(5);
this.queuePlayer.fade(5, true);
// Swap the references for currentPlayer and queuePlayer. If currentPlayer was
// player1, it becomes player2 and queuePlayer becomes player1.
var tmp = this.currentPlayer;
this.currentPlayer = this.queuePlayer;
this.queuePlayer = tmp;
// Swap the sound controllers' depths so that
// currentPlayer has the greater depth.
this.currentPlayer.swapDepths(this.queuePlayer);
// If the new current song is loaded, begin playback. Otherwise, the
// onEnterFrame( ) method monitors it until it is loaded, then begins playback.
if (this.currentPlayer.snd.isLoaded) {
this.currentPlayer.start( );
} else {
this.currentPlayer.onEnterFrame = function ( ) {
if (this.snd.isLoaded) {
this.start( );
delete this.onEnterFrame;
}
}
}
};
// Register the class to the corresponding linkage identifier name.
Object.registerClass("JukeboxControllerSymbol", JukeboxController);
#endinitclip
The JukeboxController component code is not very difficult once you
examine it more closely.
To begin with, it creates two instances of the SoundController
component. One controller is visible while the other is hidden. We
set the _alpha property to 0 instead of setting
the _visible property to false.
This hides the second controller but also allows us to fade visually
between the two controllers. We create two properties that references
the two controller instances, allowing us to switch between them
easily.
function JukeboxController ( ) {
this.attachMovie("SoundControllerSymbol", "player2", _root.getNewDepth( ));
this.attachMovie("SoundControllerSymbol", "player1", _root.getNewDepth( ));
this.player2._alpha = 0;
this.currentPlayer = this.player1;
this.queuePlayer = this.player2;
}
The startNext( ) method is responsible for switching
between the two sound controllers. First, it calls the custom
MovieClip.fade(
) method to fade visually between the two
controller instances. If we simply set the
_visible properties of the two controllers to
true and false, there is no
need to change the depths. However, because we adjust the
_alpha property instead, we must ensure that the
controller for the current song has the greater depth using the
swapDeths( ) method. If the target sound for the
new current player is loaded, we play it immediately. Otherwise, it
begins playing as soon as it has loaded. In later sections,
you'll see that the code also fades in sounds as
they start and fades out sounds as they stop.
JukeboxController.prototype.startNext = function ( ) {
this.currentPlayer.fade(5);
this.queuePlayer.fade(5, true);
var tmp = this.currentPlayer;
this.currentPlayer = this.queuePlayer;
this.queuePlayer = tmp;
this.currentPlayer.swapDepths(this.queuePlayer);
if (this.currentPlayer.snd.isLoaded) {
this.currentPlayer.start( );
} else {
this.currentPlayer.onEnterFrame = function ( ) {
if (this.snd.isLoaded) {
this.start( );
delete this.onEnterFrame;
}
};
}
};
26.3.2 Creating the Main Jukebox Movie
The final step in the jukebox application
is to create the main jukebox Flash movie that incorporates all of
the other elements. The jukebox movie has the following
functionality:
A text field in which a URL/path to an MP3 file can be typed directly
to be added to the playlist Buttons that open the local and server MP3 selectors A menu in which the playlist is displayed (items can be reordered and
deleted) A jukebox controller to control the playback of the MP3s
Here are the steps you should complete to create the main jukebox
movie:
Open jukebox.fla if it is not already open. Add the following code to the default layer of the main timeline: // Include Sound.as from Chapter 13 and Forms.as from Chapter 11.
#include "Sound.as"
#include "Forms.as"
// Include DataGlue.as and RecordSet.as,
// which come with the Flash Remoting components.
#include "DataGlue.as"
#include "RecordSet.as"
// The init( ) function creates a local connection that listens for songs that
// have been added via the local or server MP3 selectors. Additionally, the
// init( ) function creates a recordset to hold the playlist information.
function init ( ) {
receiver = new LocalConnection( );
receiver.receivePathInfo = function (path) {
if (path != "" && path != undefined && path != null) {
_root.addSongToList(path);
}
};
receiver.connect("pathSendConnection");
songsList = new RecordSet("url, snd");
}
// The createElements( ) method creates all the component instances and text
// fields.
function createElements ( ) {
_root.attachMovie("FListBoxSymbol", "songsListBox", _root.getNewDepth( ));
_root.createAutoTextField("newSongURLLabel", _root.getNewDepth( ),
0, 0, 0, 0, "URL to MP3:");
_root.createInputTextField("newSongURL", _root.getNewDepth( ));
_root.attachMovie("FPushButtonSymbol", "moveUpBtn", _root.getNewDepth( ));
_root.attachMovie("FPushButtonSymbol", "moveDownBtn", _root.getNewDepth( ));
_root.attachMovie("FPushButtonSymbol", "removeBtn", _root.getNewDepth( ));
_root.attachMovie("FPushButtonSymbol", "addSoundBtn", _root.getNewDepth( ));
_root.attachMovie("FPushButtonSymbol", "startPlayBtn", _root.getNewDepth( ));
_root.attachMovie("FPushButtonSymbol", "getLocalBtn", _root.getNewDepth( ));
_root.attachMovie("FPushButtonSymbol", "getServerBtn", _root.getNewDepth( ));
_root.attachMovie("JukeboxControllerSymbol", "jukeboxCtrl", _root.getNewDepth( ));
addSoundBtn.setLabel("Add Song");
moveUpBtn.setLabel("Move Up");
moveDownBtn.setLabel("Move Down");
removeBtn.setLabel("Remove Song");
startPlayBtn.setLabel("Begin Playback");
getLocalBtn.setLabel("Get Local MP3");
getServerBtn.setLabel("Get Server MP3");
addSoundBtn.setClickHandler("addSound");
startPlayBtn.setClickHandler("startNextSong");
moveUpBtn.setClickHandler("moveSongUp");
moveDownBtn.setClickHandler("moveSongDown");
removeBtn.setClickHandler("removeSong");
getLocalBtn.setClickHandler("getMP3");
getServerBtn.setClickHandler("getMP3");
}
// The layoutElements( ) function positions the components and text fields in a
// table.
function layoutElements ( ) {
tc0 = new TableColumn(5, songsListBox);
tc1 = new TableColumn(5, startPlayBtn, moveUpBtn, moveDownBtn, removeBtn);
tc2 = new TableColumn(5, newSongURLLabel, newSongURL, addSoundBtn,
getLocalBtn, getServerBtn);
tc3 = new TableColumn(5, jukeboxCtrl);
tr0 = new TableRow(5, tc0);
tr1 = new TableRow(5, tc1, tc2);
tr2 = new TableRow(5, tc3);
t = new Table(5, 0, 0, tr0, tr1, tr2);
}
// The formatSongsList( ) function is used by the DataGlue.bindFormatFunction( )
// call in updateListView( ) to format each element in the song list.
function formatSongsList(element) {
// Get the name of the MP3 from the URL/path. If you split
// the URL using "/" as the delimiter, the name of the MP3 is
// the last element of the resulting array.
var urlAr = element.url.split("/");
if (urlAr.length == 1) {
urlAr = element.url.split("\\");
}
var name = urlAr[urlAr.length - 1];
// Create the object to return to bindFormatFunction( ). The data should be the
// element itself, and the label should be the name of the MP3 file.
var obj = new Object( );
obj.data = element;
obj.label = name;
// If the song is already loaded, display its duration next to the name.
// Otherwise, display the percentage that has loaded.
if (element.snd.isLoaded) {
obj.label += " (" + SoundController.timeDisplay(element.snd.duration) + ")";
} else {
obj.label += " [" + element.snd.percentLoaded + "%]";
}
return obj;
}
// The updateListView( ) function is called continually to update the playlist
// menu to reflect load progress while any songs are loading.
function updateListView ( ) {
var selected = _root.songsListBox.getSelectedIndex( );
DataGlue.bindFormatFunction(_root.songsListBox, _root.songsList,
_root.formatSongsList);
_root.songsListBox.adjustWidth( );
_root.songsListBox.setSelectedIndex(selected);
}
// The addSongToList( ) function adds a song to the playlist from a URL.
function addSongToList (url) {
// Load a sound into a new sound object created using the custom
// createNewSound( ) method from Recipe 13.1.
var mySound = Sound.createNewSound( );
mySound.loadSound(url);
// Add the song to the songsList recordset.
songsList.addItem({url: url, snd: mySound});
// Create an onEnterFrame( ) method that calls updateListView( ) continually
// until the song is loaded.
mySound.mc.onEnterFrame = function ( ) {
_root.updateListView( );
if (this.parent.isLoaded) {
delete this.onEnterFrame;
}
};
}
// The startNextSong( ) function plays the next song in the playlist.
function startNextSong ( ) {
// The counter variable keeps track of which song is currently being played. If
// it is undefined or greater than the number of songs, reset it to 0.
if (counter >= songsListBox.getLength( ) || counter == undefined) {
counter = 0;
}
// Highlight the current song in the playlist.
songsListBox.setSelectedIndex(counter);
// Get the sound object for the current song.
var snd = songsListBox.getItemAt(counter).data.snd;
// Set the sound to fade in for the first five seconds
// and to fade out for the last five seconds.
snd.fadeIn(5000);
snd.fadeOut(5000);
// When the song fades out, call this function (startNextSong( )) again.
snd.setOnFadeOut("startNextSong");
// When the song ends (as opposed to when it starts fading out), call
// stopPlayer( ) to stop the song.
snd.setOnStop("stopPlayer");
// Load the new song into the queue and tell the
// jukebox controller to play the next song.
jukeboxCtrl.loadQueue(snd);
jukeboxCtrl.startNext( );
counter++;
}
// When the end of the song is reached, this function stops the song.
function stopPlayer ( ) {
_root.jukeboxCtrl.queuePlayer.stop( );
}
// The addSound( ) function is the click handler for the Add Song button; it adds
// a song to the playlist from the URL typed into the text field.
function addSound ( ) {
var url = newSongURL.text;
newSongURL.text = "";
addSongToList(url);
}
// The moveSongUp( ) function moves a song up in the playlist.
function moveSongUp ( ) {
// Get the index of the of playlist item the user has selected.
var selected = songsListBox.getSelectedIndex( );
// Move the song only if it isn't already first in the list.
if (selected > 0) {
// Insert the item into the list at one index prior to its current position.
songsList.addItemAt(selected - 1, songsListBox.getItemAt(selected).data);
// Remove the item from its original position in the list.
songsList.removeItemAt(selected + 1);
// Update the view of the playlist and set the selected index such that the
// song that was moved is highlighted.
updateListView( );
songsListBox.setSelectedIndex(selected - 1);
}
}
// The moveSongDown( ) function does the opposite of the moveSongUp( ) function.
function moveSongDown ( ) {
var selected = songsListBox.getSelectedIndex( );
// Move the song only if it isn't already last in the list.
if (selected < songsListBox.getLength( ) - 1) {
songsList.addItemAt(selected + 2, songsListBox.getItemAt(selected).data);
songsList.removeItemAt(selected);
updateListView( );
songsListBox.setSelectedIndex(selected + 1);
}
}
// The removeSong( ) function removes a song from the list.
function removeSong ( ) {
// Get the index of the selected song in the playlist.
var selected = songsListBox.getSelectedIndex( );
// Make sure a song is selected before trying to remove one.
if (selected != undefined) {
// Get the sound associated with the song.
var snd = songsList.getItemAt(selected).snd;
// Remove the song from the playlist.
songsList.removeItemAt(selected);
updateListView( );
if (selected < counter) {
// If the index of the deleted song is less than counter (the index of the
// song that is currently playing), decrement counter.
counter--;
} else if (selected == counter) {
// If the deleted song was the current song, fade out the song from its
// current play position.
snd.fadeOut(5000, snd.position);
}
}
}
// The getMP3( ) function is the click handler for the
// getLocalBtn and getServerBtn buttons. It opens the HTML
// pages for the selectors in new browser windows.
function getMP3 (btn) {
if (btn == getLocalBtn) {
this.getURL("javascript:void(window.open('localFileForm.html',
'_blank', 'width=300,height=150'))");
}
else if (btn == getServerBtn) {
this.getURL("javascript:void(window.open('directoryBrowser.html',
'_blank', 'width=210,height=360'))");
}
}
init( );
createElements( );
layoutElements( ); Save the Flash document and publish the .swf and
.html files.
At this point, you have completed the entire jukebox application. You
can test
it by opening jukebox.html in a web browser. If
you are using the server MP3 selector, you need to open
jukebox.html so that it is served by the web
server. For example, if you have a web server running on your local
machine, you can use
http://localhost:8500/jukebox.html.
Most of the code in the main jukebox movie becomes clearer with a
little further examination, so let's take a look at
some of the code elements in more detail.
The init( ) function creates the
local connection object that
listens for communications from both MP3 selectors. Both MP3
selectors send messages over the connection named
"pathSendConnection" to a
receivePathInfo(
) method, and they pass that method the
URL/path to the selected MP3. The receivePathInfo(
) method passes the URL/path along to the
addSongToList(
) function to add the song to the
playlist. The init( ) function also creates a
recordset that is used to keep track of the songs in the playlist. A
recordset is convenient for populating a list box with complex data,
which in this case is a URL/path to the MP3 file as well as a
Sound object for the song.
function init ( ) {
receiver = new LocalConnection( );
receiver.receivePathInfo = function (path) {
if (path != "" && path != undefined && path != null) {
_root.addSongToList(path);
}
};
receiver.connect("pathSendConnection");
songsList = new RecordSet("url, snd");
}
The formatSongsList(
) function is a formatter function that
is used by the DataGlue.bindFormatFunction(
) method in the updateListView(
) function. The playlist displays the
name of each song as well as the duration of the song or the
percentage that has loaded. These values are obtained from the
songsList recordset. Each record has a
url column that contains the URL/path to the MP3
file. You can extract the name of the MP3 from this value using the
same technique discussed earlier (in the MP3 selector code), in which
you split the URL/path into an array using
"/" as the delimiter. If the
url value is a URL, it should be split using a
forward slash (/) as the delimiter. However, if the MP3 has been
added to the playlist from the local MP3 selector, the
url can potentially use backslashes. In that case,
split the value into an array using the backslash (\) as the
delimiter. Don't forget to escape the backslash
delimiter. Additionally, each item in the playlist list box should
contain a data value that includes both the
URL/path to the MP3 as well as the Sound object
into which the song has been (or is being) loaded. You can accomplish
this by setting the list box element's
data property to the record from the recordset.
var urlAr = element.url.split("/");
if (urlAr.length == 1) {
urlAr = element.url.split("\\");
}
var name = urlAr[urlAr.length - 1];
var obj = new Object( );
obj.data = element;
obj.label = name;
If the song has already loaded, you can use the
duration property to determine its length in
seconds. The SoundController class (the class
for the SoundController component) includes a static
timeDisplay( ) method to format the time in standard
minutes and seconds. Otherwise, if the song has not yet loaded, you
cannot access the duration, so you should display the percentage that
has loaded instead.
if (element.snd.isLoaded) {
obj.label += " (" + SoundController.timeDisplay(element.snd.duration) + ")";
} else {
obj.label += " [" + element.snd.percentLoaded + "%]";
}
The updateListView(
) function is called to update the
playlist display, such as when a new song is added or an old one is
moved or removed. Also, the display is updated to show a
song's load progress. The updateListView(
) function uses the DataGlue.bindFormatFunction(
) method to update the contents of the list box. However,
there are a few other things that need to be done for formatting and
display to be correct. First of all, because the MP3 names can vary
in length, use the adjustWidth(
) method to resize the list box to fit its
contents. Also, call setSelectedIndex( ) to
reselect the item that was selected before updateListView(
) was invoked.
function updateListView ( ) {
var selected = _root.songsListBox.getSelectedIndex( );
DataGlue.bindFormatFunction(_root.songsListBox, _root.songsList,
_root.formatSongsList);
_root.songsListBox.adjustWidth( );
_root.songsListBox.setSelectedIndex(selected);
}
The addSongToList(
) method is called when the user clicks
the Add Song button or adds a song via the local or server MP3
selector. The function adds the new song to the
songsList recordset. The URL value is passed
automatically to the addSongToList( ) function,
so that part is already known. For the snd column,
the function creates a new Sound object and
loads the song into it. Finally, addSongToList(
) attaches an onEnterFrame( ) method
to the mc property of the
Sound object—the mc
property is a reference to the sound-holder movie clip created
automatically by Sound.createNewSound( ). The
onEnterFrame( ) method calls the
updateListView( ) function continually until the
song has completed loading.
function addSongToList (url) {
var mySound = Sound.createNewSound( );
mySound.loadSound(url);
songsList.addItem({url: url, snd: mySound});
mySound.mc.onEnterFrame = function ( ) {
_root.updateListView( );
if (this.parent.isLoaded) {
delete this.onEnterFrame;
}
};
}
The startNextSong(
) function is interesting in that it is
both the click handler function for the Begin Playback button and the
onFadeOut callback function for each song. When
the function is invoked, it starts the next song in the playlist. The
code in this function is central to the core functioning of the
jukebox, so let's look at each piece of it. First,
it uses the counter variable to keep track of
which song is being played. The startNextSong( )
function increments counter each time it
completes. Therefore, if counter is greater than
the number of songs in the playlist, start back at the beginning of
the playlist by setting counter to 0.
if (counter >= songsListBox.getLength( ) || counter == undefined) {
counter = 0;
}
Each time the next song starts, you want to highlight that song in
the playlist. You can do this with the setSelectedIndex(
) method:
songsListBox.setSelectedIndex(counter);
Each item in the playlist list box has data that includes both
url and snd properties. The
snd property is a Sound
object into which the song has been loaded, and we can use it to
instruct the song to begin playing. To perform a cross-fade between
songs, each song should fade in for five seconds at its beginning and
fade out for five seconds at its end. The next song should start when
the onFadeOut event occurs within the current
song. This way, the next song begins playing at the point that the
previous song begins to fade out.
var snd = songsListBox.getItemAt(counter).data.snd;
snd.fadeIn(5000);
snd.fadeOut(5000);
snd.setOnFadeOut("startNextSong");
snd.setOnStop("stopPlayer");
jukeboxCtrl.loadQueue(snd);
jukeboxCtrl.startNext( );
counter++;
The moveSongUp( ) and moveSongDown(
) functions change the order of the
selected song in the playlist. They first insert a copy of the
selected item into the playlist at the new position using the
addItemAt( ) method, which automatically shifts the
subsequent items in the list box. Then, they simply delete the
selected song from the original position using the
removeItemAt( ) method. Here is the code for
moveSongUp( ); moveSongDown(
) is similar:
function moveSongUp ( ) {
var selected = songsListBox.getSelectedIndex( );
if (selected > 0) {
songsList.addItemAt(selected - 1, songsListBox.getItemAt(selected).data);
songsList.removeItemAt(selected + 1);
updateListView( );
songsListBox.setSelectedIndex(selected - 1);
}
}
The removeSong( ) function is the click handler
function for the Remove Song button. The function makes sure that a
song is selected before trying to remove anything. If the
getSelectedIndex( ) method for
songsListBox returns undefined,
the function doesn't remove anything. If a song is
selected, the function does several things. First, it retrieves the
Sound object associated with the song (because
it cannot get the reference after deleting it). It then removes the
song from the playlist by deleting it from the recordset and calling
updateListView( ) to refresh the
playlist's display. If the removed song is currently
playing, it fades out the song using fadeOut( ).
Because of the way that you have configured each song in the
startNextSong( ) function, when the current song
is instructed to fade out, the next song automatically starts.
function removeSong ( ) {
var selected = songsListBox.getSelectedIndex( );
if (selected != undefined) {
var snd = songsList.getItemAt(selected).snd;
songsList.removeItemAt(selected);
updateListView( );
if (selected < counter) {
counter--;
} else if (selected == counter) {
snd.fadeOut(5000, snd.position);
}
}
}
|