Using DLLs and
The Windows API
Understanding the Multimedia
Class
While
Visual Basic is great at detaching a
programmer from some of the underlying
complexities of Windows itself, there are
still certain areas in which the API just
can't be beaten, multimedia being one of
them. For example, the multimedia class
you just wrote uses just one API call, mciSendString. Before we take
an in-depth look at the code itself, it's
probably a good idea to look at this
particular API call in a little detail.
The Multimedia Control
Interface
Windows
itself actually consists of a number of
subsystems: separate units within Windows
that handle entire areas of
functionality. One such area is something
called the MCI. MCI stands for Multimedia
Control Interface, which provides a
device-independent way to use the
multimedia features of Windows through
code.
Device-independent?
OK! In the bad old days of DOS, a
programmer writing a video game, for
example, would have to cope with every
possible standard and type of sound and
video cards in order to satisfy the games
market. Device-independence, and the
device drivers that Windows provides, let
you hit any sound card, video card, and
so on with the same code, just so long as
it's supported by Windows. The MCI
provides this layer of independence,
putting a neat bunch of functionality
between you, the programmer, and the
devices which would normally be used to
handle multimedia data, namely the video
and sound cards.
All
this theory is great, but how exactly
does the MCI work, and how does it
provide this independence? The answer to
both questions is that the MCI is
responsible for talking to the Windows
device drivers, and ultimately the
multimedia hardware. You, the programmer,
issue commands to the MCI using the API
call mciSendString. These commands
are then translated into calls to the
appropriate Windows device driver,
something that you no longer have to
worry about. To put it into VB terms
then, the MCI is a built-in Windows
class, which we have essentially
super-classed in the earlier example.
Hang on
a minute-programmers issue commands to
the MCI. A little strange? Well, yes.
Normally, when you deal with API calls,
you are actually calling the subroutines
and functions embedded in Windows in
order to do something. The MCI really is
an independent object. It can be
programmed, and has its own programming
language. When you use mciSendString you are in fact
programming the MCI-just as easily as if
you were firing VB commands out of the
debug window.
Using mciSendString
The
format of mciSendString is quite simple
<ResultCode>
= mciSendString("<Command>",
<ReturnString>,
<ReturnLength>,
<CallbackHandle>)
This
needs a bit of explanation. The <ResultCode> is a long
integer, and varies depending on the
command issued. The <Command> part (notice it
is in quotes, so it's passed as a string
literal) is the command you're sending to
the MCI-such as Play to play a file,
Open to open one,
and so on. We'll look at exactly which
commands the MCI understands later on,
but for now let's cover the rest of the
parameters.
Some
MCI commands actually return a string
value to your program. The MCI Status command, for
example, can return a string telling your
code whether a file is Stopped, Paused, Playing and so on. The
string variable you place here will
contain that return string.
The API
call needs to know just how much data it
can put in this string variable, so the
next parameter passed is a number that is
the length of the string. For this
reason, if you are issuing a command to
the MCI which returns a string, you must
pass a fixed length string variable to
the call and tell the mciSendString just how long
that string is.
Dim
sReturnString As String * 255
Dim
nReturn As Long
nReturn
= mciSendString("status waveaudio
mode", sReturnString, 255, 0)
Don't
worry about what this specific command
does at this point, but notice the use of
the fixed length string. Adding the * 255 to the
declaration of sReturnString tells VB to fix
its length to 255 characters.
The
final parameter, the <CallbackHandle>, is something a
bit specialist, which we're not going to
be using in this book. However, to
satisfy your curiosity, here's a brief
description of what it means.
Callback Functions in Visual
Basic
Callback
functions really only used to apply
to those writing code in C, C++, Delphi,
or some other low-level compiled
language-and not Visual Basic. However,
as of version 5, Visual Basic now lets
you use callbacks in your code, without
requiring special add-ins that were the
only way to make it work in earlier
versions.
When
you use API subroutines and functions
normally, your code has no way of knowing
what's going on while that routine is
running. You have to wait until it ends,
then examine the return value. The
principle idea of a callback is that an
API function can call routines that are
inside your Visual Basic code,
while the API routine is running.
To do
this, you have to create a Public function inside
a VB code module, which has the correct
number and type of parameters-as expected
by the API routine. Then, when you call
the API routine, you send it a pointer
(the address in memory) of your VB
callback function. To do this, you make
use of the new Visual Basic AddressOf operator:
nResult
= SomeAPIFunction(ParamOne, ParamTwo,
AddressOf MyCallback)
As the
API routine executes, it will call your
VB function and send it the relevant
parameters. This is often used to update
status bars, get lists of system fonts,
and other varied tasks.
As I
said earlier, we won't be covering
callbacks in this book. They provide an
extra layer of complexity in your code,
and even more ways to crash your system.
However, the Visual Basic help files do
give some examples if you'd like to
experiment further.
Opening the Media File
Before
you can tell the MCI what to do, you have
to tell it which file you want to do it
on. This is like using disk files. You
must start by sending the Open command before
you can do anything else.
The
first part of this code should be self
explanatory: you tell the Open command the name
of the file that you want to open. This
is a standard filename like c:\video.avi.
Open
<filename>
Type <typestring>
Alias <aname>
...
...
'Issue
commands to do something to the file
...
...
Close
<aname>
After
the Type keyword, you
need to tell Windows what kind of file
you are dealing with. The standard
Windows ones are WaveAudio for WAV files, AVIVideo for AVI files and Sequencer for MID files.
Finally,
you can tell the MCI to give the file you
just opened a name, an Alias, which you will
use from now on to refer to the open
file. It's rather like naming a variable,
in that the name can be almost anything
you want. For example:
Open
c:\video.avi Type AVIVideo Alias Peter
If you
sent this to the MCI with MCISendString, it would tell
the MCI to open a file called c:\video.avi as a Microsoft
video file and that, in future MCI
commands, we'll refer to this file using
the name Peter.
Once
opened, normal MCI commands can be issued
using the alias to play the file, stop
it, pause it, find out its status and so
on. For example:
Play
Peter
Pause
Peter
Stop
Peter
There
are literally hundreds of combinations of
MCI commands that you can use, and we'll
take a look at the most common a little
later.
Once
you have done your stuff with a file, you
need to close it down by sending the Close command,
followed by the alias of the file you are
dealing with.
nReturn
= mciSendString("Close Peter",
"",0,0)
Let's take a
look at how the code does what it does
now, and see more of what our new class
can do.
Try It Out - Displaying
Status and Position for a Multimedia File
1 Open
the TestMM.vbp project we just
created if it isn't already open.
2 We're
going to add some controls to see how the
Status and Position properties of
our MMedia class can be
used. Add a ProgressBar, a Label, and a Timer control to your
form, so it looks like this:
If
you can't find the progress bar in your
toolbox, add it by selecting Components from the Project menu, and
check the Microsoft
Windows Common Controls 5.0 option box.
3 Open
the properties window for the Timer control, and
set its Enabled property to False, and its Interval property to 500. Clear the
caption for the label control at the same
time.
4
Double-click on the Load and play a
file
button to open the code window for its Click event. Add
these highlighted lines to the code:
Private
Sub Command1_Click()
...
'other
existing stuff here
...
If
CommonDialog1.Filename <>
"" Then
Multimedia.Wait
= False
Multimedia.mmOpen
CommonDialog1.Filename
ProgressBar1.Value
= 0
ProgressBar1.Max
= Multimedia.Length
Timer1.Enabled
= True
Multimedia.mmPlay
End If
End Sub
5 Back
in the form, double-click the Timer1 control to open
the code window for its Timer event. Add this
code to it:
Private
Sub Timer1_Timer()
ProgressBar1.Value
= Multimedia.Position
Label1
= "Status: " &
Multimedia.Status
If
ProgressBar1.Value = ProgressBar1.Max
Then
Multimedia.mmClose
Timer1.Enabled
= False
End
If
End Sub
As it
stands now, we have a minor problem. We
defined the variable that refers to
instance of our MMedia class within
the Command1_Click() event routine.
Now we want to refer to it from our Timer1_Timer() event routine
as well.
6 In
the Click event for the
command button, select the line that
declares the Multimedia variable, press
Ctrl-X to cut it to the clipboard.
This removes it from the Command1_Click() event routine.
Then select (General) in the top left
drop-down list in the code window, and
hit Ctrl-V to paste it into the General Declarations section, where
it will be available to all the code in
this form.
7 OK,
we're ready to go. Click the Load and play a
file
button, and select a multimedia file as
before. For a change this time, we've
chosen one of the 'Welcome to Windows 95'
videos from the Windows CD-ROM.
8 You
can see the progress bar now shows how
far through the file we are, and the
status-in this case playing. When the video
is finished, you'll see this change to stopped.
How It Works
We'll
use this example to show you in more
detail how some parts of the MMedia class work, and
how we've used them. Firstly, the code
that runs when you click the Load and play a
file
button.
We
added four lines to the routine you saw
in the earlier example. These are all
concerned with setting up the controls on
the form, and the properties of the Multimedia object, before
we start playing the file.
If
CommonDialog1.Filename <>
"" Then
Multimedia.Wait
= False
Multimedia.mmOpen
CommonDialog1.Filename
ProgressBar1.Value
= 0
ProgressBar1.Max
= Multimedia.Length
Timer1.Enabled
= True
Multimedia.mmPlay
End If
Our MMedia class, and
hence our Multimedia object, has a
property named Wait. This
determines if the code in our VB program
will continue to run (multitask) while
the file is playing, or just stop and
wait for it to finish, like it did in the
earlier example. The mmPlay method of the
class, which we will shortly use to start
the file playing, looks at the value of a
private variable in the class named bWait. If it's True, it includes
the command wait in the mciSendString call:
Public
Sub mmPlay() 'in the MMedia class
Dim
nReturn As Long
If
sAlias = "" Then Exit Sub
If
bWait Then
nReturn
= mciSendString("Play " &
sAlias & " Wait",
"", 0, 0)
Else
nReturn
= mciSendString("Play " &
sAlias, "", 0, 0)
End
If
End Sub
And how
does bWait get set to the
correct value? Remember from our
discussions on class modules, that we can
supply property routines which allow the
values of the internal variables to be
set and read just like a normal Visual
Basic control's properties:
Property
Get Wait() As Boolean
'Routine
to return the value of the object's wait
property.
Wait
= bWait
End
Property
Property
Let Wait(bWaitValue As Boolean)
'Routine
to set the value of the object's wait
property
bWait
= bWaitValue
End
Property
The
next step is to open the file we want to
play. To do this, we use the MMedia class' mmOpen method.
Opening the File
First
off, we declare a couple of local
variables to hold temporary values. We'll
see what these are for shortly. Then
there's a lot of code devoted to building
up the command string, before we send it
to the MCI.
Public
Sub mmOpen(ByVal sTheFile As String)
Dim
nReturn As Long
Dim
sType As String
If
sAlias <> "" Then
mmClose
End
If
Select
Case UCase$(Right$(sTheFile, 3))
Case
"WAV"
sType
= "Waveaudio"
Case
"AVI"
sType
= "AviVideo"
Case
"MID"
sType
= "Sequencer"
Case
Else
Exit
Sub
End
Select
sAlias
= Right$(sTheFile, 3) & Minute(Now)
If
InStr(sTheFile, " ") Then
sTheFile = Chr(34) & sTheFile &
Chr(34)
nReturn
= mciSendString("Open " &
sTheFile & " ALIAS " &
sAlias _
&
" TYPE " & sType &
" wait", "", 0, 0)
End
Sub
First
the mmOpen routine checks
a class level/module level variable
called sAlias.
If
sAlias <> "" Then
mmClose
End
If
Whenever
you deal with the MCI, it's a good idea
to assign aliases to each file you have
open. Here, the MMedia class works out
a name for the alias for you and stores
it in sAlias. When you next
go to open a file with mmOpen, or set the
file name property, the code can check
this and call another routine which
closes the first file down. Closing
multimedia files down when they are no
longer needed frees up memory and speeds
up playback, so it's always a good idea.
A
familiar Select Case construct is
then used to figure out the file type.
When you open a file with the MCI, you
need to tell it what type of data the
file holds. The Select Case construct here
does that, storing the MCI type name in
the sType variable
declared at the start of the routine.
Select
Case UCase$(Right$(sTheFile, 3))
Case
"WAV"
sType
= "Waveaudio"
Case
"AVI"
sType
= "AviVideo"
Case
"MID"
sType
= "Sequencer"
Case
Else
Exit
Sub
End
Select
At this
point, any previously opened file has
been closed, and the type name of the new
file has been stored in sType. All that
remains is to decide on an alias for the
file, and then open it. Aliases must be
unique, since the MCI can cope with
having more than one file open-and even
playing. Since we don't want to force
unnecessary complexity on the user of the
class, it makes sense if the class
decides on its own alias.
sAlias
= Right$(sTheFile, 3) & Minute(Now)
It does
this by taking the right three characters
of the file name and appending the
minutes part of the current system time.
So if it was 16:15 when you opened c:\video.avi, then the alias
the class would come up with would be AVI15. The newly
calculated alias is then stored in the sAlias module level
variable that we checked when the
procedure first started. The value in sAlias is then used
throughout the rest of the module
whenever we need to do something to the
open file-like play it.
So,
armed with the file type in one variable,
the alias in a second, and the file name
in a third, we can finally send the Open command to the
MCI. The only fly in the ointment is the
need to surround file names which contain
spaces with quotation marks (") first:
If
InStr(sTheFile, " ") Then
sTheFile = Chr(34) & sTheFile &
Chr(34)
nReturn
= mciSendString("Open " &
sTheFile & " ALIAS " &
sAlias & " TYPE " &
sType & " wait",
"", 0, 0)
The
command sent is Open, followed by
the file name, followed by ALIAS then the new
alias (held in sAlias), followed by TYPE and the type,
and finally, the word wait.
The wait statement on
the end here tells the API not to let our
VB code continue running until it has
finished loading the file. Without this,
on a fast machine with a slow hard disk,
problems can occur-you might try to play
the file before it has loaded, simply
because the code is running a lot faster
than the hard disk. Notice that this
isn't the same as the Wait property that
we looked at earlier, which controls
whether our program continues to run
while the file is playing, and not
while it's loading.
So,
now, we can set up the progress bar ready
to show the progress of the file's
playback. But we need to know how long it
is, so that we can set the progress bar's
Max value. And of
course, we'll need to know the relative
position in the file at regular intervals
so that we can update it while the file
is playing. To recap, here's the Command1_Click event again. So
far we've only managed to open the file
that we want to play:
If
CommonDialog1.Filename <>
"" Then
Multimedia.Wait
= False
Multimedia.mmOpen
CommonDialog1.Filename
ProgressBar1.Value
= 0
ProgressBar1.Max
= Multimedia.Length
Timer1.Enabled
= True
Multimedia.mmPlay
End If
The
highlighted code above is the bit that
looks after setting up the progress bar.
We set it's current Value to 0, which resets
it from the last time it was used (i.e.
if another file has been played
previously). Then we set its Max value to length
of the file we've just opened, using the Length property of our
Multimedia object.
Getting the File Length
We can
use mciSendString to return
values, as well as to set them.
The Length property of our
MMedia class is
read-only, because we haven't provided a Property Let routine. We
wouldn't expect to be able to set the
length of the file anyway. However, we do
have a Property Get which returns
the length of the currently open file:
Property
Get Length() As Single
Dim
nReturn As Long, nLength As Integer
Dim
sLength As String * 255
If
sAlias = "" Then
Length
= 0
Exit
Property
End
If
nReturn
= mciSendString("Status " &
sAlias & " Length",
sLength, 255, 0)
nLength
= InStr(sLength, Chr$(0))
Length
= Val(Left$(sLength, nLength - 1))
End
Property
First sAlias is checked to
see if a file has been opened. If it
hasn't, then the value returned from the
property procedure is 0. If a file has
been opened, then the MCI Status Length command is used
to find out how long it is. You needn't
worry about how the length of the file is
measured, since any unit of measurement
will be suitable for setting up the
progress bar. Just for your interest,
though, you can set the unit of
measurement to various values, ranging
from frames in a video clip to
milliseconds in a sound file. However, a
complete rundown of the options is a
little beyond the scope of what we are
trying to achieve here.
The Status command is a
rather special MCI command and can be
used in conjunction with keywords like Length, Position, and Mode to find out a
great deal of information about the
current file. It returns this information
in a fixed length string variable which
is passed to mciSendString after the MCI
command. In this example, the return
string is called sLength and is declared
to be 255 characters
long.
Of
course, you're not always going to get 255 characters back
from the Status command,
especially if you only want to know the
length of the file. The unused space in
the string is filled with the character 0, making it easy
for us to use VB's InStr function to
find out exactly how long the returned
data is, and pull it out.
nReturn
= mciSendString("Status " &
sAlias & " Length",
sLength, 255, 0)
nLength
= InStr(sLength, Chr$(0))
Length
= Val(Left$(sLength, nLength - 1))
Here,
the position of the first Chr(0) is stored in nLength, since InStr returns the
character position at which the search
data is located, or 0 if it can't
find what you are looking for. This now
gives us enough information to pull the
characters from the left side of the
fixed length string, convert them to a
number (rather than a string) and return
that value ready to go into the Length property.
Getting the Current Position
When
the file is actually playing, the Status
Position command can be used repeatedly
to find out exactly where in the file
playback has reached. The code to return
the value of the Position property looks
strangely familiar:
Property
Get Position() As Single
Dim
nReturn As Integer, nLength As Integer
Dim
sPosition As String * 255
If
sAlias = "" Then Exit Property
nReturn
= mciSendString("Status " &
sAlias & " Position",
sPosition, _
255,
0)
nLength
= InStr(sPosition, Chr$(0))
Position
= Val(Left$(sPosition, nLength - 1))
End
Property
The
only real difference this time is that
instead of sending Status Length to the MCI, we
are sending Status Position.
Getting the Current Status
To get
the text message for the status,
sometimes called the mode, we
query the class' Status property. This
is done using another Property Get routine, almost
identical to the Position property we've
just seen. The only differences are that
we send the command Status Mode instead of Status Length or Status Position to the mciSendString function. And,
of course, we don't need to convert the
result to a number, because it's supposed
to be a text string:
...
nReturn
= mciSendString("Status " &
sAlias & " Mode", sStatus,
255, 0)
nLength
= InStr(sStatus, Chr$(0))
Status
= Left$(sStatus, nLength - 1)
...
OK,
let's go back to that Command1_Click event again,
and see where we are. So far, we've set
the Wait property,
opened the file, and set up the progress
bar. Before we actually start playing the
file, we'll set our Timer control
ticking. You'll see what it does in just
a moment. Then, finally, we'll play the
file by calling the mmPlay method of our Multimedia object:
If
CommonDialog1.Filename <>
"" Then
Multimedia.Wait
= False
Multimedia.mmOpen
CommonDialog1.Filename
ProgressBar1.Value
= 0
ProgressBar1.Max
= Multimedia.Length
Timer1.Enabled
= True
Multimedia.mmPlay
End If
Starting the File Playing
Here,
again, is the complete code for the mmPlay method of our MMedia class. To
execute it, we just need to call it in
our Multimedia object. It
first checks to see we have a file open
by examining the sAlias variable, then
if all is well it executes the MCI Play command. We've
already seen the way we set and use the Wait property, which
controls whether the command is actually Play Wait or just Play.
Public
Sub mmPlay() 'in the MMedia class
Dim
nReturn As Long
If
sAlias = "" Then Exit Sub
If
bWait Then
nReturn
= mciSendString("Play " &
sAlias & " Wait",
"", 0, 0)
Else
nReturn
= mciSendString("Play " &
sAlias, "", 0, 0)
End
If
End
Sub
Updating the Progress Bar and
Label Controls
Our
final task is to update the progress bar
and the label on the form, as the file is
playing. We've already covered all the
stuff we need to do this with. Before we
set the file playing, we enabled a Timer control with an
Interval of 500. So it will
fire every half a second. Each time it
does, the code in the Timer1_Timer() event routine
runs:
Private
Sub Timer1_Timer()
ProgressBar1.Value
= Multimedia.Position
Label1
= "Status: " &
Multimedia.Status
If
ProgressBar1.Value = ProgressBar1.Max
Then
Multimedia.mmClose
Timer1.Enabled
= False
End
If
End
Sub
You're
probably well ahead of me by now. All
this code does is take the value of the Multimedia object's Position property and
assign it to the ProgressBar control's Value property,
updating the display on the form. Then it
gets the Multimedia object's
current Status property, which
is a text string, and pops it into the Label1 control on the
form. Remember, this is happening twice a
second.
Simply
retrieving these properties, of course,
runs the Property Get routines we
wrote in the class, which in turn use the
MCI Status
Position and Status Mode commands each
time. The only other thing is to stop it
all happening when we get to the end of
the file. This is done by comparing the
progress bar's current and maximum
values. When they're the same, it's time
to close the file using the Multimedia object's mmClose method, then
disable the timer to prevent this routine
running again until another file is
opened.
Our MCI Commands Summary
To end, here's a list of the MCI
commands used in our MMedia class:
Command
|
Description
|
Play
|
Plays a
file.
|
Pause
|
Pauses
playback, ready to start up again
at any time.
|
Stop
|
Stops a
file-you need to seek to a
position to continue playback.
|
Seek
|
Followed
by a number, seeks to that
position in the file.
|
Status
Mode
|
Returns
a string indicating what the file
is doing (i.e. Stopped, Paused, Playing, Ready).
|
Status
Position
|
Returns
a number indicating the position
through the file that playback
has reached.
|
Status
Length
|
Returns
the length of the file and helps
to put the number returned from Status
Position into
some meaningful context.
|
Close
|
Closes
the file and frees up the memory
it previously occupied.
|
The MCI
supports a few more commands than this,
and also a number of specialized ones for
each file format. What I hope to have
shown you is how even apparently complex
topics, such as multimedia, really aren't
that hard if you drop down to the API.
Also, that the API itself is not the
daunting mother of all nightmares that
many VB programmers make it out to be.
Summary
This
chapter should have provided you with a
glimpse of the power that lies beyond the
strict limits of Visual Basic, as defined
in the language itself. This is a huge
subject, and so what we've done is touch
on the main principles of using the
Windows API. With power comes
responsibility, because once you choose
to operate outside the confines of Visual
Basic, you have to look after yourself.
It pays to develop a deep understanding
of all the components of Windows, so you
can write Visual Basic programs that are
truly well-behaved members of the Windows
desktop community.
In our
brief tour we covered:
How Windows and
Visual Basic fit together
The Windows API
How to use the
Windows API to extend the power of Visual
Basic
How to write
well-behaved Windows applications
Page [<<
PREV] 1 2
3
4
Back to Tutorials
- Main
|