Building a CD Player - Part 2

From EDM2
Jump to: navigation, search

Written by Stephane Bessette

Opening the Device

The first command to be issued to the CD device should be MCI_OPEN. Just as you first need to turn on the power of an audio CD player, you have to open the CD device before you can use it. Unfortunately, we're missing some information: we do not know the ID of the CD device and need to pass this ID as the first parameter of mciSendCommand(). Here's the workaround. We can place the name of the device in the pszDeviceType member of the MCI_OPEN_PARMS structure, and then open the device by passing 0 as the first parameter:

  mciOpenParameters.pszDeviceType = MCI_DEVTYPE_CD_AUDIO_NAME;

  ulrc = mciSendCommand(0,
                        MCI_OPEN,
                        MCI_NOTIFY | MCI_OPEN_SHAREABLE,
                        &mciOpenParameters,
                        0);

After the command has executed, the ID of the device can be retrieved from the usDeviceId member of the MCI_OPEN_PARMS structure:

  usDeviceID = mciOpenParameters.usDeviceID;

We'll receive an MM_MCINOTIFY message when the CD device is opened since we specified that we wanted to be notified. If the command was successful, we can continue with our program. Otherwise, we were unable to establish a connection with the device, and the program should not proceed.

Sharing the Device

By specifying the MCI_OPEN_SHAREABLE flag, we indicate that the device can be shared with other programs. Two messages are implicated in the device-sharing scheme. MM_MCIPASSDEVICE is received when the program is gaining or losing use of the device. The first short of mp2 will contain MCI_GAINING_USE when we are gaining use of the device, and MCI_LOSING_USE when losing use of the device. There is nothing to do in response to this message, except to keep track of whether we own (have use of) the device or not.

The second message implicated in device-sharing is WM_ACTIVATE. When mp1 is TRUE, the window is being activated. In that situation, we have to determine if we currently have use of the device. If we do not, then we need to acquire it:

ulrc = mciSendCommand(usDeviceID,
                      MCI_ACQUIREDEVICE,
                      MCI_WAIT,
                      &mciGenericParameters,
                      NULL);

And in the case where mp1 is false, the window is losing focus. And no, we do not have to release the device; it is up to the other programs to acquire the device if they need it. To clarify this, consider the situation where the CD Player is the only program using the CD device. If we released the CD device when the window lost focus, then the program would stop playing the audio CD as soon as we switched to another application. Clearly this is not desirable.

Querying the Capacities

Now that we've opened the CD device, we need to find out what it can and cannot do. The best way to accomplish this is by querying each capacity that is important to us. (In the source code, I've implemented all the possible capacities.) We'll look at one here. The first step is to specify which capacity we wish to query by specifying that capacity in the ulItem member of the MCI_GETDEVCAPS_PARMS structure and using the MCI_GETDEVCAPS_ITEM flag:

  mciGetDevCapsParameters.ulItem = MCI_GETDEVCAPS_CAN_PLAY;

  ulrc = mciSendCommand(usDeviceID,
                        MCI_GETDEVCAPS,
                        MCI_WAIT | MCI_GETDEVCAPS_ITEM,
                        &mciGetDevCapsParameters,
                        0);

Once the command has been processed, we determine if it was successful. If it was, we will find the answer to our query in the ulReturn member of the MCI_GETDEVCAPS_PARMS structure: MCI_TRUE if the device has the capacity, otherwise MCI_FALSE. But if the command generated an error, we should assume that the device does not possess the capacity. Here's an example:

  if(LOUSHORT(ulrc) == MCIERR_SUCCESS) {
       // The command was successful
       if(mciGetDevCapsParameters.ulReturn == MCI_TRUE) {
            // The device has the capacity
       }
       else {
            // The device does not have the capacity
       }
  }
  else {
       // Since the command was not successful, we
       // should assume that the device does not possess
       // the queried capacity
  }

The MCI_GETDEVCAPS_CAN_PLAY query is special. An affirmative response means that the device can Play (MCI_PLAY). But it also means that it can Stop (MCI_STOP), Pause (MCI_PAUSE), and Resume (MCI_RESUME). In short, this query is concerned with the basic audio CD commands.

