Borland Online And The Cobb Group Present:


January, 1996 - Vol. 3 No. 1

Getting started with Windows multimedia

by Kent Reisdorph

Multimedia is one of the fastest growing segments of personal computing today. The Windows Media Control Interface (MCI) API handles multimedia operations, but it's a fairly complex topic. Windows has two methods of accessing MCI: the command-message interface, which uses mciSendCommand, and the MCI string-message interface. In this article, we'll look at the MCI API, concentrating on the command-message interface because the string-message interface isn't quite as powerful and suffers a slight performance hit as the strings are translated and converted to mciSendCommand calls.

The real benefit of MCI is that it frees you from worrying about the device-specific characteristics of various sound boards, video cards, and other devices. You use the same command to play a WAV file regardless of the sound card manufacturer. In fact, you use the same command to play a WAV file, MIDI file, AVI video, FLI animation, CD audio, or even a videodisc.

MCI offers three levels of complexity and power. The high-level audio function, sndPlaySound(), allows simple playing of WAV files. The middle level, which we'll concentrate on primarily, is implemented using the mciSendCommand() function. Low-level services include the waveOutxxx, waveInxxx, midiOutxxx, midiInxxx, and AuxOutxxx families of functions. This article will focus on the most commonly used MCI commands that are supported by all devices.

High-level audio

If all you need to do is to play waveform files, then the sndPlaySound() function will do nicely for you. To play a WAV file you need to use only one line of code:

sndPlaySound(ding.wav, SND_SYNC);

The SND_SYNC flag specifies that Windows won't return control to the calling application until the sound has finished playing. To return control to the calling application as soon as the sound begins playing, use the SND_ASYNC flag. Other flags that can be used with the SND_ASYNC and SND_SYNC flags are SND_LOOP, SND_MEMORY, SND_NODEFAULT, and SND_NOSTOP. See online help for descriptions of these flags.

Where it all begins

The mciSendCommand() function is the heart of the MCI API. You'll access nearly all of MCI's mid-level features through this function. The mciSendCommand() function has the following signature:

MCIERROR mciSendCommand(
    MCIDEVICEID  IDDevice,
    UINT  uMsg,  // message to send
    DWORD  fdwCommand,  // flags
    DWORD  dwParam  // pointer to command-
                    //specific structure
   );

Let's look at these parameters one at a time. The parameters are interrelated, so we'll give you a quick overview and then look at each in more detail as we go along.

The MCIDEVICEID parameter is the ID that MCI assigns to a device when you open it. You obtain this device ID by calling mciSendCommand() with an MCI_OPEN message.

The UINT uMsg parameter is one of the MCI_xxx commands we mentioned in the introduction. At the most basic level, these commands might include MCI_OPEN, MCI_PLAY, MCI_RECORD, MCI_SEEK, MCI_STOP, and MCI_CLOSE. There are about 30 such commands, so as you can see, MCI can be a little overwhelming.

The third parameter is a DWORD containing the flags. Some flags apply to all commands; others are command-specific. We'll take a look at the flags a little later.

The final DWORD parameter is a void pointer to a structure that contains the specific values needed for each command. At a minimum, this structure will contain a DWORD that holds the HWND of the window to receive the MM_MCINOTIFY message. The MCI API contains literally dozens of these structures. MCI uses a command-specific structure for nearly every command in the MCI API. In addition, a particular device may need additional data and so extends the basic MCI structure for a given command. For example, there's an MCI_OPEN_PARMS struct, an MCI_WAVE_OPEN_PARMS struct, an MCI_ANIM_OPEN_PARMS struct, and on and on.

The mciSendCommand() function returns a value of 0 if the function was successful; it returns one of the MCIERR_xxx error messages in the case of an error.

MCI_OPEN and the MCI device ID

The first step in controlling a multimedia device is to obtain a device ID from MCI. Subsequent MCI calls for that device will use this ID. You obtain the device ID by issuing the MCI_OPEN command using a device ID of 0. This command will return a device ID in the MCI_OPEN_PARMS structure's wDeviceID member. The following example assumes that you have a class member called DeviceID that holds the device ID that MCI returned, and also a DWORD called ErrCode to hold the return value of the mciSendCommand() function.

