Windows Sights and Sounds

By Matt Hart

This article first appeared in Visual Developer Magazine.
(Note: This is my unedited original and may differ slightly from the published version.)


SYSTEM REQUIREMENTS: VB 5, VB 6, Professional or Enterprise Versions
LEVEL: Intermediate to Advanced Programmers

The Windows Media Control Interface, or MCI, is often the first thing a new programmer attempts to use. Playing a sound, recording a CD track, or showing a video clip isn't quite as easy as drawing a control on a form and setting a few properties, but it isn't as difficult as it seems at first glance. This article will teach you the basics of MCI and other Windows audio interfaces via a Visual Basic sample project that records audio clips and plays CDs, audio clips, and video clips.

There are two basic API functions that access the MCI, mciSendString and mciSendCommand. They do the same thing: one with command strings and one with command constants. This article uses strictly the command strings because they make the program easy to read. A device identifier such as "movie" is easier to understand than lDevID.

The mciSendString API function has four parameters:

Public Declare Function mciSendString Lib "winmm.dll" Alias _
    "mciSendStringA" (ByVal lpstrCommand As String, _
    ByVal lpstrReturnString As String, _
    ByVal uReturnLength As Long, _
    ByVal hwndCallback As Long) As Long
The lpstrCommand parameter is the actual MCI command you want to execute (see Table 1). The other three parameters are necessary only when you want to receive information back from the function. The lpstrReturnString parameter is a string buffer initialized to uReturnLength characters. The hwndCallBack variable is a handle to a callback window. This article doesn’t use callback windows since they are of limited use in MCI functions. You can subclass a form to watch for the MCI_NOTIFY messages, but there is not a lot of information in them – the aborted, failure, successful, and superseded notification messages are not as useful as the return value of the API function itself. If the result of mciSendString is non-zero, then you can easily discover exactly what the error was.

   Table 1 – MCI Command Strings
   Command          Description
   capability       Returns capability information
                      capability cdaudio can eject
   close            Closes an MCI device
                      close cdaudio
   info             Returns device information
                      info cdaudio identity
   open             Opens an MCI device
                      open sequencer!filename.mid alias midi
   pause            Pauses playing or recording
                      pause cdaudio
   play             Begins playing an open device
                      play movie from 0 to 1
   put              Defines the area and window used for display
                      put movie window client at 0 0 100 100
   record           Begin recording
                      record capture overwrite
   save             Saves recorded data
                      save capture filename.wav
   set              Establish control settings
                      set capture channels 2
   status           Returns info on the current status of an MCI device
                      status cdaudio position
   stop             Halt playing or recording
                      stop capture
   sysinfo          Returns information about the MCI system
                      sysinfo cdaudio quantity open
   window           Controls the display window
                      window movie stretch

Informative Errors
The MCI includes built-in error messages via the mciGetErrorString API function. All you need to do is pass the error number to the API function and provide a buffer for the returned error information. Note that all information returned by the MCI API functions are null terminated strings. The CString procedure truncates the buffer beginning at the null terminator:

Public Function CString(aStr As String) As String
    Dim k As Long
    k = InStr(aStr, Chr$(0))
    If k Then
        CString = Left$(aStr, k - 1)
    Else
        CString = aStr
    End If
End Function
The sample application uses the custom MCIString function to actually execute all MCI commands.

Private Function MCIString(aCommand As String, _
Optional aRet As String = vbNullString, _
Optional bUpdateText As Boolean = True) As Boolean

    Dim lRet As Long
    lRet = mciSendString(aCommand, aRet, Len(aRet), 0)
    If lRet = MMSYSERR_NOERROR Then MCIString = True
    If Not bUpdateText Then Exit Function
    With txtStatus
        If lRet = 0 Then
            If Not bNoStatus Then
                If Len(.Text) Then .SelText = vbCrLf
                .SelText = aCommand & " successful"
            End If
        Else
            If Len(.Text) Then .SelText = vbCrLf
            .SelText = aCommand & " ERROR - "
            MCIError lRet
        End If
    End With
