Building a CD Player - Part 4

Written by Stephane Bessette

Ejecting and Inserting the Tray Door
To eject the tray door, you need to specify the MCI_SET_DOOR_OPEN flag, and to close the tray door, you specify the MCI_SET_DOOR_CLOSED flag: ulrc = mciSendCommand(usDeviceID,                      MCI_SET,                       MCI_WAIT | MCI_SET_DOOR_OPEN,                       &mciSetParameters,                       0);

After issuing the Eject command, we have to disable many actions (Play for instance). And after issuing the Insert command, we have to go through the detection algorithm presented earlier.

Next, we're going to cover changing track and seeking.

Previous and Next Track
Three events will require a change of track: Several steps are involved in the process of changing track. The first is to determine which is the previous or the next track. Several schemes are possible: Of these, only the sequential scheme has been implemented.
 * the user issues the command
 * the current track has ended
 * seeking past the boundaries of the current track
 * sequential
 * random
 * repeat (the same track over and over)
 * a user-selected sequence

The second step is to reset the cuepoint to the end of the new track, so that we'll receive an MM_MCICUEPOINT message when that time location is reached:

mciCuepointParameters.ulCuepoint = TOC.record[current_track].ulEndAddr; ulrc = mciSendCommand(usDeviceID,                           MCI_SET_CUEPOINT,                            MCI_WAIT | MCI_SET_CUEPOINT_ON,                            &mciCuepointParameters,                            0);

However, since there can only be one cuepoint, we first have to remove the previous cuepoint.

And the final step is to issue the command to either seek to that new location if we're not playing, or to play from that new location if we were playing.

There's one gotcha. If the user presses Previous, does he/she want to go to the previous track or to the beginning of the current track? The solution I've elected is that if more than half a second of the current track has been played, then we go to the beginning of the current track. But if less than half a second has been played, then we go to the beginning of the previous track. Here's how to query the current time location within the current track:

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

As usual, the answer to our query is to be found in the ulReturn member of the MCI_STATUS_PARMS structure.

Backward and Forward Seek
Seeking has been implemented with the help of graphical buttons. These have various features, but the one that is relevant here is that they issue a message when pressed (GBN_BUTTONHILITE) and another when released (GBN_BUTTONUP).

When either seek button is pressed, we'll start a timer:

WinStartTimer(wd->hab,       // (1)               hwnd,           // (2)               wd->timerID,    // (3)               150             // (4)              );


 * 1) handle to the anchor block
 * 2) handle to the window
 * 3) ID of the timer
 * 4) interval of the event (this value feels good)

And for every WM_TIMER message received, we'll seek one second in the proper direction (backward of forward). There are two ways of implementing the seek command. The first is to actually move to that new time location for every WM_TIMER message received:

mmtime += MSECTOMM(1000);// 1 second in MMTIME mciSeekParameters.ulTo = mmtime; ulrc = mciSendCommand(usDeviceID,                           MCI_SEEK,                            MCI_WAIT | MCI_TO,                            &mciSeekParameters,                            0);

Since we do not receive MM_MCIPOSITIONCHANGE messages while seeking (only during playback), we'll have to update the time display ourselves.

Although this scheme works, it is not very efficient. There is no reason to actually seek to that time location since the user is not done seeking. Rather, we should wait until the seek button is released. In this second implementation, we'll take no actions other than updating the time display every time the WM_TIMER message is received.

When we finally receive the GBN_BUTTONUP message, we'll stop the timer: WinStopTimer(hab,     // (1)              hwnd,     // (2)              timerID); // (3)

and seek to that new time location or resume playback from that new time location.
 * 1) handle to the anchor block
 * 2) handle to the window
 * 3) ID of the timer

One thing to watch for when seeking are the boundaries of the current track. If the user seeks beyond the beginning or the end of the current track, we also have to update the current track and the track display.

This completes our coverage of the audio CD Player. What else could there be? Well, what the user sees: the interface.

Menus
There's more to an interface than an eye pleaser. An interface may be pretty but fail at being functional and/or easy to understand and use. We have to keep it simple, but powerful. Let's explore the possibilities.

First, the menus will contain ALL the possible actions. I don't know if there is a rule somewhere about this, but it sure makes a lot of sense to do it. If a user wants to perform some action and can't understand the symbols you've used on your buttons, he/she will have the possibility of going through the menus and finding, in words, the desired action.

The actions should be assorted in related groups. A first group should contain actions common to any program (open, close, exit), the last group should contain the help items (not many in this application), and groups in between are specific to the application. In this application, we'll place the CD commands in one group (play, stop, ...), and configuration items (type of connector, volume) in another.

Buttons
Next comes the selection of buttons. Buttons are fun since they are easily accessible: you don't have to click through a lot of menus to execute an action. But they take up space. Because of this, their number should be kept to a minimum, unless you want to have a large application filled with icons. We'll include only the actions commonly found with CD players: Play, Stop, Pause, Resume, Previous and Next track, Backward and Forward seek, Insert, Eject, Mute (real handy when you're blasting the speakers and the phone rings!), and Unmute. That's 12 buttons. Hmm, there's a way to reduce that count.

Some buttons can be paired. When one member of the pair is active, it cannot be selected. And the other member of the pair can only be selected if the first member is active. Here's an example that will clear this up. Play and Stop will make up one pair. When the CD Player is stopped, pressing Stop again yields nothing. So the user loses nothing if that action is unavailable. However, selecting Play makes sense in this situation. And once Play has been executed, it now makes little sense to press Play once again, but it makes sense to press Stop. Aside from the Play/Stop pair, there's also Pause/Resume, Eject/Insert, and Mute/Unmute. If this still isn't clear, have a look at the program. To enable one action, we simply have to show one member of the pair and hide the other member of the pair. The following function takes care of this: WinShowWindow(WinWindowFromID(hwnd, button_ID),                   state);

There's still one more thing to consider. Most programmers are familiar with pushbuttons. The user clicks on one and you respond to a WM_COMMAND. No sweat. And that's how we'll implement most of the actions. But what about Backward and Forward seek? If we used pushbuttons, every time the user wanted to seek in either direction, he/she would have to repeatedly click on these buttons: to seek 10 seconds backwards would require 10 clicks. And what about seeking 30 seconds: 30 clicks!! We're not building a game here. That's why I used graphical buttons.

Display
Aside from issuing commands, an interface has to provide information to the user. Currently, the current track and one of four time display are provided. But there could be additional items, such as the name of the audio CD and the name of the current track. And as far as the implementation goes, two simple static text fields are used. Nothing fancy here.

Enhancements
Let's now look at a list of enhancements.

No product is ever complete. And this one could profit from a few enhancements. Here are some ideas.

These three could amount to the same thing:
 * AutoDetecting the presence of a CD
 * Monitoring tray door status: opened or closed
 * Detecting manual insert and eject

These simply require additional menus and a few variables:
 * Completing the code for AutoPlay
 * Completing the code for ContinuousPlay

For the artistically inclined:
 * Better graphics for the buttons
 * Better graphics for the track and time displays
 * A background bitmap
 * A better icon

Additional information to display:
 * CD title
 * track title

Additional features:
 * recording from the CD
 * playing a portion of a track (MCI_FROM and MCI_TO)

And some final touches: ulStartAddr = ulEndAddr = 0; TrackNum = 0; so that the display can be properly set
 * When there's no audio CD, create a TOC with one track
 * Remember various settings:
 * connector
 * time display
 * volume
 * position of the dialog