DWORD flags = MCI_OPEN_ELEMENT | 
              MCI_OPEN_TYPE;
UINT devID;
MCI_OPEN_PARMS openParms;
openParms.lpstrDeviceType = waveaudio;
openParms.lpstrElementName = ding.wav;
ErrCode = mciSendCommand(0, MCI_OPEN, flags, (DWORD)(LPVOID) &openParms);
if (!ErrCode)  DeviceID = 
                   openParms.wDeviceID;

Note that if the MCI_OPEN command succeeds, then the class member DeviceID is set to the device ID returned in the wDeviceID member of the MCI_OPEN_PARMS structure. We'll use this device ID in all subsequent commands.

In this example, we're using only two of the data fields of the MCI_OPEN_PARMS structure. We set the flags to tell MCI that we're providing both a filename and a device type, and then set the corresponding members of the MCI_OPEN_PARMS struct to the appropriate values passed. The device type is not case-sensitive and corresponds to one of the devices listed in the MCI section of the SYSTEM.INI file. (In reality, we need only specify a filename or a device type, not both. MCI will automatically choose the correct device based on the extension of the filename you provide.)

Flags used in MCI

Before going further, we need to discuss two flags that you can use with all of the MCI commands­­MCI_NOTIFY and MCI_WAIT. You can use these flags together or individually. Note that you should always provide at least one of these flags­­the mciSendCommand() function fails if you use 0 for the flags parameter. In addition, you may need to specify a command-specific flag, depending on the MCI command being issued.

MCI_WAIT tells MCI to keep control until after the command finishes. If you issue an MCI_PLAY command with this flag, your app won't regain control until after the file is finished playing. (This is the same effect as using the SND_SYNC flag with sndPlaySound(), as we explained previously.) If you want Windows to return control to your app as soon as the file begins playing, then specify only the MCI_NOTIFY flag. (This simulates the effect of using sndPlaySound() with the SND_ASYNC flag.)

The MCI_NOTIFY command tells MCI to send an MM_MCINOTIFY message when the command finishes or when it's interrupted by MCI_STOP or a similar command. The message is sent to the window specified in the dwCallback member of the parameter struct. The calling app should catch and process this message. For ObjectWindows Library (OWL) apps, you'll use the EV_MESSAGE macro to catch this message, just as you would for user-defined messages. The function signature must be

LRESULT FunctionName(WPARAM, LPARAM); 

The WPARAM value will contain a value indicating whether the MCI command was completed, superseded, interrupted, or failed. The LPARAM value will contain the device ID of the device that sent the message.

You'll often use MCI_NOTIFY without MCI_WAIT so that your app can continue to process messages while a particular multimedia command is being carried out. For instance, if you play an AVI video and your app contains a Stop button for the video, you'll need normal messaging to continue so your app can process a click of the Stop button. Most of the time, specifying only the MCI_NOTIFY flag will accomplish this. If it doesn't, MCI provides a method by which you can use MCI_WAIT and a callback function so your app can process messages while it executes a given command, but that topic is beyond the scope of this article.

Playing a multimedia file

Once you've opened a file for playback, you need to tell MCI to start playing the file. You do so using the MCI_PLAY command. Now that we've obtained a device ID, we'll use it to tell MCI which file to play. The following code demonstrates how to begin playback.

DWORD flags = MCI_NOTIFY;
MCI_PLAY_PARMS playParms;
playParms.dwCallback = MAKELONG(HWindow, 0);
playParms.dwFrom = 0;
playParms.dwTo = 0; 
ErrCode = mciSendCommand(DeviceID,
          MCI_PLAY, flags, (DWORD)(LPVOID) 
          &playParms);

Because we haven't used the MCI_WAIT flag, the code in the above example begins playback and then returns control to the calling app. Note the dwFrom and dwTo members of the MCI_PLAY_PARMS structure. These members allow you to specify a beginning point and an end point for playback. If you specify either point, you'll need to modify these parameters and add the corresponding values to the flags parameter.

MCI uses MCI_FORMAT_MILLISECONDS by default for WAV files. To modify the above code to play from the one-second mark in the file to the five-second mark, we'd use this code:

