Building a CD Player - Part 4

From EDM2
Jump to: navigation, search

Written by Stephane Bessette

Part 1 Part 2 Part 3 Part 4

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:

  • the user issues the command
  • the current track has ended
  • seeking past the boundaries of the current 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:

  • sequential
  • random
  • repeat (the same track over and over)
  • a user-selected sequence

Of these, only the sequential scheme has been implemented.

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)
  1. handle to the anchor block
  2. handle to the window
  3. ID of the timer

and seek to that new time location or resume playback from that new time location.

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:

  • When there's no audio CD, create a TOC with one track
ulStartAddr = ulEndAddr = 0;
TrackNum = 0;

so that the display can be properly set

  • Remember various settings:
    • connector
    • time display
    • volume
    • position of the dialog