Access to Movies: Adding Multimedia with Access and Visual Basic

By John Selby and Breccia Berryhill

Sound and video can display data in a way no ordinary form, chart, or report can match. Audio/video interleave (.AVI) files can be used in a project to demonstrate the functionality of a product, show a 360-degree rotation of a part, or show a video representation of the meaning of the data returned in a report. Music or a human voice from a .WAV file can also dramatically convert cold, hard data into concise, clear, meaningful information. This is the great advantage of multimedia; not adding bells and whistles to a project, but making a custom application more user-friendly — and more impressive to potential customers.

Access 97 is a wonderful development tool, although some developers find it lacks the multimedia capabilities today's market demands. They are partially correct; Access itself lacks the ability to play .WAV and .AVI files, but Windows 95 and Windows NT provide this functionality via their Application Programming Interfaces (APIs). Although API programming has been referred to as the black art of Visual Basic programming, we will try to squelch some of that fear with a little understanding.

To accomplish this, we'll use the Windows API functions and Sub procedures declared in a module of a small Access 97 application. We will show the steps, statement-by-statement, that play, pause, resume, and stop .AVI files, and play .WAV files. The concept is simple; include the module in your project, and invoke its Sub procedures to play sound and audio/video clips. The .AVI and .WAV files will play asynchronously, i.e. they will run independently, and the end-user is free to use the application without waiting for them to finish playing.

Playing an .AVI

An .AVI file consists of a series of still frames displayed in sequence, like a celluloid filmstrip. The audio is interleaved like a sound track on the filmstrip, so the sound plays in sync with the video. The video frames of each .AVI have a specific height and width, but all .AVI files are not created equal. We will account for this difference in size with the PlayAVI Sub procedure's Auto argument.

Framing the .AVI.

FIGURE 1 demonstrates how to play an .AVI audio/video clip (within a user-defined display area) on Form1. In this example (see FIGURE 2), the display area is represented by the Top, Left, Height, and Width properties of a Rectangle control named Box1. We first set the focus to Text1, which contains the path and filename of the .AVI to be played.

FIGURE 1: Playing an .AVI file on Form1.

Private Sub Command3_Click()
   
  Me!Text1.SetFocus
  PlayAVI Me!Text1.Text, _
    Forms![Form1]![Box1].Top, _
    Forms![Form1]![Box1].Height, _
    Forms![Form1]![Box1].Left, _
    Forms![Form1]![Box1].Width, True

  ' Activate the Timer and set the interval to 1 second.
  Me.TimerInterval = 1000
   
End Sub

FIGURE 2: The Basket.avi playing in Auto mode (centered within the display area).

The PlayAVI Sub procedure, invoked from the Command3_Click event, requires the following six arguments:

  1. the name of the file to be played

  2. the location of the top of the display area on your form, in twips (A twip is a printer term meaning 1/20 of a point, which is 1/1440 of an inch.)

  3. the height of the display area, in twips

  4. the location of the left edge of the display area, in twips

  5. the width of the display area, in twips

  6. the playback mode (True = Auto, False = Manual)

The path and filename, in this example, are provided by text1.text. The twip values for the display area are provided by the Top, Height, Left, and Width properties of Box1. Box1 is used for appearance only, and if you don't want a background screen, you can still use a Rectangle control temporarily as a tool to obtain the desired twip parameters for your display area. The last argument is the display mode (Auto). This argument determines how the Sub procedure will display, manipulate, and/or resize the .AVI, based on the size of the display area.

A value of True will center the .AVI within the display area, if it will fit. If it won't fit, it's proportionally resized to fit the display area, then centered. A value of False will non-proportionally resize the .AVI to fit the display area, regardless of its natural size.

Setting the timer.

Form1's timer is used to stop the .AVI after it has finished playing. The third statement in FIGURE 1 starts Form1's timer by changing its TimerInterval property from 0 (stopped) to 1000:

Me.TimerInterval = 1000

A timer interval of 1000 will invoke a timer event every second. The Form_Timer code will then check the status of the .AVI every second (see FIGURE 3). The statement:

vid = AVIFunction("status TheVideo mode", TheMode, 20, 0)

will store the .AVI status mode (playing, seeking, stopped, paused, or not ready) in the string variable, TheMode. When the timer detects that the .AVI has stopped, the following code will turn the timer off, and invoke Module1's StopAVI Sub procedure, which closes the .AVI window:

If TheMode = "stopped" Then
  StopAVI
  Me.TimerInterval = 0
End If

If the .AVI was not stopped by the timer event, the last frame of the .AVI will remain on the screen. The user will not be able to play another .AVI until the current .AVI is stopped.