DWORD flags = MCI_NOTIFY | MCI_FROM |
              MCI_TO;
MCI_PLAY_PARMS playParms;
playParms.dwCallback = MAKELONG(HWindow, 0);
playParms.dwFrom = 1000;
playParms.dwTo = 5000; 
ErrCode = mciSendCommand(DeviceID, MCI_PLAY,
          flags, (DWORD)(LPVOID) &playParms);

Recording and saving WAV audio output

If all we're going to do is play WAV audio, we really don't need MCI, since we could just use the sndPlaySound() function. But how do we record audio and save it to a file? It's fairly simple, really:

DWORD flags = MCI_TO | MCI_FROM | MCI_WAIT;
MCI_RECORD_PARMS recordParms;
recordParms.dwFrom = 0;
recordParms.dwTo = 5000;
ErrCode = mciSendCommand(DeviceID,
          MCI_RECORD, flags,  
          (DWORD)(LPVOID)&recordParms);

This example will record five seconds of audio. If you don't specify dwFrom and dwTo, then you'll need to issue an MCI_STOP command to stop the recording operation. If you don't specify a start and stop point, and you use the MCI_WAIT flag, then you'll have no way of stopping the recording! Fortunately, MCI has provided a break key combination to use in the event of a runaway. By default, the key combination is [Ctrl][Break], but you can change the break key combination by using the MCI_BREAK command.

Now that we've recorded five seconds of waveform audio, let's save it to a file:

DWORD flags = MCI_WAIT | MCI_SAVE_FILE;
MCI_SAVE_PARMS saveParms;
saveParms.dwCallback = MAKELONG(HWindow, 0);
saveParms.lpfilename = test.wav;
ErrCode = mciSendCommand(DeviceID,
          MCI_SAVE, flags, (DWORD)(LPVOID)
          &saveParms);

Notice I added the MCI_SAVE_FILE bit to the DWORD flags. When you use any of the members of an MCI parameter structure, you need to set the appropriate flag to validate that member. Also note that I have been setting the dwCallback member. Despite this fact, you can only use this member if the MCI_NOTIFY flag is set. I've included code to set the data member in these examples to remind you that dwCallback is a member of every MCI parameter structure.

Stopping and pausing playback or recording

Sooner or later, you'll want to manipulate recording or playback of your multimedia files. In the scope of a real application, doing so will, at a minimum, consist of starting an MCI operation without the MCI_WAIT flag and performing some processing in your app while the MCI operation is executing. To use an obvious example, you'd start recording with a button press or a menu selection, and you'd stop recording with an offsetting button press or menu selection. To stop recording, you'd need to issue an MCI_STOP or MCI_PAUSE command.

The difference between these two commands is that the MCI_PAUSE command leaves devices cued and in a position to restart when your app sends an MCI_RESUME command. In practice, there's not much difference in performance between MCI_STOP and MCI_PAUSE. The commands have the same basic format. Both use the MCI_GENERIC_PARMS structure, which contains only the dwCallback member.

MCI_GENERIC_PARMS genericParms;
genericParms.dwCallback = MAKELONG(HWindow,
                          0);
ErrCode = mciSendCommand(DeviceID,
          MCI_STOP, flags, (DWORD)(LPVOID)
          &genericParms);

Fast forward, rewind, and seek

MCI implements these common tape-player terms via the MCI_SEEK command. In addition to the ever-present MCI_NOTIFY and MCI_WAIT, other flags for MCI_SEEK are MCI_SEEK_TO_START, MCI_SEEK_TO_END, and MCI_TO. When using MCI_TO, remember to set the dwTo member of the MCI_SEEK_PARMS structure to the value of the position you want to seek to.

For instance, let's assume that a particular AVI video is 60 frames in length. To seek to the middle of the file, you'd use the following code:

DWORD flags = MCI_TO | MCI_WAIT;
MCI_SEEK_PARMS seekParms;
seekParms.dwTo = 30;
ErrCode = mciSendCommand(DeviceID,
          MCI_SEEK, flags, (DWORD)(LPVOID)
          &seekParms); 

