Windows and Multimedia

In one sense, multimedia is all about getting access to various pieces of hardware through device-independent function calls. Let's look at this hardware first and then the structure of the Windows multimedia API.

Multimedia Hardware

Perhaps the most commonly used piece of multimedia hardware is the waveform audio device, commonly known as the sound card or sound board. The waveform audio device converts microphone input or other analog audio input into digitized samples for storage in memory or disk files with the .WAV extension. The waveform audio device also converts the waveform back into analog sound for playing over the PC's speakers.

The sound board usually also contains a MIDI device. MIDI is the industry standard Musical Instrument Digital Interface. Such hardware plays musical notes in response to short binary messages. The MIDI hardware usually can also accept a cable connected to a MIDI input device, such as a music keyboard. And often external MIDI synthesizers can also be attached to the sound board.

The CD-ROM drive attached to most of today's PCs is usually capable of playing normal music CDs. This is known as "CD Audio." The output from the waveform audio device, MIDI device, and CD Audio device are often mixed together under user control with the Volume Control application.

A couple other common multimedia "devices" don't require any additional hardware. The Video for Windows device (also called the AVI Video device) plays movie or animation files with the .AVI ("audio-video interleave") extension. The ActiveMovie control plays other types of movies, including QuickTime and MPEG. The video board on a PC may have specialized hardware to assist in playing these movies.

More rare are PC users with certain Pioneer laserdisc players or the Sony series of VISCA video cassette recorders. These devices have serial interfaces and thus can be controlled by PC software. Certain video boards have a feature called "video in a window" that allows an external video signal to appear on the Windows screen along with other applications. This is also considered a multimedia device.

An API Overview

The API support of the multimedia features in Windows is in two major collections. These are known as the "low-level" and the "high-level" interfaces.

The low-level interfaces are a series of functions that begin with a short descriptive prefix and are listed (along with high-level functions) in /Platform SDK/Graphics and Multimedia Services/Multimedia Reference/Multimedia Functions.

The low-level wavefrom audio input and output functions begin with the prefix waveIn and waveOut. We'll be looking at these functions in this chapter. Also examined in this chapter will be midiOut functions to control the MIDI Output device. The API also includes midiIn and midiStream functions.

Also used in this chapter are functions beginning with the prefix time that allow setting a high-resolution preemptive timer routine with a timer interval rate going down to 1 millisecond. This facility is primarily for playing back MIDI sequences. Several other groups of functions involve audio compression, video compression, and animation and video sequences; unfortunately, these will not be covered in this chapter.

You'll also notice in the list of multimedia functions seven functions with the prefix mci that allow access to the Media Control Interface (MCI). This is a high-level, open-ended interface for controlling all multimedia hardware in the Multimedia PC. MCI includes many commands that are common to all multimedia hardware. This is possible because many aspects of multimedia can be molded into a tape recorder-like play/record metaphor. You "open" a device for either input or output, you "record" (for input) or "play" (for output), and when you're done you "close" the device.

MCI itself comes in two forms. In one form, you send messages to MCI that are similar to Windows messages. These messages include bit-encoded flags and C data structures. In the second form, you send text strings to MCI. This facility is primarily for scripting languages that have flexible string manipulation functions but not much support for calling Windows APIs. The string-based version of MCI is also good for interactively exploring and learning MCI, as we'll be doing shortly. Device names in MCI include cdaudio, waveaudio, sequencer (MIDI), videodisc, vcr, overlay (analog video in a window), dat (digital audio tape), and digitalvideo. MCI devices are categorized as "simple" and "compound." Simple devices (such as cdaudio) don't use files. Compound devices (like waveaudio) do; in the case of waveform audio, these files have a .WAV extension.

Another approach to accessing multimedia hardware involves the DirectX API, which is beyond the scope of this book.

Two other high-level multimedia functions also deserve mention: MessageBeep and PlaySound, which was demonstrated way back in Chapter 3. MessageBeep plays sounds that are specified in the Sounds applet of the Control Panel. PlaySound can play a .WAV file on disk, in memory, or loaded as resources. The PlaySound function will be used again later in this chapter.

Exploring MCI with TESTMCI

