Chapter 3. Back then, I told you that MCI stood for Media Control Interface, and that the API was designed for multimedia devices, like CD players. The MCI is the easiest way to use CDs. There are ways to talk to them on a lower level, but unless you're writing audio extraction software, you won't need them.Figure 9.1 illustrates the process of using MCI: you see what devices are available, pick one and open it, bark commands at it, and close it when you're done. You can interface via API functions, or you can actually format a text command string and send that. For example, the command string "play thissound from 0 to 2000 notify" tells the system to play the "thissound" from its beginning to position 2000, and to notify you when it's done. If you forget the syntax of the command strings, just pretend you're a drill sergeant speaking to a CD player.

Figure 9.1: The process for using the Media Control Interface (MCI).
The MCI library is a powerful tool, capable of doing everything most games will require from a CD player. For more information on the API as a whole, check out the MSDN documentation, at Platform SDK\Multimedia Audio\MCI.MCI is a dual-interface API—you can use freeform text commands (the command string interface) or a set of message IDs and structures (the command message interface) to communicate with it. I've chosen to ignore the free-form string interface, and do everything through the message interface.The Mother Brain of the message interface is the mciSendCommand API call. This function takes a command message ID (for example, MCI_PLAY), along with a structure for that message (for example, MCI_PLAY_PARMS). It's a little scary at first, because you have to cast the address of your structures to a DWORD as you pass them to the function:
MCI_PLAY_PARMS mciPlayParms;
ZeroMemory(&mciPlayParms, sizeof(MCI_PLAY_PARMS));
mciPlayParms.dwFrom = MCI_MAKE_TMSF(bTrack, 0, 0, 0);
mciPlayParms.dwTo = MCI_MAKE_TMSF(bTrack + 1, 0, 0, 0);
mciPlayParms.dwCallback = (DWORD) hWndNotify;
MCIERROR e = mciSendCommand(wDeviceID, MCI_PLAY,
MCI_FROM | MCI_TO | MCI_NOTIFY, (DWORD)(LPVOID) &mciPlayParms);
This is a typical chunk of code taken from the CD player class's Play method. You can see the code setting all of the different members of the MCI_PLAY_PARMS structure, the one required by the MCI_PLAY command. The code specifies the play range via the dwFrom and dwTo members, throws in a window handle for a callback, and then calls mciSendCommand to fire off the command. The second parameter to mciSendCommand is the message ID, and the third parameter is the set of flags for the message.Don't worry about the details yet—I just want you to become familiar with this pattern because you'll see it throughout the CD player class. If mciSendCommand succeeds, it returns zero; otherwise, it returns an MCI error code which you can decode into a text string via the mciGetErrorString function.
Enumerating, Opening, and Closing Devices
The first order of business is to figure out what kind of hardware your gamer has. There was a time when this was easy because most PCs had zero or one drives. Thank the proliferation of CDRW drives and USB for making this complex—nowadays a user can theoretically have dozens of peripherals from which a CD could be played. Your game disc is in one of them—how do you figure out which one?It's at this point I need to mention that sometimes, the way "They" want you to do things is needlessly complex. For example, if you were to start browsing through the MSDN documentation on the MCI API, you'd quickly discover that the proper way to enumerate devices is to send the MCI_SYSINFO command once with certain flags, to get the number of MCI devices, and then several more times (with different flags) to get each individual device.But unless you're doing something above and beyond just simple music playback, you probably don't need to do that. Instead, you can use an easier method.Windows gives each CD audio-capable peripheral (CD player, DVD player, CD burner, and so on) a drive letter between D and Z (remember, drives A and B are usually floppies and drive C is usually a hard drive). Therefore, all you need to do is loop through drive letters D through Z, and see if a CD audio device is connected to that drive. If one is, you can strike up a conversation with that drive to determine what CD (if any) is in it. This is easier than doing proper MCI enumeration, and it also tells you the drive letter of your game CD (in case you want to play movies from it or something).Here's the CCDPlayer::GetAvailableDevices method. This method looks at all the devices on the system and returns a vector of device information classes—one device information class for each CD audio drive it finds:
vector<CCDDeviceInfo> CCDPlayer::GetAvailableDevices() {
DWORD retvalue=0;
MCIERROR e;
vector<CCDDeviceInfo> devices;
// loop through entire alphabet of drive letters
for (char drive = 'C'; drive <= 'Z'; drive++) {
char buf[256];
char driveletter[3] = "C:";
// see if we can open this drive as a CD audio device
MCI_OPEN_PARMS openparms;
ZeroMemory(&openparms, sizeof(MCI_OPEN_PARMS));
openparms.lpstrDeviceType = (LPCSTR) MCI_DEVTYPE_CD_AUDIO;
driveletter[0] = drive;
openparms.lpstrElementName = driveletter;
e = mciSendCommand(NULL, MCI_OPEN,
MCI_OPEN_TYPE | MCI_OPEN_TYPE_ID | MCI_OPEN_ELEMENT,
(DWORD)(LPVOID)&openparms);
if (!e) {
// success! Query this CD Audio drive and add it to
// our vector.
CCDDeviceInfo devinfo;
devinfo.m_DriveLetter = drive;
int deviceid = openparms.wDeviceID;
// removed from this listing to save space:
// ask device for its description
// ask device for the product code of the CD in it
// ask device for the ID of the CD in it
// close device
MCI_GENERIC_PARMS genericparms;
ZeroMemory(&genericparms, sizeof(MCI_GENERIC_PARMS));
mciSendCommand(deviceid, MCI_CLOSE, MCI_WAIT,
(DWORD)(LPVOID)&genericparms);
// add it to the vector
devices.push_back(devinfo);
}
} // next drive letter
return(devices);
}
I've stripped out the repetitive guts of this GetAvailableDevices function so you can more easily see the overall structure.Essentially, the code runs through the alphabet, trying to open each drive letter as a CD audio drive. Of key importance is MCI_OPEN_ELEMENT, which triggers the MCI to look at the lpstrElementName member of the MCI_OPEN_PARMS struct. In this context, lpstrElementName is actually the drive letter of the device.If the drive letter given is a CD audio device, the code will receive a success return code (zero) from the MCI_OPEN message sent with mciSendCommand. This tells the code that the drive letter refers to a device capable of playing an audio CD. The code then sends a few MCI_INFO messages, asking the drive for its description, as well as the product code and ID of the CD in its drive (if any). I've left that piece of code out of the preceding listing, but it's pretty straightforward.
Tip | A CD's universal product code (UPC) is cut onto the CD by the manufacturer, and is guaranteed unique in the entire universe. It's sort of like the CD equivalent of the Zebra bar codes on your favorite items at the grocery store. For example, you can use the CD's UPC to query a CDDB (CD Database) on the Internet for the title, artist, and track names of the CD. Beware, however: some older CDs don't have a product code.Unlike the CD product code, the CD ID code is a makeshift identifier, calculated by Windows based on the track lengths and other attributes of the CD. Windows can generate an ID for any CD, but it's possible (though unlikely) that two different CDs will have the same CDID. |
Once the code has finished querying the device, it closes it by sending it a MCI_CLOSE command. Notice the MCI_WAIT flag. MCI commands are by default asynchronous; specifying MCI_WAIT tells MCI to not return until the device has processed the message. The code plays it safe by waiting for the close command to complete before adding the device info class to the vector, and moving on to the next drive letter.This static function returns a vector of CCDDeviceInfo classes to the caller. Once the caller has the list of valid drive letters, it picks one, and calls CCDPlayer's Init method. I haven't included the Init source code here because it's straightforward. Init opens the device corresponding to the supplied drive letter, and stashes its device ID into m_DeviceID. This device ID is like a file handle; the code receives it in response to the open message, and uses it to specify the target for more interesting messages, like MCI_PLAY. This is how, in a multiple CD audio drive system, you can get the right disc to spin up.Once the device is open, the Init method sends an MCI_SET message to set the time format to something called TMSF, which stands for track, minute, second, frame—four identifiers which you use together to pinpoint an exact location on an audio CD. There are other time formats (for example, plain old milliseconds), but TMSF is your best choice for situations where you're mainly playing entire tracks—you'll see why when you learn about playing tracks.Finally, the Init method sets the m_InitGood flag to true. Yep, you guessed it—two-phase construction again. This flag is the "seal of approval" that the CD player is ready; all of the other methods of CCDPlayer check this flag before attempting to do anything else.When you're all done using the CD player class, call its UnInit method, which closes the device by sending an MCI_CLOSE message.
MCI Time Formats
Time to take a quick detour and talk about CD time formats. I've written a helper class called CTrackMinSecFrame which contains integers for track, minute, second, and frame. However, the MCI functions pack all four of these values into a single DWORD, one byte for each value. MCI supplies macros to pack and unpack a TMSF DWORD; you can use these macros to create methods that convert between CTrackMinSecFrame classes and DWORDs:
class CTrackMinSecFrame
{
public:
// snip
CTrackMinSecFrame &operator=(DWORD dw) {
m_Track = MCI_TMSF_TRACK(dw);
m_Minute = MCI_TMSF_MINUTE(dw);
m_Second = MCI_TMSF_SECOND(dw);
m_Frame = MCI_TMSF_FRAME(dw);
return(*this);
}
operator DWORD() { return(MCI_MAKE_TMSF(m_Track, m_Minute, m_Second, m_Frame)); }
};
Here are two nifty operator overloads. The first, operator=, uses four MCI macros to extract the individual track, minute, second, and frame bytes, and put them in their new integer homes. The second method, operator DWORD, uses the MCI_MAKE_TMSF to pack them back up.These operators are cool because together they allow us to treat a CTrackMinSecFrame as if it were a DWORD—anywhere! The C++ lesson here is that when used to automate banal-type conversions, operator overloading can make your life easier.
Playing a Track
The bad news is that you can't just tell the CD player to "play track 8." You have to give it start and end times. The good news is that because you're using the TMSF format, this is pretty easy:
void CCDPlayer::Play(int track)
{
if (!m_InitGood) return;
m_Paused = false;
MCIERROR e;
MCI_PLAY_PARMS mciPlayParms;
mciPlayParms.dwFrom = 0L;
mciPlayParms.dwTo = 0L;
mciPlayParms.dwFrom = MCI_MAKE_TMSF(track, 0, 0, 0);
mciPlayParms.dwTo = MCI_MAKE_TMSF(track + 1, 0, 0, 0);
mciPlayParms.dwCallback = (DWORD)m_hWnd;
e = mciSendCommand(m_DeviceID, MCI_PLAY,
MCI_FROM | MCI_TO | MCI_NOTIFY,
(DWORD)(LPVOID) &mciPlayParms);
if (e) HandleError(e);
}
By using TMSF, the code is able to easily specify the playback range—it's from the start of the track we want (track, 0, 0, 0), up to but not including the start of the next track (track+1, 0, 0, 0). TMSF is beautiful here—without it, you'd have to calculate the millisecond range you needed.Also important in the preceding code is the MCI_NOTIFY flag. You'll learn about that in the Synchronous versus Asynchronous Commands section, in a few pages.
Stopping
To stop a CD from playing, send it an MCI_STOP message:
void CCDPlayer::Stop()
{
m_Paused = false;
if (!m_InitGood) return;
MCI_GENERIC_PARMS genericparms;
ZeroMemory(&genericparms, sizeof(MCI_GENERIC_PARMS));
mciSendCommand(m_DeviceID, MCI_STOP, MCI_WAIT, (DWORD)(LPVOID)&genericparms);
}
The messages that take only generic parameters are the easiest. Here, the code sends a MCI_STOP command and supplies a generic parameter structure.Notice the MCI_WAIT flag, which ensures that the CD player is stopped by the time the Stop method returns.
Pausing and Resuming
Notice that the first thing the Play method does is set the m_Paused flag to false. It may surprise you to learn that MCI doesn't support pause and resume for all CD players. To make a rock-solid CD player class, you'll have to implement pause and resume yourself.Fortunately, this isn't difficult—to pause, all you have to do is query the current play position, remember it, then send a stop command. To resume, just start playing from where you paused until the end of the track. Add a flag to remember whether or not the CD player is paused, and you're all set.Implementing this form of "pseudo-pausing" requires a couple of additional member variables. First, there's m_Paused, the Boolean flag. Next, there's m_PausedPos, a CTrackMinSecFrame that stores the exact instant the CD was paused.Here's how it looks in code:
void CCDPlayer::Pause()
{
if (!m_InitGood) return;
m_PausedPos = GetCurrentPos();
Stop();
m_Paused = true;
}
void CCDPlayer::Resume()
{
if (!m_InitGood) return;
if (!m_Paused) return;
// play from pause position to end of track
MCIERROR e;
MCI_PLAY_PARMS mciPlayParms;
mciPlayParms.dwFrom = 0L;
mciPlayParms.dwTo = 0L;
mciPlayParms.dwFrom = m_PausedPos;
mciPlayParms.dwTo = MCI_MAKE_TMSF(m_PausedPos.m_Track + 1, 0, 0, 0);
mciPlayParms.dwCallback = (DWORD)m_hWnd;
e = mciSendCommand(m_DeviceID, MCI_PLAY,
MCI_FROM | MCI_TO | MCI_NOTIFY,
(DWORD)(LPVOID) &mciPlayParms);
if (e) HandleError(e);
m_Paused = false;
}
The Pause method is easy (you'll learn about GetCurrentPos in the next section), and the only thing tricky about the Resume method is the play range. The code sets up the MCI_PLAY_PARMS structure so that the CD player starts up where it left off (m_PausedPos), and then continues up to the start of the next track (m_PausedPos.m_Track+1, 0, 0, 0). Finally it sets the m_Paused flag to false, completing the illusion of pause.
Tip | The CD player's Play method isn't terribly intelligent. Play doesn't automatically check the m_Paused flag; it unconditionally starts playing from the beginning of a track. The CD Player class relies on other code to keep track of the pause state and either call Play or Resume, as appropriate. |
You'll see a couple of other methods that set the paused flag to false. For example, the method that ejects the CD also sets m_Paused to false.
Determining Play Position
To ask a CD player what it's currently playing, send a MCI_STATUS message. MCI_STATUS lets you do a whole bunch of useful status queries—look at all of the flags in MSDN—but to get the current play position, you need to pass in MCI_STATUS_POSITION as the dwItem in the status parameters structure, MCI_STATUS_PARMS.Here's the code:
CTrackMinSecFrame CCDPlayer::GetCurrentPos()
{
CTrackMinSecFrame pos;
if (!m_InitGood) return(pos);
if (m_Paused) return(m_PausedPos);
if (!IsPlaying()) return(pos);
MCI_STATUS_PARMS statusparms;
ZeroMemory(&statusparms, sizeof(MCI_STATUS_PARMS));
statusparms.dwItem = MCI_STATUS_POSITION;
MCIERROR e = mciSendCommand(m_DeviceID,MCI_STATUS,MCI_STATUS_ITEM
| MCI_WAIT,
(DWORD)(LPVOID)&statusparms);
if (e) HandleError(e);
pos = statusparms.dwReturn;
return(pos);
}
Sometimes, knowing where to put an MCI flag is difficult. In the case of status messages, I've found the most straightforward approach is to specify the MCI_STATUS_ITEM flag, which tells MCI to look at the dwItem member of the MCI_STATUS_PARMS structure to figure out which status item you want. MCI then puts the information you've requested into the dwReturn member, which the preceding code looks at, converts into a CTrackMinSecFrame (there's that nifty assignment operator overload!), and returns.
Ejecting a CD
As Figure 9.2 illustrates, writing code that makes things move is neat, so writing code to get the CD player to stick its tongue out is thrilling. Okay, maybe only if you're in severe need of a life, but still—tray ejection code can be useful. Here's how to do it:

Figure 9.2: The effects of sending an MCI_SET_DOOR_OPEN command.
void CCDPlayer::OpenCDDoor()
{
if (!m_InitGood) return;
m_Paused = false;
mciSendCommand(m_DeviceID,MCI_SET,MCI_SET_DOOR_OPEN,0);
}
It doesn't get much easier than that. Just send a MCI_SET message, specify the MCI_SET_DOOR_OPEN flag, and you're done—no need to pass along a structure.Closing the door is just as easy:
void CCDPlayer::CloseCDDoor()
{
if (!m_InitGood) return;
mciSendCommand(m_DeviceID,MCI_SET,MCI_SET_DOOR_CLOSED,0);
}
Here, the MCI_SET_DOOR_CLOSED flag tells the CD audio device to suck its tray back in. If the tray's not sticking out, no harm is done.
Detecting When a CD Has Changed
There are two parts to accurate CD change detection. The first part involves querying the device to see if there's a CD in the drive. Here's that code:
bool CCDPlayer::IsCDInDrive()
{
if (!m_InitGood) return(false);
MCI_STATUS_PARMS statusparms;
ZeroMemory(&statusparms, sizeof(MCI_STATUS_PARMS));
statusparms.dwItem = MCI_STATUS_MEDIA_PRESENT;
MCIERROR e = mciSendCommand(m_DeviceID,MCI_STATUS,MCI_STATUS_ITEM |
MCI_WAIT,
(DWORD)(LPVOID)&statusparms);
if (e) HandleError(e); return(statusparms.dwReturn == TRUE);
}
Fairly straightforward code—it sends a MCI_STATUS message asking about MCI_STATUS_MEDIA_PRESENT, and gets a TRUE back if there is indeed a CD in the drive.The next, and slightly trickier, part is knowing when a CD has changed. You don't want to poll MCI_STATUS_MEDIA_PRESENT continuously. To properly detect a CD change, you need to listen for the WM_DEVICECHANGE Windows message. Windows will send this message to the top-level HWND of all programs when a CD is removed or inserted.
Tip | Technically, the WM_DEVICECHANGE message means that the hardware configuration has changed, so you'll also get this message when you start plugging and unplugging USB and firewire devices. |
To properly interpret this WM_DEVICECHANGE message, you'll need to look at its wParam. If a CD has been pulled out, the wParam will contain DBT_DEVICEREMOVECOMPLETE (that's "device remove complete"). When a new CD is put in, you'll see DBT_DEVICEARRIVAL. For more information, consult the MSDN docs for WM_DEVICECHANGE.
Determining What's on a CD
You have almost attained complete mastery over the CD. One thing remains—how do you figure out what's on a CD? Sure, you know how to get a CD's product code and generated ID, but how do you know how many tracks there are?Bonus points if you guessed that it would involve the MCI_STATUS message. Here's the code:
int CCDPlayer::GetNumTracks()
{
if (!m_InitGood) return(false);
MCI_STATUS_PARMS statusparms;
ZeroMemory(&statusparms, sizeof(MCI_STATUS_PARMS));
statusparms.dwItem = MCI_STATUS_NUMBER_OF_TRACKS;
MCIERROR e = mciSendCommand(m_DeviceID,MCI_STATUS,MCI_STATUS_ITEM |
MCI_WAIT,
(DWORD)(LPVOID)&statusparms);
if (e) HandleError(e);
return(statusparms.dwReturn);
}
Tip | There are many more status commands than what I've covered here. Look at the MSDN docs if you want to learn how to do things like query each track's length or track type (data or audio). |
MCI provides a special type of status query, MCI_STATUS_NUMBER_OF_TRACKS, that gets the job done nicely. It puts the number of tracks into the dwReturn member of the MCI_STATUS_PARMS structure you gave it.
Synchronous versus Asynchronous Command Execution
One last topic—back when describing the Play method, I mentioned the MCI_NOTIFY flag. You can use this flag on any MCI message, and it's vital because it tells MCI that you'd like to be notified when whatever you've requested is complete. Tack it onto a MCI_PLAY message and MCI will notify you when the playing is complete, that is, when the CD hits the end of what you've told it to play.MCI notifies you through a MM_MCINOTIFY message passed to your main window. You can identify what specifically happened by looking at the wParam of the MM_MCINOTIFY message, which will either be MCI_NOTIFY_ABORTED, MCI_NOTIFY_FAILURE, MCI_NOTIFY_SUCCESSFUL, or MCI_NOTIFY_SUPERSEDED. The lParam contains the device ID you sent the message to.Using MM_MCINOTIFY, you can loop tracks indefinitely, shuffle a CD, and do all sorts of other cool things. Just take the appropriate action when you get a MM_MCINOTIFY message with your CD player's device ID in the lParam and MCI_NOTIFY_SUCCESSFUL in the wParam.On the other hand, if you want a command to execute synchronously—that is, you want mciSendCommand to wait until the command's complete before returning—you can specify the MCI_WAIT flag. MCI_WAIT allows you to play it safe and know for sure that a message has been completely processed—this is useful if you're sending a bunch of messages in a row.Check out Figure 9.3 for a comparison of synchronous and asynchronous command execution, and for more information, consult MM_MCINOTIFY in MSDN.

Figure 9.3: Synchronous versus asynchronous command execution.
Controlling CD Audio Volume
You can control the volume of the CD player by using the Mixer API, described in Chapter 5. I thought I'd mention it just in case you missed it back then—simply provide the correct line settings (you can use the pre-built CDAudio constant in CMixer), and set the volume as you wish.