Channels ▼
RSS

Database

An Intelligent MP3 Jukebox

Source Code Accompanies This Article. Download It Now.


Oct02: An Intelligent MP3 Jukebox

Mark works on IP Telephony for Cisco Systems and is a DDJ contributing editor. He can be reached at markn@ieee.org.


Wouldn't it be nice to have a radio station that always played your favorite music? An intelligent jukebox that could read your mind, mixing in your old favorites with new music you really like?

While mental telepathy is still a bit out of reach, I'll present in this article a Windows-based MP3 player that keeps track of your musical tastes and does its best to accommodate them. By giving you the opportunity to rate artists, albums, and individual songs, the MP3 player applies a bias towards the music you like and away from the music you don't.

Louis, Louis

My program is named Louis, Louis in honor of Louis Glass, who was credited with inventing the modern jukebox when, in 1889, he introduced the first coin-operated phonograph at the Palais Royal saloon in San Francisco. (The complete source code for Louis, Louis is available electronically; see "Resource Center," page 5.)

Figure 1 is Louis, Louis in action. The main window of the program is divided into three parts. At the top is information about the track currently being played, including the Artist, Album, and Song, along with your current ratings for each. The center section of the dialog contains the embedded copy of Windows Media Player (WMP). This includes an area for the display of the current WMP visualization, a caption area for information about what's playing, and player controls. This entire section is part of a single monolithic instance of the WMP OCX. The bottom section of the dialog contains buttons used for various functions I've added to the program.

Louis, Louis is an MP3 player that has two different modes of operation. Random Play mode is enabled by checking the box of the same name in the top section of the Dialog. In this mode, the program randomly selects tracks from the master catalog. As it selects each song, it determines how likely you are to want to hear it. The selection algorithm first checks to see if you've rated the song. If you haven't, it then checks to see if you've rated the album, and finally the artist. Your chances of hearing the song range from 0 to 100 percent, depending on your rating on a score of 0 to 10. If you haven't applied any rating at all, the probability is fixed at 25 percent.

If you uncheck the Random Play box, Louis, Louis operates more or less like a conventional MP3 player. You can set it to start playing tracks anywhere in your playlist, and it works its way sequentially through all your music. You can navigate through the playlist by clicking the Playlist button on the main screen, which brings up a tree view of all your music; see Figure 2.

You can add tracks to the database at any time by clicking the Add Tracks button on the main window. It brings up a dialog that lets you select a folder and scan it for new material. If you keep all your music in the same folder, you can rescan every time you add new tracks. Figure 3 is a scan in progress.

After scanning the hard drive for files and starting Louis, Louis, you can start entering ratings for Artists, Albums, and Songs. All this takes is a quick movement of the appropriate slider control and you're done. Every time you enter a new rating, Louis, Louis becomes better and better at playing the music you want to hear.

Building an MP3 Player

If you want to build your own MP3 player, there are several options. For example, the folks at http://www.javazoom.net/ have developed a free MP3 player component written in pure Java, suitable for developing apps that can play on multiple platforms. There are many other freeware MP3 players you can modify to suit your needs.

