The Ultimate Home Jukebox

Dr. Dobb's Journal January 2000

MP3 puts your CD collection onto your hard disk

By Charlie Munro and Mark Nelson

Mark and Charlie work on IP telephony software for Cisco Systems in Dallas, Texas. Mark's most recent book is Serial Communications: A C++ Developer's Guide, Second Edition (IDG Books, 1999). Mark can be reached at markn@ieee.org. Charlie's email address is cmunro@cisco.com.

Last summer a remarkable thing happened in Dallas, and for once it did not have anything to do with triple digit temperatures or the Dallas Cowboys. In fact, this story didn't even make the front page of the newspaper; it was buried in a garish Fry's advertisement. What was it that made this advertisement newsworthy? Simply that an 18-GB disk drive could be had for the price of $180.

What makes this so interesting is that it means we have now reached the point where a megabyte of hard disk space can be had for the grand total of one cent. And doing the math led us to an inescapable conclusion -- that the ultimate home jukebox is now within reach.

Laying Down Tracks

After a quick trip to Fry's, followed by a few hours of rehab work on some older PCs, we had built the foundation for inexpensive home music servers that were ready to be loaded with our personal collections. Our choice of format for recording music from CDs is MPEG-1 audio layer 3, popularly known as "MP3." We found that most CDs can be digitally converted to MP3 format at a rate of 160 Kbits/sec. with virtually no loss of fidelity. (Trained ears or high-quality audio gear might require higher rates.) This meant that our new 18-GB drives could hold up to 300 CDs -- enough for a good-sized personal collection.

Creating MP3 recordings on your PC involves two steps -- ripping and encoding. Ripping refers to the process of extracting the music from the CD itself. Ideally, this is done digitally, reading the actual bits directly from disk. We were able to perform digital ripping on most machines most of the time, but in some situations, we had to fall back to analog ripping. This less-desirable method involves playing the CD tracks through your sound card, then digitizing its analog output. (The slight decrease in fidelity may be minimal, but human psychology seems to magnify the loss out of proportion.)

Once the tracks have been ripped to your hard disk (usually as WAV files), the encoder converts them to MP3 files. The MP3 files can be encoded as low as 16 Kbits/sec. and as high as 320 Kbits/sec. Our selected rate of 160 Kbits/sec. reduces the WAV file size by roughly 9:1.