Interestingly, the different multimedia file types use different time formats by default. For example, WAV files use milliseconds, AVI videos and FLI animations use frames, and so on. We won't go into a discussion of time formats here, but simply knowing that time formats differ may save you some headaches at some point in your MCI journeys.

Querying the status and device capabilities

The MCI_STATUS command queries a device for data such as the current position in the file, the length of the media, the current status (playing, stopped, seeking, recording), and the current time format being used, to name just a few items. We've already seen that the return value of mciSendCommand() reports an error code. For this reason, MCI returns the value being queried in the dwReturn member of the MCI_STATUS_PARMS structure. You specify the item that you want to query by setting the dwItem member of the MCI_STATUS_PARMS structure before calling mciSendCommand().

For example, let's find out how long a particular WAV file is. Assuming we have opened the file and have a valid MCI device ID, we'll use this code:

DWORD flags = MCI_WAIT | MCI_STATUS_ITEM;
MCI_STATUS_PARMS statusParms;
statusParms.dwItem = MCI_STATUS_ LENGTH;
ErrCode = mciSendCommand(DeviceID,
          MCI_STATUS, flags,
          (DWORD)(LPVOID) &statusParms);
DWORD mediaLength;
if (!ErrCode) mediaLength =  
    statusParms.dwReturn;

You can obtain more than 30 values by using the MCI_STATUS command. Review online help for a complete listing of the flags you can set for the different devices available.

The MCI_GETDEVCAPS command works identically to the MCI_STATUS command. That is, you set the dwItem member of the device capability you're seeking in the dwItem member of the MCI_GETDEVCAPS_PARMS structure, and MCI returns the value to you in the dwReturn member. Device capabilities include the ability to play, record, use files, eject the media, play in reverse, freeze the image, and use audio or video. You can even find out the device's MCI type.

These functions lend themselves nicely to inline functions that might query the device capabilities or device status. You need only write a function that performs like the above example and then write a series of inline functions that pass the appropriate flag to this function and then pass back its return value.

Setting device information

The MCI_SET command allows you to set certain features of a given device. With this command you can turn audio on or off, set the input to mono or stereo, change the audio recording parameters, turn video on or off, or change the time format that a given file uses. As in previous examples, you set the appropriate flag, set the corresponding value of the MCI_SET_PARMS structure, and call mciSendCommand() with the MCI_SET command. Again, remember to check the return value from mciSendCommand() to ensure that the value was indeed set.

Turn the light off when you leave

Before your application terminates, it must close any open devices. If you leave an MCI device open, other apps may not be able to use that device. Closing the device frees it and, if no other apps are using that device, unloads the MCI driver from memory. In most cases, the MCI_CLOSE command doesn't require callback notification, so you can simplify the command. You need to write only one line of code:

mciSendCommand(DeviceID, MCI_CLOSE, 0, 
              NULL);

Now let's create a simple application that uses some of the MCI commands we've described.

Sprinting to MCI commands

To test the MCI commands we've discussed, create a new 16-bit Windows application project named MCIAPP.IDE. In the main source file for the project, enter the code from Listing A.


Listing A: MCIAPP.CPP

#include <owl\applicat.h>
#include <owl\framewin.h>
#include <owl\dialog.h>
#include <mmsystem.h>

#define IDD_MAIN 200
#define IDC_START  101
#define IDC_STOP  102

class MainDlg : public TDialog {
  public:
    MainDlg(TWindow* parent) : 
      TDialog(parent, IDD_MAIN) {}
    ~MainDlg();
  protected:
    void SetupWindow();
    void CmStart();
    void CmStop();
  private:
    LRESULT MciNotify(WPARAM, LPARAM);
    void ReportError(const char far* type, 
                     DWORD code);
    uint deviceID;

  DECLARE_RESPONSE_TABLE(MainDlg);
};

DEFINE_RESPONSE_TABLE1(MainDlg, TDialog)
  EV_COMMAND(IDC_START, CmStart),
  EV_COMMAND(IDC_STOP, CmStop),
  EV_MESSAGE(MM_MCINOTIFY, MciNotify),
END_RESPONSE_TABLE;

MainDlg::~MainDlg()
{
  DWORD errCode = mciSendCommand(deviceID, 
                    MCI_CLOSE, 0, NULL);
  if (errCode) ReportError("Close", errCode);
}