Now that we've obtained various information about the device, we need to determine whether there's an audio CD in the drive.

Is the Device Ready?

The CD Player program can be in two states. The first is where an audio CD is detected in the CD device, and the second is where there is no audio CD detected. Two steps are involved. The first is to determine if there's a CD in the device. If there is a CD in the device, we then determine if it contains audio tracks.

If there's a CD in the tray, the device will report that it is ready. If there's no CD in the tray, or if the tray door is opened, the device will report that it is not ready:

  mciStatusParameters.ulItem = MCI_STATUS_READY;

  ulrc = mciSendCommand(usDeviceID,
                        MCI_STATUS,
                        MCI_WAIT | MCI_STATUS_ITEM,
                        &mciStatusParameters,
                        0);

The ulReturn member of the MCI_STATUS_PARMS structure will contain MCI_TRUE if the device is ready, otherwise it will contain MCI_FALSE:

  if(mciStatusParameters.ulReturn == MCI_TRUE) {
       // There's a CD in the tray
  }
  else {
       // There's no CD in the tray, or the tray
       // door is opened (ejected)
  }

If a CD has been detected, if the device is ready, then we're ready to query the number of audio tracks present on the CD:

mciStatusParameters.ulItem = MCI_STATUS_NUMBER_OF_TRACKS;

ulrc = mciSendCommand(usDeviceID,
                      MCI_STATUS,
                      MCI_WAIT | MCI_STATUS_ITEM,
                      &mciStatusParameters,
                      NULL);

if(LOUSHORT(ulrc) == MCIERR_SUCCESS) {
     // The command was successful
     TrackCount = (int) mciStatusParameters.ulReturn;
}
else {
     // The command was not successful
     TrackCount = 0;
}

The number of tracks is returned in the ulReturn member of the MCI_STATUS_PARMS structure. If there is(are) audio track(s), then we have to perform several tasks.

Audio Track(s) Detected

One group of tasks is to enable most of the controls: Play, Previous, Next, Backward seek, Forward seek, Mute, Eject, and Table of Contents. This involves enabling menuitems, showing and activating pushbuttons. And we would disable these controls if there were no audio tracks.

A second task is to request notifications for every second the audio CD plays. This will permit us to properly update the display. This topic will be discussed in the Updating the Display section.

A third task is to query the Table of Contents. We need this information in case the user requests it, of course. But we also require this to properly display the time in any of the four implemented formats. And it is also essential when seeking and to set cuepoints.

The final tasks are to set the first track as the current track, update the track and time display, and set the cuepoint at the end of the first track. These topics will be discussed in later sections.

Since the information contained within the Table of Contents is necessary for quite a number of tasks, we'll cover that topic next.

Querying the Table of Contents

The Table of Contents contains important pieces of information: the number, the starting time position and the ending time position of each track. The query of the Table of Contents requires more preparation than other MCI commands. First we have to create a Table of Contents object:

MCI_TOC_PARMS TOC;

Then we have to create room to contain the information about each audio track (records). We'll use the already queried number of tracks for this:

MCI_TOC_REC *record;
record = new MCI_TOC_REC [TrackCount];

Finally, we fill in the relevant members of the MCI_TOC_PARMS structure; the address to the array of records and the total size of this array of records:

TOC.tocHeader.pBuf = TOC.record;
TOC.tocHeader.ulBufSize = TOC.track_count * sizeof(MCI_TOC_REC);

ulrc = mciSendCommand(usDeviceID,
                      MCI_GETTOC,
                      MCI_WAIT,
                      &TOC.tocHeader,
                      NULL);

TOC.tocHeader is now an array containing information about each track. Here are the members we are interested in:

BYTE  TOC.tocHeader[].TrackNum    // Track number
ULONG TOC.tocHeader[].ulStartAddr // Starting address
ULONG TOC.tocHeader[].ulEndAddr   // Ending address

And that's all!