Integrated programs that rip/encode generally also take advantage of online CD track databases. These databases keep playlists for as many titles as possible, and can be accessed via the Internet (see http://www.cddb.com/ for more information on these databases). The encoder can embed this track information in the MP3 file in an information element known as the "ID3 tag."

Figure 1 is a typical ripping session in progress. The program in question, AudioCatalyst, is an integrated ripper/encoder from Xing (http://www.xingtech.com/), a company that produces multimedia compression and rendering engines.

Getting Organized

The point of putting your entire CD collection on a hard drive is ease of access. Being able to play what you want, when you want, is like being the program director of your own radio station.

Making the database of music easy to work with involved a couple of different design decisions. The first decision we made was the naming scheme for the files and directories that would hold the music repository. Each CD was placed in its own directory, which fell under a virtual root using the directory naming scheme of Artist/Title. The individual songs were named according to the scheme Track Number; Title.mp3, for instance. Figure 2 illustrates the resulting organization.

This naming scheme was really designed for human navigation. Regardless of the software used to play our music, we knew we could find individual CDs or tracks quickly and easily. With Windows drag-and-drop, it's easy to quickly select individual tracks from a CD and drag them onto a player. This gives us the convenience of a simple database without any real programming.

This directory structure is good, but scanning it when looking for music can be time consuming. Asking the operating system to traverse the structure with a library of several hundred CDs can mean minutes of disk activity. To avoid doing this constantly, we also created a single directory of M3U playlists. By making our Playlist folder a Virtual Directory on the web server, we have an easy way to link to these files from a web page.

Most popular MP3 players (including the Windows Media Player; http://www .microsoft.com/) support the M3U format, which is actually just an ASCII list of files. Figure 3 is a typical M3U file. The file names are given as network shared drives. This is critical for making music playable on any system in your home network. It also lets us expand our collection across disks or even machines. M3U files can be created using the command-line DIR command, or using most popular players. We usually use WinAmp's (http://www.wina.com/) playlist editor to create ours.

The Friendly Front End

Once you've gone to the trouble of setting up a home music server, you are probably already thinking about serving up HTML pages from the same system. We thought it would be nice to have a web page that listed music in a somewhat organized form. Better yet, the web page ought to be able to dynamically create the index based on the contents of an M3U directory.

It takes a bit more than plain HTML to accomplish this, but nothing too sophisticated. Because we decided to implement our directory using Active Server Pages, we were committed to using Microsoft's servers. We've used both IIS and Microsoft's Personal Web Server with good results.

Figure 4 shows the web page that organizes the music. Having the titles organized by artist, having all the artists in alphabetical order, then having links that let you move quickly through the page, all contribute to making it quick and easy to choose your selections. Each album name you see is a hot link to an M3U file. Clicking on it automatically launches your MP3 player and begins playing the CD.

The Script Behind the Page

The web page that generates Figure 4 is not a static HTML page. Nearly all of the HTML for the page is generated when the page is loaded using some simple JavaScript. Listing One, default_js.asp, contains all the code used by this page.

The routine that sets up all the data for this page to display is the function GetFileList(). It uses an instance of the ASP FileSystemObject to create a list of all the M3U files on our Playlist directory. The Server.MapPath() method lets us get the path to the M3U files from the virtual directory name. One thing to notice in the GetFileList() function is the way we loop through the files. Unlike VBScript, where you can iterate through members of a files collection using "For Each Item" in CollectionObject, JavaScript requires you to explicitly create an Enumerator object for the files collection. We then use the object.item() method to set a variable to the current file object in the collection.

These file names are stored in the array fileList[] when the function is called near the top of the page. The list of files is then sorted using JavaScript's array .sort() method. Because the file names start with the artist, this clumps all the files for a given artist together in the list. One note of caution: Because the JavaScript array.sort() method uses ASCII sorting (A-Z before a-z) by default, it's important to have all your file names start with uppercase letters.

After generating the HTML for the start of the page, we create a dynamic list of hot links to each of the possible first letters of the artist names in the collection. (It was there when I thought I should list A-Z). Once the index of single letter links has been written out, the rest of the page is created in a simple loop that iterates over each file in the fileList[] array. For every file in the list, we are going to create a link on the web page that has the name of the CD (with the artist name stripped off) and points to the M3U file. The code that does that is at the bottom of the loop; see Example 1. In this part of the code, the title variable gets the album title portion of the file name, and it is what is displayed. The full path name is used as the actual link, but this is simply our virtual directory name and the file name. The formatting for the whole listing is accomplished using Definition List block elements (<DL>) for each letter, with each artist as a defined term (<DT>) and each title as a definition (<DD>).

We open and close the definition list elements using code executed conditionally at the top of the loop. For each title, we check to see if the artist has changed by comparing lastArtist and Artist. If the artist has changed, we check to see if that artist is the first to start with a new letter. If it is the first, we call doNextLetter(), which closes out the current list (<DL>) of titles, inserts a new anchor, starts the next definition list, and returns. For each new artist, we insert the artist name as a Defined Term (<DT>). If the artist for the current file is the same as lastArtist, then we write just the title as a definition (<DD>).

For More Information

MP3 Tech (http://www.mp3tech.org/) provides a great deal of technical and programming information regarding the MP3 format. MP3.com (http://www.mp3.com/) has links to popular freeware, shareware, and commercial players and track rippers. Also, see CDex 1.2 (beta), a free ripper from ALFA Technologies (http://www.surf .to/cdex/) and RealJukebox Plus from Real Networks (http://www.real.com/).

DDJ

Listing One

<%@ LANGUAGE=JavaScript %>
<%
// Script to generate an alphabetical listing of files with links to 
// each file done in JavaScript to take advantage of the built-in 
// sorting capability for JavaScript arrays

var m3uRoot = "/Playlists";  // Virtual directory for M3U files
var lastArtist = " ";        // used to check for unique artist names
var thisLetter = "";         // current letter for index and listing

var artist;                  // current artist's name from file name
var title;                   // current album title from file name
var href;                    // complete path to M3U file

// get a listing of playlist files in the specified folder
var fileList = GetFileList(m3uRoot);

// sort the file list (in ASCII order, so A-Z is before a-z!)
fileList = fileList.sort();

// get the number of playlist files to show a count in the page title
var fileCount = fileList.length;

// get the first letter of the first file - we'll need this to know
// when we're at the top of the listing
var firstLetter = fileList[0].charAt(0);

// find out which browser is being used to view the page so that we 
// can write the URLs correctly
var browser = Request.ServerVariables("HTTP_USER_AGENT").Item
var isIE = (browser.indexOf("MSIE") != -1)

// - - - start of the HTML document - - - %>
<HTML>
<HEAD>
<META NAME="GENERATOR" CONTENT="Microsoft Visual Studio 6.0">
<!-- using one of the themes that comes with VInterDev 6.0 -->
<LINK REL="stylesheet" TYPE="text/css" HREF="THEME.CSS">
<LINK REL="stylesheet" TYPE="text/css" HREF="GRAPH0.CSS">
<LINK REL="stylesheet" TYPE="text/css" HREF="COLOR0.CSS">
<LINK REL="stylesheet" TYPE="text/css" HREF="CUSTOM.CSS">
<TITLE>The M3U Project</TITLE>
</HEAD>
<BODY>
<H1>The M3U Project</H1>
<H2><%= fileCount %> Titles for Your Enjoyment<BR>
 <SMALL>(listed by artist and albums)</SMALL></H2>
<%
Response.Write("<P><FONT FACE=\"Georgia,Times New Roman,Times\">");
// loop through files to get a list of first characters, and generate 
// links to headings
for (var i = 0; i < fileList.length; i++)
{
  fileName = fileList[i];
  if (fileName.charAt(0) != thisLetter) {
    thisLetter = fileName.charAt(0);
    Response.Write("<A HREF=\"#" + thisLetter + "\">" + thisLetter 
                  + "</A>\n");
  }
}
Response.Write("</FONT></P>\n")

for (i = 0; i < fileList.length; i++)
{
  fileName = fileList[i];
  // find the delimiter separating artist and album title
  idx = fileName.indexOf(" - ");                  
  if (idx != -1)
  {
    // get the artist name with leading and trailing spaces removed
    artist = jTrim(fileName.substring(0, idx));   
    if (artist != lastArtist)
    {
      // for each new artist, check to see if a new first character
      // is present, and start a new section if needed.
      if (artist.charAt(0) != lastArtist.charAt(0))
        doNextLetter(artist.charAt(0));
      Response.Write("<DT><B>" + artist + "</B></DT>\n");
      lastArtist = artist;
    }
    title = fileName.substring(idx + 3, fileName.length - 4);
    title = jTrim(title);
    
    href = m3uRoot + "/" + fileName;
    if (!isIE) jReplace(fileName, " ", "%20", 1, -1, 1);
    Response.Write("<DD><A HREF=\"" + href + "\">" + title 
                   + "</A></DD>\n");
  }
}
%>
</DL><A HREF="default.asp#top">back to top</A></BLOCKQUOTE>
</BODY>
</HTML>
<SCRIPT LANGUAGE="javascript" RUNAT="server">
//
// function doNextLetter - closes the current list and starts the
// next list with the letter heading as an anchor
function doNextLetter(nextLetter)
{
  var c = nextLetter.toUpperCase();
  var lc = (c.toLowerCase() == c) ? "" : c.toLowerCase();
    
  // close the previous section if not at top of page
  if (nextLetter != firstLetter)
    Response.Write("</DL><A HREF=\"#top\">back to top</A>" 
                   + "</BLOCKQUOTE>\n");
  // start the next section
  Response.Write("<H3><A NAME=\"" + c + "\">" + c + lc + "</A></H3>\n"
                 + "<BLOCKQUOTE><DL>\n");
}
function GetFileList(vRoot)
{
  var folderName = Server.MapPath(vRoot);
  var avarFileList = new Array();
  var fileCount = 0;
  var f;
  
  // Use ASP's FileSystemObject to get list of files in the playlist folder
  var objFS = Server.CreateObject("Scripting.FileSystemObject");
  var objSourceFolder = objFS.GetFolder(folderName);

  // since we want to acces the Files collection from the folder
  // object, we use the JScript Enumerator object to hold the files
  var objFiles = new Enumerator(objSourceFolder.Files);   

  // Using the files collection, create an array for list of playlist files.
  for (; !objFiles.atEnd(); objFiles.moveNext())
  {
    // Since we need the format of the playlist file name to have the 
    // artist and album title separated with a space-dash-space 
    // combination, we'll use that to filter out unwanted files 
    // (in addition to looking for the .m3u extension)
    
    f = objFiles.item();  // gets the current file

    if ((f.name.indexOf(" - ") != -1) && 
        (f.name.lastIndexOf(".m3u") == f.name.length - 4))
    {
      // put the file name in the list (array)
      avarFileList[fileCount] = f.name;
      fileCount++;
    }
  }
  return avarFileList;
}
</SCRIPT>
<SCRIPT LANGUAGE="vbscript" RUNAT="server">
'// It's easier to do a Trim in VBScript, so just create a wrapper for
'// calling it in JavaScript
Function jTrim(strTemp)
  If strTemp = "" Or strTemp = Null Then
    jTrim = ""
  Else
    jTrim = Trim(strTemp)
  End If

End Function

'// It's easier to do the replace with VB than working with JScript's regular
'// expressions, so again, use VBScript to create a wrapper for Replace method
Function jReplace(sSource, sFind, sReplace, iStart, iCount, iCompare)

jReplace = Replace(sSource, sFind, sReplace, iStart, iCount, iCompare)

End Function

</SCRIPT>

Back to Article