DekGenius.com
[ Team LiB ] Previous Section Next Section

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:

  1. 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.

  2. 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.

  3. Create a new movie clip symbol named JukeboxController.

  4. Open the linkage properties for the JukeboxController symbol using the Library panel's pop-up Options menu.

  5. Select the Export for ActionScript and Export on First Frame checkboxes.

  6. Give the symbol a linkage identifier of JukeboxControllerSymbol.

  7. Click OK to close the Linkage Properties dialog box.

  8. Edit the JukeboxController symbol.

  9. 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:

  1. Open jukebox.fla if it is not already open.

  2. 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(  );
  3. 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);
    }
  }
}
    [ Team LiB ] Previous Section Next Section