FIGURE 3: Setting the timer interval to 1000 checks the status of the .AVI every second.

Private Sub Form_Timer()

  Dim TheMode As String * 20

  vid = AVIFunction("status TheVideo mode", TheMode, 20, 0)
  If TheMode = "stopped" Then
    StopAVI
    Me.TimerInterval = 0
  End If
   
End Sub

Pause, resume, stop. Pausing, resuming, and stopping an .AVI file are far more straightforward than playing the .AVI. Command4_Click (see FIGURE 4) invokes Module1's PauseAVI or ResumeAVI Sub procedures to pause or resume the .AVI, based on the current caption of Command4. The If statement acrobatically sets the caption of Command4 to indicate the function it will perform.

FIGURE 4: The Command4 Sub procedure pauses or resumes an .AVI.

Private Sub Command4_Click()

  If Command2.Caption = "Command2 Pause" Then
    ' Pause.
    Command2.Caption = "Command2 Resume"
    PauseAVI
  Else
    ' Resume.
    Command2.Caption = "Command2 Pause"
    Me!Text1.SetFocus
    ResumeAVI
  End If

End Sub

Private Sub Command5_Click()
  StopAVI
  Me.TimerInterval = 0
  Me!Text1.SetFocus
End Sub

Stopping the .AVI is simpler yet. The Command5_Click event invokes the StopAVI Sub procedure and turns the timer off. Resetting the timer is best accomplished here, instead of from the StopAVI Sub procedure, because Module1 has no way of knowing which form is playing the .AVI. This allows Module1 to be used as a component by many forms and projects.

Don't forget the sound.

FIGURE 5 demonstrates how to play and stop .WAV files. The PlaySound Sub procedure requires only one string argument, which identifies the path and filename of the .WAV to be played. The .WAV file is stopped by invoking the StopSound Sub procedure.

FIGURE 5: Playing and stopping .WAV files is simple.

Private Sub Command1_Click()
  PlaySound "d:\stuf\InTheJungle.wav"
  Me!Text1.SetFocus
End Sub

Private Sub Command2_Click()
  StopSound
  Me!Text1.SetFocus
End Sub

The Things That Sub Procedures Do

Listing One (beginning on page XX) contains Module1's general declarations, and the PlayAVI Sub procedure. Module1 also consists of the PauseAVI, ResumeAVI, and StopAVI Sub procedures listed in FIGURE 6, and the PlaySound and StopSound Sub procedures listed in FIGURE 7.

FIGURE 6: The PauseAVI, ResumeAVI, and StopAVI Sub procedures.

Sub PauseAVI()
  vid = AVIFunction("pause TheVideo", 0&, 0, 0)
End Sub

Sub ResumeAVI()
  vid = AVIFunction("resume TheVideo", 0&, 0, 0)
End Sub

Sub StopAVI()
  vid = AVIFunction("close TheVideo", 0&, 0, 0)
End Sub

FIGURE 7: The PlaySound and StopSound Sub procedures.

Sub PlaySound(WAVFile As String)

  Dim CmdStr As String
   
  StopAVI    ' Stop any playing AVI.
  StopSound  ' Stop any playing .WAV.
   
  ' Open the waveaudio device with WAVFile.
  CmdStr = ("open " & WAVFile & " type waveaudio alias TheWAV")
  vid = AVIFunction(CmdStr, 0&, 0, 0)

  ' Play WAVFile.
  vid = AVIFunction("play TheWAV", 0&, 0, 0)
   
End Sub

Sub StopSound()

  ' Stop any playing .WAV.
  vid = AVIFunction("close TheWAV", 0&, 0, 0)

End Sub

The PlayAVI Sub procedure does quite a bit of work to play the .AVI. First, it provides the multimedia control interface (MCI) device with the initial information (filename, file type, an alias, and window type). It then determines the size of the .AVI (which is returned in pixels). The hardware-independent size (in pixels-per-inch) is then determined, so the .AVI size can be compared to the display area which Access returns in twips. Then, the .AVI size (in pixels) is compared to the display area size (in twips), and the .AVI size is manipulated and positioned according to the PlayAVI Auto argument. Finally, the .AVI is played.

Initializing the MCI.

The mystery begins with the statement:

FRMHwnd = Screen.ActiveForm.hWnd

The window handle for the current active form is loaded into the long variable FRMHwind. A window handle is the identification that Windows uses to refer to each loaded window. Next, CmdStr is loaded with the command string required to initialize the MCI device. The CmdStr contains the Open command, the path and name of the .AVI file, the alias (TheVideo) that AVIFunction will use, the Windows handle of the object (Form1) that the .AVI window will be placed on, and style of window to open. In the following statement, the AVIFunction uses the CmdStr variable to initialize the MCI device:

vid = AVIFunction(CmdStr, 0&, 0, 0)

In case of error. It doesn't play the video yet. If an error occurs, the code:

If vid <> 0 Then  ' An error occurred.
  vid = GetAVIError(vid, TheError, Len(TheError))
  MsgBox TheError, vbOKOnly + vbCritical, "AVI Error"
End If

displays a message box. The text of the error is stored in the 100-character string variable, TheError, by the GetAVIError function.

Sizing It Up

The following determines the size of the .AVI in pixels:

vid = AVIFunction("status TheVideo window handle", _
                  TheAVIHwnd, Len(TheAVIHwnd), 0)
vid = GetAVISize(TheAVIHwnd, TheAVIRect)
AVISizeH = TheAVIRect.Bottom - TheAVIRect.Top
AVISizeW = TheAVIRect.Right - TheAVIRect.Left

The first statement uses the status window handle commands and the TheVideo alias to store the .AVI window's handle in the 100-character string variable, TheAVIHwind. This variable will be used by the GetAVISize function to determine the current size of the .AVI's window. GetAVISize uses the GetClientRect function of User32.dll to return the Left, Top, Right, and Bottom (in pixels) of the .AVI's window to the RECT variable, TheAVIRect. A RECT is a Windows data structure with four long integers values for Left, Top, Right, and Bottom. The last two statements do the math to return the .AVI height and width to AVISizeH, and AVISizeW, respectively.

Now, these statements:

hDC = GetResSpec(FRMHwnd)  
PixPerInX = GetPixPerIn(hDC, 88)  
PixPerInY = GetPixPerIn(hDC, 90)  
vid = RelResSpec(FRMHwnd, hDC)

find the pixels per inch, horizontally and vertically, from the form that has received the .AVI's window. These values will be used to compare the .AVI size to the display area, measured in twips. In the first statement, GetResSpec gets the handle of the device context of Form1, and returns it to the long variable, hDC.

A device context is a Windows object, which, in this case, is the display. The second and third statements use the Gdi.dll's GetDeviceCaps function to get the device capabilities of the Windows object, which is identified by the hDC returned in the first statement. In this case, the pixels per inch (horizontally and vertically) are returned. Finally, the last statement releases the Windows object (the display), so that it can be used programmatically by other applications.

The If statement in FIGURE 8 will manipulate the .AVI's size and its position within the display area, based on the Auto property. If Auto is True, the first twelve statements will proportionally resize the .AVI, if it's larger than the display area, and center it. This is done by proportionally shrinking the height and width of the .AVI, if it's wider than the display area. These statements will also proportionally shrink the .AVI again if its height is still greater than the display area's height.

FIGURE 8: This If statement resizes and positions the .AVI.

If Auto = True Then
  ' Proportionally resize .AVI if necessary and center in
  ' display area.
  If AVISizeW > ((ScreenWidth / 1440) * PixPerInY) Then
    AVISizeH = (((ScreenWidth / 1440) * PixPerInY) / _
                AVISizeW) * AVISizeH
    AVISizeW = ((ScreenWidth / 1440) * PixPerInY)
  End If
   
  If AVISizeH > ((ScreenHeight / 1440) * PixPerInX) Then
    AVISizeW = (((ScreenHeight / 1440) * PixPerInX) / _
                AVISizeH) * AVISizeW
    AVISizeH = ((ScreenHeight / 1440) * PixPerInX)
  End If

  ScreenTop = Int(((ScreenTop + (ScreenHeight / 2)) / _
                  1440 * PixPerInY) - (AVISizeH / 2))
  ScreenLeft = Int(((ScreenLeft + (ScreenWidth / 2)) / _
                   1440 * PixPerInX) - (AVISizeW / 2))

Else  ' Auto = False
   
  ' Non-proportionally resize .AVI to fit the display area.
  AVISizeW = Int((ScreenWidth / 1440) * PixPerInX)
  AVISizeH = Int((ScreenHeight / 1440) * PixPerInY)
  ScreenTop = Int((ScreenTop / 1440) * PixPerInY)
  ScreenLeft = Int((ScreenLeft / 1440) * PixPerInX)
      
End If

ScreenWidth and ScreenHeight are arguments of the PlayAVI Sub procedure, in twips. They are divided by 1440 — to convert them to inches — to compare them to PixPerInX and PixPerInY in common units of measure. The AVISizeH and AVISizeW will still be returned in pixels. Now that the .AVI has been resized as required, the top and left values of the display area will be reset, so the .AVI will actually display centered within the user-defined display area. In these calculations, the ScreenTop and ScreenLeft are divided by 1440 to convert them to pixels. The AVIFunction requires that all values be in screen coordinates or pixels. If Auto is False, the last six statements will simply resize the .AVI to fit the display area, and convert the top and left properties of the display area to pixels.