Back in the early days of Windows multimedia, the software development kit included a C program called MCITEST that allowed programmers to interactively type in MCI commands and learn how they worked. This program, at least in its C version, has apparently disappeared. So, I've recreated it as the TESTMCI program shown in Figure 22-1. The user interface is based on the old MCITEST program but not the actual code, although I can't believe it was much different.

Figure 22-1. The TESTMCI program.

TESTMCI.C

/*----------------------------------------
   TESTMCI.C -- MCI Command String Tester
                (c) Charles Petzold, 1998
  ----------------------------------------*/

#include <windows.h>
#include "resource.h"

#define ID_TIMER    1

BOOL CALLBACK DlgProc (HWND, UINT, WPARAM, LPARAM) ;

TCHAR szAppName [] = TEXT ("TestMci") ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,

                    PSTR szCmdLine, int iCmdShow)

{
     if (-1 == DialogBox (hInstance, szAppName, NULL, DlgProc))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
                      szAppName, MB_ICONERROR) ;
     }
     return 0 ;
}

BOOL CALLBACK DlgProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static HWND hwndEdit ;
     int         iCharBeg, iCharEnd, iLineBeg, iLineEnd, iChar, iLine, iLength ;
     MCIERROR    error ;
     RECT        rect ;
     TCHAR       szCommand [1024], szReturn [1024], 
                 szError [1024], szBuffer [32] ;

     switch (message)
     {
     case WM_INITDIALOG:
               // Center the window on screen

          GetWindowRect (hwnd, &rect) ;
          SetWindowPos (hwnd, NULL, 
               (GetSystemMetrics (SM_CXSCREEN) - rect.right + rect.left) / 2,
               (GetSystemMetrics (SM_CYSCREEN) - rect.bottom + rect.top) / 2,
               0, 0, SWP_NOZORDER | SWP_NOSIZE) ;

          hwndEdit = GetDlgItem (hwnd, IDC_MAIN_EDIT) ;
          SetFocus (hwndEdit) ;
          return FALSE ;

     case WM_COMMAND:
          switch (LOWORD (wParam))
          {
          case IDOK:
                    // Find the line numbers corresponding to the selection

               SendMessage (hwndEdit, EM_GETSEL, (WPARAM) &iCharBeg, 
                                                 (LPARAM) &iCharEnd) ;

               iLineBeg = SendMessage (hwndEdit, EM_LINEFROMCHAR, iCharBeg, 0) ;
               iLineEnd = SendMessage (hwndEdit, EM_LINEFROMCHAR, iCharEnd, 0) ;

                    // Loop through all the lines
               for (iLine = iLineBeg ; iLine <= iLineEnd ; iLine++)
               {
                         // Get the line and terminate it; ignore if blank

                    * (WORD *) szCommand = sizeof (szCommand) / sizeof (TCHAR) ;

                    iLength = SendMessage (hwndEdit, EM_GETLINE, iLine, 
                                                     (LPARAM) szCommand) ;
                    szCommand [iLength] = `\0' ;

                    if (iLength == 0)
                         continue ;

                         // Send the MCI command

                    error = mciSendString (szCommand, szReturn, 
                              sizeof (szReturn) / sizeof (TCHAR), hwnd) ;

                         // Set the Return String field

                    SetDlgItemText (hwnd, IDC_RETURN_STRING, szReturn) ;

                         // Set the Error String field (even if no error)

                    mciGetErrorString (error, szError, 
                                       sizeof (szError) / sizeof (TCHAR)) ;

                    SetDlgItemText (hwnd, IDC_ERROR_STRING, szError) ;
               }
                    // Send the caret to the end of the last selected line

               iChar  = SendMessage (hwndEdit, EM_LINEINDEX,  iLineEnd, 0) ;
               iChar += SendMessage (hwndEdit, EM_LINELENGTH, iCharEnd, 0) ;
               SendMessage (hwndEdit, EM_SETSEL, iChar, iChar) ;
               
                    // Insert a carriage return/line feed combination

               SendMessage (hwndEdit, EM_REPLACESEL, FALSE, 
                                      (LPARAM) TEXT ("\r\n")) ;
               SetFocus (hwndEdit) ;
               return TRUE ;

          case IDCANCEL:
               EndDialog (hwnd, 0) ;
               return TRUE ;

          case IDC_MAIN_EDIT:
               if (HIWORD (wParam) == EN_ERRSPACE)
               {
                    MessageBox (hwnd, TEXT ("Error control out of space."),
                                szAppName, MB_OK | MB_ICONINFORMATION) ;
                    return TRUE ;
               }
               break ;
          }
          break ;

     case MM_MCINOTIFY:
          EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_MESSAGE), TRUE) ;

          wsprintf (szBuffer, TEXT ("Device ID = %i"), lParam) ;
          SetDlgItemText (hwnd, IDC_NOTIFY_ID, szBuffer) ;
          EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_ID), TRUE) ;

          EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_SUCCESSFUL),
                              wParam & MCI_NOTIFY_SUCCESSFUL) ;
          
          EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_SUPERSEDED),
                              wParam & MCI_NOTIFY_SUPERSEDED) ;

          EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_ABORTED),
                              wParam & MCI_NOTIFY_ABORTED) ;

          EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_FAILURE),
                              wParam & MCI_NOTIFY_FAILURE) ;

          SetTimer (hwnd, ID_TIMER, 5000, NULL) ;
          return TRUE ;

     case WM_TIMER:
          KillTimer (hwnd, ID_TIMER) ;

          EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_MESSAGE), FALSE) ;
          EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_ID), FALSE) ;
          EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_SUCCESSFUL), FALSE) ;
          EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_SUPERSEDED), FALSE) ;
          EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_ABORTED), FALSE) ;
          EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_FAILURE), FALSE) ;
          return TRUE ;

     case WM_SYSCOMMAND:
          switch (LOWORD (wParam))
          {
          case SC_CLOS
E:
               EndDialog (hwnd, 0) ;
               return TRUE ;
          }
          break ;
     }
     return FALSE ;
}

TESTMCI.RC (excerpts)

//Microsoft Developer Studio generated resource script.

#include "resource.h"
#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
// Dialog

TESTMCI DIALOG DISCARDABLE  0, 0, 270, 276
STYLE WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "MCI Tester"
FONT 8, "MS Sans Serif"
BEGIN
    EDITTEXT        IDC_MAIN_EDIT,8,8,254,100,ES_MULTILINE | ES_AUTOHSCROLL | 
                    WS_VSCROLL
    LTEXT           "Return String:",IDC_STATIC,8,114,60,8
    EDITTEXT        IDC_RETURN_STRING,8,126,120,50,ES_MULTILINE | 
                    ES_AUTOVSCROLL | ES_READONLY | WS_GROUP | NOT WS_TABSTOP
    LTEXT           "Error String:",IDC_STATIC,142,114,60,8
    EDITTEXT        IDC_ERROR_STRING,142,126,120,50,ES_MULTILINE | 
                    ES_AUTOVSCROLL | ES_READONLY | NOT WS_TABSTOP
    GROUPBOX        "MM_MCINOTIFY Message",IDC_STATIC,9,186,254,58
    LTEXT           "",IDC_NOTIFY_ID,26,198,100,8
    LTEXT           "MCI_NOTIFY_SUCCESSFUL",IDC_NOTIFY_SUCCESSFUL,26,212,100,
                    8,WS_DISABLED
    LTEXT           "MCI_NOTIFY_SUPERSEDED",IDC_NOTIFY_SUPERSEDED,26,226,100,
                    8,WS_DISABLED
    LTEXT           "MCI_NOTIFY_ABORTED",IDC_NOTIFY_ABORTED,144,212,100,8,
                    WS_DISABLED
    LTEXT           "MCI_NOTIFY_FAILURE",IDC_NOTIFY_FAILURE,144,226,100,8,
                    WS_DISABLED
    DEFPUSHBUTTON   "OK",IDOK,57,255,50,14
    PUSHBUTTON      "Close",IDCANCEL,162,255,50,14
END

RESOURCE.H (excerpts)

// Microsoft Developer Studio generated include file.
// Used by TestMci.rc

#define IDC_MAIN_EDIT                   1000
#define IDC_NOTIFY_MESSAGE              1005
#define IDC_NOTIFY_ID                   1006
#define IDC_NOTIFY_SUCCESSFUL           1007
#define IDC_NOTIFY_SUPERSEDED           1008
#define IDC_NOTIFY_ABORTED              1009
#define IDC_NOTIFY_FAILURE              1010
#define IDC_SIGNAL_MESSAGE              1011
#define IDC_SIGNAL_ID                   1012
#define IDC_SIGNAL_PARAM                1013
#define IDC_RETURN_STRING               1014
#define IDC_ERROR_STRING                1015
#define IDC_DEVICES                     1016
#define IDC_STATIC                      -1

Like many of the programs in this chapter, TESTMCI uses a modeless dialog box as its main window. Like all of the programs in this chapter, TESTMCI requires the WINMM.LIB import library to be listed in the Links page of the Projects Settings dialog box in Microsoft Visual C++.

This program uses the two most important multimedia functions. These are mciSendString and mciGetErrorText. When you type something into the main edit window in TESTMCI and press Enter (or the OK button), the program passes the string you typed in as the first argument to the mciSendString command:

error = mciSendString (szCommand, szReturn, 
                       sizeof (szReturn) / sizeof (TCHAR), hwnd) ;

If more than one line is selected in the edit window, the program sends them sequentially to the mciSendString function. The second argument is the address of a string that gets information back from the function. The program displays this information in the Return String section of the window. The error code returned from mciSendString is passed to the mciGetErrorString function to obtain a text error description; this is displayed in the Error String section of TESTMCI's window.

MCITEXT and CD Audio

You can get an excellent feel for MCI command strings by taking control of the CD-ROM drive and playing an audio CD. This is a good place to begin because these command strings are often quite simple and, moreover, you get to listen to some music. You may want to have the MCI command string reference at /Platform SDK/Graphics and Multimedia Services/Multimedia Reference/Multimedia Command Strings handy for this exercise.

Make sure the audio output of your CD-ROM drive is connected to speakers or a headphone, and pop in an audio compact disc, for example, Bruce Springsteen's Born to Run. Under Windows 98, the CD Player application might start up and begin playing the album. If so, end the CD Player. Instead, bring up TESTMCI and type in the command

open cdaudio

and press Enter. The word open is an MCI command and the word cdaudio is a device name that MCI recognizes as the CD-ROM drive. (I'm assuming you have only one CD-ROM drive on your system; getting names of multiple CD-ROM drives requires use of the sysinfo command.)

The Return String area in TESTMCI shows the string that the system sends back to your program in the mciSendString function. If the open command works, this is simply the number 1. The Error String area in TESTMCI shows what the mciGetErrorString returns based on the return value from mciSendString. If mciSendString did not return an error code, the Error String area displays the text "The specified command was carried out."

Assuming the open command worked, you can now enter

play cdaudio 

The CD will begin playing "Thunder Road," the first cut on the album. You can pause the CD by entering

pause cdaudio

or

stop cdaudio

For the cdaudio device, these statements do the same thing. You can resume playing with

play cdaudio

So far, all the strings we've used have been composed of a command and the device name. Some commands have options. For example, type

status cdaudio position

Depending how long you've been listening, the Return String area should show something like

01:15:25

What is this? It's obviously not hours, minutes, and seconds because the CD is not that long. To find out what the time format is, type

status cdaudio time format

The Return String area now shows the string

msf

This stands for "minutes-seconds-frames." In CD Audio, there are 75 frames to the second. The frame part of the time format can range from 0 through 74.

The status command has a bunch of options. You can determine the entire length of the CD in msf format using the command

status cdaudio length

For Born to Run, the Return String area will show

39:28:19

That's 39 minutes, 28 seconds, and 19 frames.

Now try

status cdaudio number of tracks

The Return String area will show

8

We know from the CD cover that the title tune is the fifth track on the Born to Run album. Track numbers in MCI commands begin at 1. We can find out how long the song "Born to Run" is by entering

status cdaudio length track 5

The Return String area shows

04:30:22

We can also determine where on the album this track begins

status cdaudio position track 5

The Return String area shows

17:36:35

With this information we can now skip directly to the title track:

play cdaudio from 17:36:35 to 22:06:57

This command will play the one song and then stop. That last value was calculated by adding 4:30:22 (the length of the track) to 17:36:35. Or, it could be determined by using

status cdaudio position track 6

Or, you can set the time format to tracks-minutes-seconds-frames:

set cdaudio time format tmsf

and then

play cdaudio from 5:0:0:0 to 6:0:0:0

or, more simply,

play cdaudio from 5 to 6

You can leave off trailing components of the time if they are 0. It is also possible to set the time format in milliseconds.

Every MCI command string can include the options wait or notify (or both) at the end of the string. For example, suppose you want to play only the first 10 seconds of the song "Born to Run," and right after that happens, you want the program to do something else. Here's one way to do it (assuming you've set the time format to tmsf):

play cdaudio from 5:0:0 to 5:0:10 wait

In this case, the mciSendString function does not return until the function has been completed, that is, until the 10 seconds of "Born to Run" have finished playing.

Now obviously, in general, this is not a good thing in a single-threaded application. If you accidentally typed

play cdaudio wait

the mciSendString function will not return control to the program until the entire album has played. If you must use the wait option (and it is handy when blindly running MCI scripts, as I'll demonstrate shortly), use the break command first. This command lets you set a virtual key code that will break the mciSendString command and return control to the program. For example, to set the Escape key to serve this purpose, use

break cdaudio on 27

where 27 is the decimal value of VK_ESCAPE. notify option:

play cdaudio from 5:0:0 to 5:0:10 notify

In this case, the mciSendString function returns immediately, but when the operation specified in the MCI command ends, the window whose handle is specified as the last argument to mciSendString receives an MM_MCINOTIFY message. The TESTMCI program displays the result of this message in the MM_MCINOTIFY group box. To avoid confusion as you may be typing in other commands, the TESTMCI program stops displaying the results of the MM_MCINOTIFY message after 5 seconds.

You can use the wait and notify keywords together, but there's hardly a reason for doing so. Without these keywords, the default behavior is to not wait and to not notify, which is usually what you want.

When you're finished playing around with these commands, you can stop the CD by entering

stop cdaudio

If you don't stop the CD-ROM device before closing it, the CD will continue to play even after you close the device.

You can try something that may or may not work with your hardware:

eject cdaudio

And then finally close the device like so:

close cdaudio

Although TESTMCI cannot save or load text files by itself, you can copy text between the edit control and the clipboard. You can select something in TESTMCI, copy it to the clipboard (using Ctrl-C), copy the text from the clipboard into NOTEPAD, and then save it. Reverse this process to load a series of MCI commands into TESTMCI. If you select a series of commands and press OK (or the Enter key), TESTMCI will execute the commands one at a time. This lets you construct MCI "scripts," which are simply lists of MCI commands.

For example, suppose you like to listen to the songs "Jungleland" (the last track on the album), "Thunder Road," and "Born to Run," in that order. Construct a script like so:

open cdaudio
set cdaudio time format tmsf
break cdaudio on 27
play cdaudio from 8 wait
play cdaudio from 1 to 2 wait
play cdaudio from 5 to 6 wait
stop cdaudio
eject cdaudio
close cdaudio

Without the wait keywords, this wouldn't work correctly because the mciSendString commands would return immediately and the next one would then execute.

At this point, it should be fairly obvious how to construct a simple application that mimics a CD player. Your program can determine the number of tracks and the length of each track and can allow the user to begin playing at any point. (Keep in mind, however, that mciSendString always returns information in text strings, so you'll need to write parsing logic that converts those strings to numbers.) Such a program would almost certainly also use the Windows timer, for intervals of a second or so. During WM_TIMER messages, the program would call

status cdaudio mode 

to see whether the CD is paused or playing. The

status cdaudio position

command lets the program update its display to show the user the current position. But something more interesting is also possible: if your program knows the time positions of key parts of the music, it can synchronize on-screen graphics with the CD. This is excellent for music instruction or for creating your own graphical music videos.