void MainDlg::SetupWindow()
{
  TDialog::SetupWindow();
  DWORD flags = MCI_OPEN_ELEMENT | MCI_WAIT;
  MCI_OPEN_PARMS openParms;
  openParms.lpstrElementName = 
    "c:\\windows\\chimes.wav";
  DWORD errCode = mciSendCommand(0, MCI_OPEN, 
    flags, (DWORD)(LPVOID)&openParms);
  if (!errCode) deviceID = openParms.wDeviceID;
  else ReportError("Open", errCode);
}

void MainDlg::CmStart()
{
  DWORD flags = MCI_NOTIFY;
  MCI_PLAY_PARMS playParms;
  playParms.dwCallback = MAKELONG(HWindow, 0);
  DWORD errCode =
    mciSendCommand(deviceID, MCI_PLAY, flags, 
                   (DWORD)(LPVOID)&playParms);
  if (errCode) ReportError("Play", errCode);
}

void MainDlg::CmStop()
{
  DWORD errCode = mciSendCommand(deviceID, 
    MCI_STOP, MCI_WAIT, NULL);
  if (errCode) ReportError("Stop", errCode);
}

LRESULT MainDlg::MciNotify(WPARAM wParam, LPARAM)
{
  if (wParam == MCI_NOTIFY_SUCCESSFUL) {
    DWORD flags = MCI_WAIT | MCI_SEEK_TO_START;
    MCI_SEEK_PARMS seekParms;
    seekParms.dwCallback = MAKELONG(HWindow, 0);
    DWORD errCode =
      mciSendCommand(deviceID, MCI_SEEK, flags, 
        (DWORD)(LPVOID) &seekParms);
    if (errCode) ReportError("Seek", errCode);
    else CmStart();
  }
  return 0;
}

void MainDlg::ReportError(const char far* type, 
                         DWORD code)
{
  char buf[60];
  wsprintf(buf, 
   "An error occurred during MCI %s \nMCI error code
   #%d", type, code);
  MessageBox(buf, "MCI Test App Error", 
             MB_ICONEXCLAMATION | MB_OK);
}

class TTestApp : public TApplication {
  public:
    TTestApp(const char far* title) : 
      TApplication(title) {}
    void InitMainWindow();
};

void
TTestApp::InitMainWindow()
{
  EnableCtl3d();
  MainDlg* dlg = new MainDlg(0);
  MainWindow = new TFrameWindow(0, 
                 "MCI Test App", dlg, true);
  MainWindow->Attr.Style &= ~(WS_MAXIMIZEBOX | 
                              WS_THICKFRAME);
}

int OwlMain(int /*argc*/, char* /*argv*/ [])
{
  TTestApp app("MCI Test App");
  return app.Run();
}

In the MCIAPP.RC file (which contains the Windows resource descriptions), enter the code from Listing B on the next page. When you finish, build and run the application. When you click the Start Wave button, you'll hear the CHIMES.WAV file begin playing. When you click Stop Wave, it will end. To exit the MCI Test application, click Done.


Listing B: MCIAPP.RC

#define IDD_MAIN   200
#define IDC_START  101
#define IDC_STOP   102

IDD_MAIN DIALOG 8, 44, 194, 99
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "MCI Test App"
FONT 8, "MS Sans Serif"
{
 DEFPUSHBUTTON "Done", IDOK, 31, 74, 132, 14
 PUSHBUTTON "Start Wave", IDC_START, 32, 14, 50, 49
 PUSHBUTTON "Stop Wave", IDC_STOP, 113, 13, 50, 49
}

Conclusion

When you begin programming MCI enabled applications, the number of functions and the myriad options can be overwhelming. Fortunately, you can harness much of the MCI library's power by learning a few of the basic commands we've described here.

Kent Reisdorph is a professional C++ developer and a member of TeamB, Borland's online support team. You can contact Kent via CompuServe at 75522,1174.

Return to the Borland C++ Developer's Journal index

Subscribe to the Borland C++ Developer's Journal


Copyright (c) 1996 The Cobb Group, a division of Ziff-Davis Publishing Company. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis Publishing Company is prohibited. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Publishing Company.