End Function
The procedure checks the return value and adds a "success" or "error" message to the status text box at the bottom of the form (see Figure 1).

Public Function MCIError(lErr As Long)
    Dim a As String, lRet As Long
    a = Space$(255)
    lRet = mciGetErrorString(lErr, a, 255)
    With Form1.txtStatus
        If Len(CString(a)) Then
            If Len(.Text) Then .SelText = vbCrLf
            .SelText = "   " & CString(a)
        End If
    End With
End Function


Figure 1 – The CD Player tab includes controls for rewinding, fast forwarding, previous track and next track A Timer updates the track and elapsed time information.

Determining Audio Capabilities
The MCI interface provides limited control over audio recording and playback, but there are other API functions you can use to change the volume, detect recording devices, or enumerate installed codecs (compression / decompression drivers). The Knowledge Base article Q178456 shows how to use the Mixer API functions to control the volume. It’s available online at http://support.microsoft.com/support/kb/articles/q178/4/56.asp.

The Form_Load event of the sample detects all available CD-ROM drives, retrieves all input device names, and enumerates installed codecs (see Code Listing). The first block of code calls the GetDriveType API function with each drive letter to discover CD-ROM drives. An option button is added to the CD Player tab indicating the drive. The drive letter is used as the device identifier for MCI CD audio commands.

The second block of code opens the default mixer device and retrieves the name of every mixer device that supports wave audio input. These devices will later be muted or unmuted depending on the Recording Source selection on the Audio Recording tab.

The final block of code only partially exists in the Form_Load event. The rest is in the modMCI.bas module (see Code Listing). The API functions used here are part of the Audio Control Manager (ACM) interface. All installed codecs are enumerated (listed) via the acmDriverEnum API function. The codecs are in turn opened (acmDriverOpen) and the available format tags for that driver are enumerated. A format tag is assigned by Microsoft to identify compression / decompression formats. The most common format is PCM (pulse-coded modulation). You may also see MPEG Layer 3, Indeo IAC2, or DSP TrueSpeech. The format tag is used with the set MCI command, but many codecs can only be used with the low-level audio interface (waveInOpen, waveInStart, etc…)

When a particular codec and format is selected, the NodeClick event of the TreeView control is fired. The program checks to see if the selected node is an actual codec format or just the driver name (see Code Listing). If the node is an actual format, it enumerates available audio quality settings using the acmFormatEnum API function (see Figure 2). This loads a ComboBox with available settings (such as 8,000 kHz, 8 Bit, Mono). Note that even if the MCI supports the codec, it might not support all quality settings. When loading the ComboBox with a setting, a global memory block is allocated to store the format information needed to specify that setting. The pointer to the memory block is stored in the ItemData property of each List item (see Code Listing). The memory blocks are freed before closing the application or modifying the ComboBox with a new codec.



Figure 2 – The Audio Recorder tab includes a TreeView control showing all installed recording codecs (compression / decompression drivers) and their recording quality capabilities. Not all codecs can be utilitized by the MCI recording commands.

Play that Funky Music
Actually playing CDs or audio/video clips is simple compared to determining the computer’s capabilities! To play anything, just open the file or device, play the device, and close it when you are finished.

	MCIString "open D: type cdaudio"	‘ open CD-ROM D:
	MCIString "play D:"	‘ play
	MCIString "close D:"	‘ stop and close
When you open a file, place an exclamation plus the filename after the device type. You should use short filenames because a space in the filename is invalid. Assign an "alias" to the opened device so it can be easily referenced throughout your code:

	MCIString "open sequencer!filename.mid alias sound"
	MCIString "open waveaudio!filename.wav alias sound"
	MCIString "open AVIvideo!filename.avi alias movie"
Note that since I specified a drive letter rather than a device type with the CD-ROM, I also had to define the device type. If you have only one CD-ROM attached to the computer, you can also use:

	MCIString "open cdaudio"
	MCIString "play cdaudio"
	MCIString "close cdaudio"
