DDJ Cover from October, 2002 Dr. Dobb's Journal October, 2002

Source code and install program for Louis, Louis

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

Mental telepathy is still a bit out of reach, but in this article I'm going to show you how to build 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 can apply a bias towards the music you like and away from the music you don't.

So that you can build an MP3 Player like this, I'll first describe the steps you need to embed Microsoft's Windows Media Player (WMP) in a Visual C++ program as an ActiveX control, and what you need to do to control it, and respond to events from it.

With that infrastructure in place, I'll then show you how to keep a music database and use the ratings it contains to select the music you like.

The Demo Program: Louis, Louis

My demo program is named Louis, Louis, in honor of Louis Glass, a man who is credited by many with being the inventor of the modern Juke Box. In 1889 Louis introduced the first coin-operated phonograph at the Palais Royal saloon in San Francisco.

Figure One shows Louis, Louis in action. The main window of the program is divided into three parts. At the top, there is information about the track currently being played, which includes the Artist, Album, and Song name, along with your current ratings for each.

Figure1.gif
Figure 1
Louis, Louis at Work
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 a set of player controls. This entire section is part of a single monolithic instance of the WMP OCX.

The bottom section of the dialog contains buttons that are used for the various functions I've added to the program.

A Few Details

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

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 will work its way sequentially through all your music. You can navigate through your playlist by clicking the Playlist button on the main screen. That brings up a tree view of all your music, as shown in Figure 2.

Figure 2
Figure 2
The Tree View of the Master Playlist
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 simply rescan every time you add new tracks. A scan in progress is shown in Figure 3.

Figure 3
Figure 3
Scanning for New Songs
After you scan your hard drive for files and start using Louis, Louis, you can start entering ratings for Artists, Albums, and Songs. All it takes to do that is a quick movement of the appopriate slider control and you're done! Every time you enter in a new rating, you'll find that 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 many avenues you can take. Just as an example, the folks at javazoom.net have developed a free MP3 player component written in pure Java, suitable for developing an app that can play on multiple platforms. There are many other freeware MP3 players that you can modify to suit your needs.

When I set out on the Louis, Louis project I was hoping to avoid the work of completely implementing an MP3 player from scratch. After a little research, I saw that both WinAmp (from Nullsoft at winamp.com) and Microsoft's Windows Media Player had SDKs that would allow me to control the player while taking advantage of the existing user interface, playlist features, and so on.

Both of these programs were good candidates for a personal jukebox, but in the end I chose the Microsoft solution. The fact that I could embed WMP as a control in my program gave me a somewhat cleaner solution than I would have controlling Winamp as an external program. Just as importantly, I found out that WMP has an additional bonus: 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

Microsoft's SDK for Windows Media Player can be found by navigationg to their developer support site. From the Downloads link you can navigate down through Graphics and Multimedia to Windows Media Technologies and finally download the Windows Media Player 7.1 SDK. When you download and install the SDK you will have to agree to an End User License Agreement which will describe the terms under which you can use WMP. For PC based MP3 player 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 a set of documentation. The help pages describing the WMP object model are essential, including all the functions and events needed to develop an app.

Although the SDK ships with a decent amount of sample code, C++ programmers should prepare for disappointment. There is a grand total of one C++ example, which is basically an API exerciser. It might help you perform a few experiments, but there is no way the sample is going to show you 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 allows you to do this without writing too much code, but you will 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 Window-based application, so I had to wing it. My initial app was created following a fairly simple recipe from Andrew Whitechapel on the invaluable CodeGuru site. I made a couple of modifications needed to change my main Window class to be 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.

ATL has a Window class called CAxWindow that is specifically designed to contain ActiveX controls, so the first thing I did was add a member of this class to CLouisDialog, then create the window as a child in CLouisDialog::OnInitDialog(). Once the window is created, it's a simple step to create an instance of the Windows Medial Player OCX in that window. The code extracted from OnInitDialog() is shown below:

C++:
  1. m_PlayerHostWindow.Create( m_hWnd,
  2. rect,
  3. "LouisPlayerHost",
  4. WS_CHILD | WS_VISIBLE |
  5. WS_CLIPCHILDREN,
  6. WS_EX_CLIENTEDGE );
  7. CComPtr pHost;
  8. HRESULT hr = m_PlayerHostWindow.QueryHost( &pHost );
  9. char *guid = "{6BF52A52-394A-11d3-B153-00C04F79FAA6}";
  10. hr = pHost->CreateControl( CComBSTR(guid), m_PlayerHostWindow, 0);

Working With the Control

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

C++:
  1. #import "WMP.OCX"

I then added a set of member variables to class CLouisDialog:

C++:
  1. protected:
  2. CComPtr m_Player;
  3. CComPtr m_Playlist;
  4. CComPtr m_Controls;
  5. CComPtr m_MediaCollection;

These four COM objects represent the interfaces by which I call methods on the various components of the player. They are initialized in CLouisDialog::OnInitiDialog() right after the WMP OCX is created:

C++:
  1. hr = m_PlayerHostWindow.QueryControl( &m_Player );
  2. hr = m_Player->get_currentPlaylist( &m_Playlist );
  3. hr = m_Player->get_controls( &m_Controls );
  4. hr = m_Player->get_mediaCollection( &m_MediaCollection );

With these objects in place I now have the ability to insert and remove songs into playlist, skip from one song to the next, and so on. All I need now is the ability to receive events.

Receiving Events

To receive events from the OCX, I need to create a separate event sink class. The ATL has a template class called IDispEventImpl that is designed to do just this. Following Microsoft's cookbook for this class, I created an event sink that had support for the only event I need, the CurrentItemChange event. The resulting class definition looks like this:

C++:
  1. class CLouisEventSink
  2. : public IDispEventImpl<1,
  3. CLouisEventSink,
  4. &DIID__WMPOCXEvents,
  5. &LIBID_WMPOCX,
  6. 1,
  7. 0>
  8. {
  9. public:
  10. CLouisEventSink( CLouisDialog &frame );
  11. virtual ~CLouisEventSink();
  12.  
  13. CLouisDialog &m_Frame;
  14.  
  15. BEGIN_SINK_MAP(CLouisEventSink)
  16. SINK_ENTRY_EX( 1, DIID__WMPOCXEvents, 0x16ae, CurrentItemChange )
  17. END_SINK_MAP()
  18. void _stdcall CurrentItemChange(IDispatch * pdispMedia );
  19. };

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. Note that 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 actually connect to the event mechanism, I create a CLouisEventSink member in CLouisDialog, then connect it to the OCX in the InitDialog() function:

C++:
  1. 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, the other major component needed for full functionality is the database. For this program, I created a simple Access database with three tables: Tracks, Albums, and Artists. Each contains the name and rating for the specific items. The Tracks table also contains a file name that denotes the actual location of the specfic song.

I created a single query that 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 nearly trivial when using OLE DB access as provided through ATL. For each of the three tables and the single query I simply used the ATL Object Wizard to create a consumer class that gives me full access to that object.

I had some concern about the speed with which the program can 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 8,000 songs seems to take only a second or two, which is acceptable. However, users with significantly slower machines or significantly larger music libraries might have to 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. This is cookie-cutter code and needs no elaboration.

Finding the file is the simple part - but once you have the file, you have to determine the title of the song, the album, and artist. Readers who remember my DDJ article of January 2000, The Ultimate Home Jukebox, know 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.

But 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. A nice explanation of how this works can be found at http://www.id3.org, the ID3v2 web site.

Rather than writing my own ID3 decoder class, I went to one of my favorite programming web sites, The Code Project, and found the CMP3Info class by Roman Nurik. I dropped it into my project and it worked perfectly with no muss, no fuss!

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 pretty good to me.

Overall Operations

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 insures that that user 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 we 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 a Quirk

To build and work with Louis, Louis, you will need to have Visual C++ 6, and it's probably a good idea to bring it up to Service Pack 5. In addition, you'll 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.

One additional point that you'll need to be aware of before using Louis, Louis. 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, you will need to start Windows Media Player, select the Tools|Options menu item, switch to the Media Library tab, and select Full Access for Internet Site rights.

In summary, creating Louis, Louis has been a lot of fun. To Microsoft's credit, Windows Media Player gives me 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 pretty happy with. If only all programming assignments could follow this path!

© 2002 Mark Nelson