The window.

The MCI device has been initialized, but the .AVI window hasn't yet been created. This is done with AVIFunction:

vid = AVIFunction("put TheVideo window at" & " " & _
                  " " & ScreenLeft & " " & ScreenTop & _
                  " " & AVISizeW & " " & AVISizeH, _
                  0&, 0, 0)

AVIFunction uses Windows screen coordinates (in pixels) to place the .AVI window (within the display area) in the client area of Form1. The client area is the area within a window's frame. There is one thing to be aware of: In this project (again, see FIGURE 2), Form1 doesn't display a record selector. In this case, the top-left corner of Form1's detail is coordinate 0, 0 in twips. If Form1 did have a record selector, coordinate 0, 0 of the detail would be shifted to the right by the width of the record selector. When the twip coordinates of the display area on Form1's detail was converted to pixels, the width of the record selector would not be accounted for, and the .AVI window would be shifted to the left by the width of the record selector. If your project requires a record selector, its width would have to be accounted for here. You could also set the form's record selector property to False when the .AVI plays, and set it back to True when the it stops, but the simplest option is simply to not display the record selector. You should also be aware that even if the form has scroll bars, the .AVI window will not scroll. It will be permanently displayed relative to the client area of the form, so it is advisable to design your form so that it doesn't have scroll bars.

The Sound

The statement:

StopSound

invokes the StopSound Sub procedure to stop any .WAV files that might be playing. If a .WAV file is allowed to continue playing, the .AVI would have no sound. The MCI device can only play one sound track at a time, and it's "first come, first served." This would also be the case if a second .AVI were played. The video would de displayed, but the first .AVI's sound would continue to play.

Multiple .AVIs.

In this project, the MCI alias, TheVideo, is hard-coded into Module1. To play multiple .AVI's, each one must have its own alias. If you need to play more than one .AVI, you can include an additional PlayAVI string argument that would specify a unique alias for each .AVI. If you then replace TheVideo with the name of your new argument variable, you could play multiple .AVI videos (but only one sound track).

At last, the show.

Finally, the last statement:

vid = AVIFunction("play TheVideo", 0&, 0, 0)

plays the video.

FIGURE 6 contains the PauseAVI, ResumeAVI, and StopAVI Sub procedures, and much like the code that calls these Sub procedures, they are exponentially simpler than the Sub procedure that plays the .AVI. In each case, the AVIFunction requires only a simple command string to invoke the functionality and the other parameters are set to zero. None of these Sub procedures have any arguments.

Catching .WAVs

Playing and stopping .WAV files is similar, but much simpler than playing .AVI files (see FIGURE 7). As in PlayAVI, PlaySound first stops any playing .AVI's (both audio and video). If an .AVI was playing a sound track, no .WAV can use the MCI audio device. Any playing .AVI will be stopped by the StopAVI Sub procedure invoked on the second statement. Similarly, the third statement stops any currently playing .WAV with the StopWAV Sub procedure, so that the new .WAV can be played.

To play the .WAV, the MCI device is initialized with a command string containing the path and filename of the .WAV, the type of MCI device, and the .WAV alias, TheWAV. The following statement then plays the .WAV:

vid = AVIFunction("play TheWAV", 0&, 0, 0)

Stopping a .WAV is much like stopping an .AVI; it's accomplished by the AVIFunction of the StopSound Sub procedure.

Conclusion

Now, having reached the end of a long and arduous journey to play .AVI and .WAV files, developers will be able use them in many clever ways. Calling the Windows API and other .DLLs can provide a great deal of functionality that would not otherwise be possible with Access or Visual Basic. So, as you're developing a new application or updating an old one, remember these words: Lights, camera — Access.

Download source files for this article here.

John Selby is a three-year veteran of VB/Access development and Senior Freelance Developer for NorthBound Enterprises. His recent history is in technical support, but he came from the field, both technically and literally. He is an honorably discharged Marine Corporal. That's where he cultivated his talent for finding clever ways of using available resources, which is his definition of programming. He can be reached via e-mail at jselby@bellsouth.net.

Breccia Berryhill is a self-taught Access/VB developer, specializing in custom-made applications for the small to mid-range business. She comes from an administrative background, where she developed a yearning for the easy-to-use, easy-to-understand program. Being unable to find that, she decided to write her own. She is currently heading the training program for Digital's Desktop Application group, and is the head developer for Diamond-Edge Enterprises. Her e-mail address is breccia@bellsouth.net.