You can track the current play position using the status command. Each tab in the sample has its own Timer control that updates the relevant progress indicators. The ProgressBar indicators are set by retrieving the length of the file, CD track, or CD:

	MCIString "status sound length", a
and dividing that by the current position:

	MCIString "status sound position", b
	pbAStatus.Value = Int((Val(CString(b)) / Val(CString(a))) * 100)
The elapsed time indicators are updated by first setting the time format:

	MCIString "set sound time format milliseconds"
and then retrieving the current position and formatting it for output:

	MCIString "status sound position", a
j = Val(CString(a))
lblAStatus.Caption = Format(Int(j / 60000), "00") & ":" & _
    Format(Int((j Mod 60000) / 1000), "00")
Note that I’ve found that the MCI sometimes returns the position and length information incorrectly. The ProgressBar is updated as a percentage of the total length, but the milliseconds returned by the position and length status commands appear to be meaningless.

Another quirk I noticed was that the individual frames of a video clip don’t always fall exactly on the millisecond position indicated by the frames per second information returned by the status movie nominal frame rate command. The Video Player tab includes fast forward and rewind buttons that modify the play position based on the frame rate information (see Figure 3). Sometimes a single frame’s time in milliseconds is all that is needed to adjust the frame position, but it often takes two frame’s worth. The routine that moves a frame is therefore a loop that double checks the position:

Private Sub cmdAVIFF_Click()
    Dim a As String, b As String, c As String, k As Long
    a = Space$(64)
    c = a
    MCIString "status movie length", c, False
    MCIString "status movie position", a, False
    b = a
    k = Val(CString(a)) + lAVIFrameRate
    Do
        If tmr(4).Enabled Then
            MCIString "play movie from " & k
        Else
            MCIString "play movie from " & k & " to " & k
        End If
        MCIString "status movie position", a, False
        If a = b Then
            k = k + lAVIFrameRate
            If k > Val(CString(c)) Then Exit Do
        Else
            Exit Do
        End If
    Loop
    tmr_Timer 4
End Sub


Figure 3 – The Video Player tab includes a Frame that shows the video clip and options to define whether the video plays in the Frame or in a separate window. Forward and back buttons can step through the video one frame at a time.

Movies can be played either in a separate window created by the MCI or inside any VB object that has an hWnd property. The window command can set the position and size of the playback and handle defining the style of separate windows, including stretching, minimize and maximize buttons, and the window’s caption.

Recording a Wave
The main trick this program uses to record a wave is selecting an input device. You can also handle this manually by accessing the audio mixer and clicking the mute buttons of the recording sources. See Code Listing to learn how the mute controls are programatically selected and recording is started. Recording continues until the stop command is issued. The file is saved with the save command:

Private Sub cmdStopRecord_Click()
    cmdRecord.Enabled = True
    cmdStopRecord.Enabled = False
    cmdSyncStart.Enabled = True
    MCIString "stop capture"
    DoEvents
    tmr(3).Enabled = False
    Dim k As Long, aShort As String, a As String
    aShort = Space$(MAX_PATH)
    Open txtRecord.Text For Binary As 1
    Close 1
    k = GetShortPathName(txtRecord.Text, aShort, MAX_PATH)
    aShort = Left$(aShort, k)
    MCIString "save capture " & aShort
    MCIString "close capture"
    If bSync Then cmdCDStop_Click
    bSync = False
End Sub
Don’t issue the close command until after the wave file has been saved or it will be lost. Note that the file is opened for Binary then immediately closed. This is so that any long filename entered will be used even though the MCI requires short filenames.

General-purpose applications usually have little need for audio and video. However, adding simple video clips and audio cues to your application is easy when you know how to use the MCI.

Code listing for this article

Back to Articles
Copyright © 1999 by Matt E. Hart, All Rights Reserved Worldwide.
Nothing on this web site may be reproduced, in any form, without express written consent.