At the outset, I was hoping to avoid completely implementing an MP3 player from scratch. After a little research, I decided Nullsoft's WinAmp (http://www.winamp.com/) and Microsoft's Windows Media Player (http://msdn.microsoft.com/) had SDKs that would let me control the player, while still taking advantage of the existing UI, playlist features, and so on. Both programs were good candidates for a personal jukebox, but in the end, I chose Microsoft's WMP because I could embed WMP as a control in my program, giving me a somewhat cleaner solution than controlling WinAmp as an external program. Just as importantly, Microsoft's license to use MP3 technology is passed on to users of the WMP SDK. Thus, if Louis, Louis were ever to be distributed, the headache of acquiring a license to use MP3-patented technology would be avoided.

Embedding WMP in Louis, Louis

You can find Microsoft's SDK for Windows Media Player by going to http://msdn .microsoft.com/. From the Downloads link, navigate through Graphics and Multimedia to Windows Media Technologies and download the Windows Media Player 7.1 SDK. When you download and install the SDK, you have to agree to an End User License Agreement that describes the terms for using WMP. For PC-based MP3 players, you don't have to worry about royalties or other payments; you can even distribute the WMP components as long as you follow the rules.

Besides the license, the most important thing you get from the SDK is documentation. The help pages describing the WMP object model are essential, including all the functions and events needed to develop apps.

Although the SDK ships with a decent amount of sample code, C++ programmers should prepare for disappointment. There is only one C++ example, which is basically an API exerciser. It might help to perform a few experiments, but there is no way the sample is going to show how to write an application like Louis, Louis.

Since the single C++ example was an ATL-based app, I decided to create Louis, Louis as a dialog-based ATL application, embedding the WMP OCX as a control in the dialog. ATL lets you do this without writing too much code, but you have to do a substantial amount of work without the benefit of the wizard-driven coding used for MFC development.

The ATL App

Microsoft's AppWizard doesn't have an option for creating a simple ATL Windows application, so I had to wing it. I created the initial app following a straightforward recipe from Andrew Whitechapel at CodeGuru (http://www.codeguru.com/atl/ atlWindows.html). I made a couple of modifications required to make my main window class dialog based, and had an app that was ready to go.

At this point, I had a main window class, CLouisDialog, and was ready to start working with the WMP OCX. The first task was to create an instance of the OCX and embed it in the dialog.

Since ATL defines the window class CAxWindow, which is specifically designed to contain ActiveX controls, the first thing I did was add a member of this class to CLouisDialog, then create the window as a child in CLouisDialog::OnInitiDialog(). Once the window is created, it's straightforward to create an instance of the WMP OCX in that window. Listing One is code extracted from OnInitDialog().

Working with the Control

After instantiating the WMP control, you should next set up the program elements to call its methods and respond to its events. The first step is to use Visual C++'s import facility to pull in a complete definition of the COM objects in the OCX. This is done by adding a single line of code to STDAFX.H:

#import "WMP.OCX"

I then added a set of member variables to class CLouisDialog; see Listing Two. These four COM objects represent the interfaces by which you call methods on the various components of the player. They are initialized in CLouisDialog::OnInitiDialog() right after the WMP OCX is created (Listing Three). With these objects in place, you have the ability to insert songs into and remove songs from the playlist, skip from one song to the next, and so on. All you need now is the ability to receive events.

Receiving Events

To receive events from the OCX, you need to create a separate event sink class. The ATL has a template class called IDispEventImpl designed to do this. Following Microsoft's cookbook code for this class, create an event sink that provides support for the only event you need, the CurrentItemChange event. Listing Four is the resulting class definition. Adding new events to this map is simply a matter of creating a new entry in the header file sink map, then adding a new member function to handle the event. The dispid that uniquely identifies the event (0x16ae in this case) and the function prototype for the event handler are found in the WMP.TLI file created when processing the #import statement.

To connect to the event mechanism, create a CLouisEventSink member in CLouisDialog, then connect it to the OCX in the InitDialog() function:

hr = m_EventSink.DispEventAdvise( m_Player );

A corresponding Unadvise function call has to be made when the program is shutting down.

Database

With the control in place, I next used Microsoft Access to create a simple database with tables for Tracks, Albums, and Artists, each containing the name and rating for the specific items. The Tracks table also contains a file name denoting the location of the specific song.

A single query groups all of this data into a single sorted master playlist; this is what the program uses to decide what to play during normal operations. The three primary tables are only accessed directly when new tracks are being added.

Communicating with this database is via OLE DB access as provided through ATL. For each of the three tables and the single query, I used the ATL Object Wizard to create a consumer class that gives me full access to that database object.

I had some concern about the speed with which the program could access items in the database. At program startup, the single query is executed and all of its information is loaded into memory.

On my development system, processing around 8000 songs seems to take only a second or two, which is acceptable. However, if you have significantly slower machines or significantly larger music libraries, you might rethink this strategy.

Adding Tracks

Adding tracks to the database is a two-step operation. As Figure 3 shows, the first step is to identify a root folder, then execute a file search for files with the MP3 extension.

Finding the file is the simple part. However, once you have the file, you have to determine the title of the song, album, and artist. Recall from my article "The Ultimate Home Jukebox" (DDJ, January 2000) that I keep my personal music organized in a directory structure that automatically gives you the artist, album, title, and track number of any given file. However, it would be unrealistic to expect everyone to follow this convention, particularly when a better solution is available—ID3 tags. The ID3 tag is a conventional way of embedding information about a track directly in the MP3 file itself. For more information, see http://www.id3.org/.

Rather than writing my own ID3 decoder class, I went to The Code Project (http://www.codeproject.com/) where I found the CMP3Info class by Roman Nurik. I dropped it into my project and it worked perfectly.

As nice as Roman's class is, I still elected to perform a bit of surgery on it. Roman was getting some detailed information about the encoding of each block in the MP3 file, which took a considerable amount of time. I cut that code out, limiting the class to just the ID3 info I cared about.

The resulting code seems pretty efficient. On my sub-GHz development computer, I can scan my entire library of 8300 tracks; create track, artist, and album records for each; and update the database in under two minutes, or roughly 75 tracks per second. Given that you will do updates of this magnitude infrequently, this seems acceptable.

Overall Operation

The actual operation of Louis, Louis is essentially identical whether in random shuffle or normal jukebox mode. In both cases, tracks are selected from the master playlist, which is sorted by artist, then album, then track number. In random play mode, the list undergoes a single shuffle operation, and as songs are selected, they may be skipped if they fail the internal coin toss.

The WMP component is fed a playlist of five songs, allowing for a current song being played, two previous songs, and two upcoming songs. This ensures that users can use the skip buttons on WMP to immediately go ahead to the next song or back up to listen to one that was previously heard.

Each time a song completes, a CurrentItemChange event is sent to the event sink. Each time you move forward, Louis, Louis adds a new song to the playlist on the upcoming end, and removes one from the already-been-played side. This virtual playlist makes random play mode easy to implement.

Requirements and Quirks

To build and work with Louis, Louis, you need Visual C++ 6, and it's probably a good idea to bring it up to Service Pack 5. In addition, you need to install Windows Media Player 7.1. Odds are you'll want to install the Windows Media Player SDK as well, if only for the help file. Also, by default, Windows Media Player will not allow external apps to insert tracks into its playlist, which means programs like Louis, Louis are disabled by default.

To enable external control of the playlist, start Windows Media Player, select the Tools|Options menu item, switch to the Media Library tab, and select Full Access for Internet site rights.

Conclusion

Creating Louis, Louis has been a lot of fun. To Microsoft's credit, Windows Media Player gives you a powerful tool to make this happen, in combination with ATL for the UI and OLE DB for database access. I started this project with little knowledge of all three topics, and was able to fumble my way through to a finished product that I'm happy with. If only all programming assignments could follow this path!

DDJ

Listing One

m_PlayerHostWindow.Create( m_hWnd, rect, "LouisPlayerHost", WS_CHILD | 
                          WS_VISIBLE | WS_CLIPCHILDREN, WS_EX_CLIENTEDGE );
CComPtr<IAxWinHostWindow> pHost;
HRESULT hr = m_PlayerHostWindow.QueryHost( &pHost );
char *guid = "{6BF52A52-394A-11d3-B153-00C04F79FAA6}";
hr = pHost->CreateControl( CComBSTR(guid), m_PlayerHostWindow, 0);

Back to Article

Listing Two

protected:
    CComPtr<IWMPPlayer> m_Player;
    CComPtr<IWMPPlaylist> m_Playlist;
    CComPtr<IWMPControls> m_Controls;
    CComPtr<IWMPMediaCollection> m_MediaCollection;

Back to Article

Listing Three

hr = m_PlayerHostWindow.QueryControl( &m_Player );
hr = m_Player->get_currentPlaylist( &m_Playlist );
hr = m_Player->get_controls( &m_Controls );
hr = m_Player->get_mediaCollection( &m_MediaCollection );

Back to Article

Listing Four

class CLouisEventSink 
  : public IDispEventImpl<1, CLouisEventSink, &DIID__WMPOCXEvents, 
                                              &LIBID_WMPOCX, 1, 0 >
{
public:
  CLouisEventSink( CLouisDialog &frame );
  virtual ~CLouisEventSink();
  CLouisDialog &m_Frame;
  BEGIN_SINK_MAP(CLouisEventSink)
    SINK_ENTRY_EX( 1, DIID__WMPOCXEvents, 0x16ae, CurrentItemChange )
  END_SINK_MAP()
    void _stdcall CurrentItemChange(IDispatch * pdispMedia );
};

Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video