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.
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.
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.
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.)
Before going further, we need to discuss two flags that you can use with all of the MCI commandsMCI_NOTIFY and MCI_WAIT. You can use these flags together or individually. Note that you should always provide at least one of these flagsthe 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.
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);
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.
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);
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.
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.
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.
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.
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 }
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.
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.