The VB-TAPI Answering Machine application is written, as you could probably guess by
the name, in Microsoft's Visual Basic 6.0. This is by no means
a commericial grade application, or even a look at good
programming principles. There was no plan and it shows throughout
the code, but that is okay. It works and accomplished its goal
of getting me more familiar with the 1.x 2.x TAPI versions.
This uses TAPI 1.4, as no features in 2.x are needed
for the application. I would have preferred to use 3.0
but that is another project so from here on out
when you read TAPI think 1.x or 2.x, not 3.0. Mostly the
source shows how to call Win32 API's from Visual Basic. Specifically:
-
TAPI Tapi32.dll
-
MultiMedia winmm.dll
-
Memory kernel32.dll
This program is written as an exercise and is not intended for
anything. You may freely use and modify the source code contained in
this product for use in your own applications including commercial
applications. However, you may not redistribute any part of this
archive or in any way derive financial gain from this demo product
without the express permission of it's author – Steven A. Frare.
Please do NOT redistribute this archive,
instead refer people to my website, where the latest version can be
downloaded free! http://www.geocities.com/sfrare
No claim whatsoever is made concerning the suitability of this
source code product for any particular task. Anyone who decides to use
code from this sample does so at their own risk and agrees not to hold
the author or anyone connected with this sample responsible for it's
success or failure.
Copyright (C) 2000, Steven A. Frare all rights reserved
If you aren't used to calling the Win32 API's
from VB you might want to check out http://msdn.microsoft.com
first. Specifically the Microsoft Knowledge Base Article Q190000
- HOWTO: Get Started Programming With the Windows API
(LONG).
The application is standalone, no .dll's or
setup program. It simply requires itself (AnsMach.exe) a
greeting file in its program directory named Greeting.wav and it will
create a subdirectory called Messages to hold the recordings.
The first thing that happens is frmMain is loaded and it checks to see if there
are any messages in the .\Messages directory. If so it just
displays the number of them in lblMsgCount
in frmMain. Information to run
the application is retrieved through dlgSetup
which contains a
few trivial items (such as which voice card to use) and stores/retrieves them from:
HKEY_CURRENT_USER\Software\VB and VBA Program
Settings\VB-TAPI\Settings
To use TAPI we need to use a line, so we have
to find one. In TAPI you do things in the following order to get that line:
- The application calls lineInitialize/Ex and
iterates through the line devices.
- It negotiates the version numbers for all
of the lines in the application with lineNegotiateAPIVersion.
- It determines the line capabilities with
lineGetDevCaps.
- It determines the address capabilities with
lineGetAddressCaps.
- It looks for appropriate media and bearer
modes with lineGetCallInfo.
- It may also look for LINEDEVCAPS.dwPermanentLineID.
This application does not use steps 5 or 6 for
initialization, though lineGetCallInfo is called for CallerID
information. We also skip step 4 for reasons outlined below.
We don't want to simply find a line that
matches the capabilities we are after since the user may want to use a
different line. Since the sample does not have a setup program
we run into a Chicken and Egg problem right off the bat. If you
look in frmMain.myInit you will see that it
takes a flag to signal whether it is being called by the setup dialog
or if the app. is trying to run. If it is being called by the
setup dialog then it will iterate through all the lines as in the
steps outlined above and try to get any of them to negotiate a 1.4
TAPI version. If it is successful then the rest of the code in
dlgSetup can enumerate the lines and present
a choice to the user. The choices in dlgSetup
don't indicate whether the line will actually work or not since we skipped
step 4 from above. Why skip it? For me it is frustrating
to have applications try to be smarter than I am. This is
an experimental program to be able to work with the TAPI API,
and I would prefer that the application not dictate which line to
use. It will throw an error if I try to open
a line that isn't capable. Keep reading for why this technique will probably fail
the test of time and I will be forced into making the
application choose which voice card to use, or at least provide a more narrowed down list.
By capable I mean the following code used in
frmMain.myInit
nError = lineOpen(hTAPI, lLineID, hLine, lNegVer, lUnused, lUnused, _
LINECALLPRIVILEGE_OWNER,LINEMEDIAMODE_AUTOMATEDVOICE,0)
So we try to open a
line with lNegVer, our negotiated TAPI
version of 1.4 and with LINEMEDIAMODE_AUTOMATEDVOICE which is a TAPI
constant for a voice modem. If we can't open the line with those
contraints, we deal with it here. We also ask for owner
privilege of the line for the following reason from the Platform
SDK:
LINECALLPRIVILEGE_OWNER The application has
owner privileges to the call. These privileges allow the application
to manipulate the call in ways that affect the state of the call.
Since the application
answers and possibly hangs up on calls it will definitely need
the ability to affect the state of the call.
Assuming we were able
to enumerate the lines, choose and open a suitable one we are pretty
much in business. That is a pretty big assumption and I think a
bunch of bugs may be caused by the way I chose to implement this so I
will update this page if/when I make changes on how that is
done. By not restricting the modem list to capable only modems I
leave the application open to modems that aren't setup correctly,
which will of course get blamed on the application...
At any rate we then
call lineSetStatusMessages so the
application receives notifications, such as a call coming
in. We got the line states the modem can deal with just before
in the lineGetDevCaps call. Now the
application is fat, dumb and happy waiting for the phone to ring, we
disable all the buttons for the other program functions so we don't
have to deal with so many state possibilities. When we
initialized TAPI we gave it a callback address to one of our functions
so that when something interesting happens on the line we are
monitoring it could let us know. That is LineCallBack and is in Globals.bas.
TAPI sends all call state
to the application through this callback. Anything we are interested in we
either drill down on to get more info, or act on immediately.
The first thing we are interested in is a LINE_CALLSTATE message, which we then go to our
LineCallStateProc sub and if the message is
LINECALLSTATE_OFFERING we have a new call
coming in. The dwDevice parameter is the handle to the new call,
we store it in the global variable hCall.
Now that we have a
call coming in we expect a few more messages in the LineCallBack procedure. A LINE_CALLINFO message with the LINECALLINFOSTATE_CALLERID if we get CallerID
information on the line and LINE_LINEDEVSTATE
of which we drill down on through our LineDevStateProc to possibly find the only LINEDEVSTATE message we are interested in, LINEDEVSTATE_RINGING.
You will probably only
get the LINECALLINFOSTATE_CALLERID message
if you subscribe to CallerID service on your phone line. Even
then you may not get useful information since much of the time the
call information is Blocked by the caller or the caller is Out Of
Area, there are plenty of reasons not to get good information as
defined in the LINECALLPARTYID_ Constants which are in
VB_TAPI.bas. In the US the CallerID information is sent between
the first and second rings.
Assuming we do get a
CallerID message we call GetCallerInfo which takes a handle to a call
and calls the TAPI function lineGetCallInfo. This is supposed to
be a variable length struct. Not easy to do with VB. The HACK,
er.. um.. Solution! I have added a bByte() field to the end
of the struct and set it to 2k. Hey, if 640k is good enough then
2k is alright... There are more complicated ways to do this that
are more dynamic. CopyMemory could be used.
Once the lineGetCallInfo has
successfully returned we then inspect the lpCallInfo.dwCallerIDFlags to
verify that we got good info. In this application
I simply put 'UNKNOWN' in the CallerID variables. If the information is
not known to be good you could drill down to the correct reason using
the LINECALLPARTYID_ Constants mentioned earlier. If we have
good name or number info they get stuffed into the global
variables CallIDName and CallIDNumber respectively using the solution
defined above.
When we get a LINEDEVSTATE_RINGING message we inspect the
dwParam3 variable which contains the ring count and compare it with
our stored Rings To Answer or Toll Saver Ring values. If
it is equal to or greater then our preset value we answer the
phone!
After all the setup
answering the phone is almost depressingly simple. We call our
Public Sub Answer which calls the TAPI function lineAnswer which
simply takes a handle to the call and optional ISDN user-user
information which we don't supply. We also call
lineMonitorDigits through another Public Sub MonitorDigits so we will
get entered digit information. By monitoring for digits we have
the ability to enter a remote access code to play messages back from
anywhere.
The
next message from TAPI that
we want to deal with is LINECALLSTATE_CONNECTED and when
we get it we play the greeting by calling the aptly named PlayGreeting
sub. The first thing we do here is get the lineID of
the wave device to play the greeting through. This calls the lineGetID
TAPI function which takes another variable length structure, so
the same kind of hack used to get the CallerID information is used here
as well. Notice that if we don't get the lineID we set the global
variable lMediaID to -1, which is the value of the WAVE_MAPPER constant
defined in MMSystem.h. If this happens chances are
the greeting is going to get played through the PC sound system, not
the voice card.
Next we set what
information we know about the caller into the frmMain Caller
Information frame. As mentioned earlier we may not know much,
except the time which we get from the local system so that may not be
correct either. Finally we double check some state variables,
m_BPlayRec and fPlaying, load the file and play it out the device
indicated by lMediaID.
Loading and playing a
wave in VB file isn't too bad, especially since I was able to find an
example in Microsoft's Knowledge Base. The two subs, LoadFile
and Play in Wave.bas are from the Microsoft Knowledge Base Article ID:
Q182983 FILE: Playwave.exe Demonstrates How To Play a Sound
File. You might want to check that for more info. Any errors in
those subs are all mine though. Loading the file is a fairly
mundane operation, calling all the Mulit-Media I/O functions in the
correct order to verify the type of file and read its format into the
global WAVEFORMAT UDT, the number of samples are also stored in a
global variable, nSamples, and a pointer to memory allocated through
the GlobalAlloc kernel32 function is stored in the global long
bufferIn.
Once read in, we play
it. This got to be kind of tricky. Sound cards and voice
modems may wan't to be alike but they don't act alike. In the
Play sub I have had to select off of the device ID, using one
flag for the sound card and another for the modem. The intention
of the WAVE_MAPPED flag is to let Windows manage the difference
between a wave file format you are playing or recording and the format
in which the wave device can understand. Soundcards seem to
dislike the WAVE_MAPPED flag, though Voice Modems work with it, or in
my case don't work without it. Well sort of... My Creative
Labs Modem Blaster model DI 5630 worked fine with wave files recorded
in various PCM modes and those played fine out my soundcard.
However my Dialogic 160/SC didn't like the wave files at all, though
it will play several types of PCM wave files, it didn't like what I
was using. Then I found a wave format that worked on both the
Modem Blaster and the Dialogic card, but wouldn't play out the Sound
Card... I tried various sound cards as well. According to
Microsoft Knowledge Base Article ID: Q142745
The Microsoft
Consultative Committee for International Telephone and Telegraph
(CCITT) G.711 A-Law and u-Law codec can also achieve
only a 2:1 compression ratio, but is best when compatibility
with current Telephony Application Programming Interface (TAPI)
standards is a concern.
Hah! So that is
what we use; both record and playback are hard coded for the CCITT
u-Law 8.000 kHz, 8 Bit, Mono CODEC and it works from three types
of sound cards and three types of voice modems, albeit with the
WAVE_MAPPED flag twiddling that needs to occur. Once we get
around that hard won piece of information the wave out device opens
right up with a call to waveOutOpen. Then we simply set a few
bits in a WAVEHDR UDT call waveOutPrepareHeader to setup the memory
block pointed at by the WAVEHDR and call waveOutWrite to begin
playing. Of course this still leaves a bit of a question about
the WAVE_MAPPED flag, like why it doesn't work with sound cards.
If you know of a Microsoft
Source of information on this I would like to know.
FWIW, the CCITT
G.711 standard is also used in H.323 VOIP, and if you understand that
sentence fragment your are saying 'Cool' if not, skip it.
When
we called waveOutOpen we passed another callback, just like when
we initiated TAPI. Except this is a low-leval audio callback
function, which to but it bluntly means you can't do much of anything
inside that callback or more to the point, don't do anything that might
cause a hardware interrupt (such as printing a line of text to the
screen) or take too much time. How much is too much
time? It is difficult to say, but basically you should just set a
flag and get out of the callback. So we use a timer,
frmMain.Timer1 does all the work, the callback simply sets global variables.
In our Play function we enable the timer, when our callback
waveOutProc gets a MM_WOM_DONE message it sets the fPlaying flag to False
which is picked up in the frmMain.Timer1_Timer event and it closes
the wave device, frees the memory we allocated and sends a digit out
the phone line. I used a digit as the tone to mark when a
caller should begin their message for no reason other than to use the TAPI
lineGenerateDigits API call, though it did come with a bonus.
From the Platform SDK:
The TSPI
LINE_GENERATE message is sent to the LINEEVENT callback function to
notify TAPI that the current digit or tone generation has terminated.
So
TAPI notifies our application
when it is done sending the digit! Back in Globals.bas in our
LineCallBack proc we get a LINE_GENERATE message and that
signals the application to begin recording. We call our
RecordMessage sub, which gets the lineID for the "wave/in" device,
increments the global message counter variable nMessages and calls
RecStart in Wave.bas with parameters indicating the maximum message
length and the name to save the file as. All the messages are saved with any caller info that is known
and the time/date as the filename. This info is put back on the main
form when playing the message. Why not use the time/date stamp on the
file? This naming convention almost guarantees uniqueness since we
can't record files faster than 1 second resolution. However it would
probably be nicer to use the time/date off the file for display
purposes.
RecStart resides back
in our Wave.bas file. If my life depended on it I couldn't find
a sample of recording a wave file in VB. The RecStart and
AddWaveInBuffer procedures come from the MSJ Voice sample application
(converted from C of course). In that application both functions were
in the same procedure, I split them up so that it would be easier to
work with multiple buffers if I decide to do so in the future. Of
course any errors are all mine.
Again, just
like when we played a wave file, we visit the WAVE_MAPPED saga,
set the appropriate WAVEFORMAT UDT variables for our u-Law CODEC and
call waveInOpen. Then we simply calculate the needed buffer size
for our message, call our AddWaveInBuffer sub and then call
waveInStart to begin recording. This uses the same type of timer
and callback procedure as playing a wave file so we enable
frmMain.Timer1, only this time the calback is waveInProc and we are
looking for a MM_WIM_DATA message, which indicates that the buffer we
passed is filled.
The
AddWaveInBuffer sub allocates the memory for and calls the
appropriate Wave functions to submit a buffer for wave use. You can
use multiple smaller buffers (a good thing) or one huge one as I do (a
bad thing)... I have read, but not tried, that multiple smaller
buffers result in less CPU usage. Since this application uses ~10-20%
of a 400Mhz CPU when recording one would hope there was a way to put a
head on that. I should note that CPU usage varies with TSP, with
the Dialogic TSP and Wave driver there is no noticeable increase in
CPU usage, with the Unimodem 5 and Unimodem V drivers and associated
Unimodem Half-Duplex Audio Device wave driver the CPU is in the
~10-20% range on a 400Mhz CPU. Obviously a 'Generic' driver such
as the Unimodem ones need to do their work in software while a device
specific driver such as the Dialogic can utilize hardware specific
resources. You may want to keep this in mind if developing
multi-user real time applications.
All that is left to do
is save the recorded message. In Wave.bas look for
SaveToFileAsStream. This may be hard to recognize anymore but it
comes from the Platform SDK under the Multimedia, DirectSound VB
sample StreamTo. Check that for more info. As always, any errors are
all mine. All errors are ignored in this sub since If the file is
opened by another app, this one's going to fail bad. No point in
throwing an exception though. Just ignore it, so the new greeting
doesn't get saved, the old one probably won't get deleted either. This
shouldn't happen with a new message since we are almost guaranteed a
unique message name. This is actually pretty simple, we set up a
FileHeader UDT pretty much the same way as a regular WAVEHDR
UDT. The way a wave file is stored on disk is called a RIFF
file (resource interchange file format).
To save a RIFF file
just calls for a few extra bits of information for the file header,
which I have commented in the code. Other than that you simply
write the header to the file, then write all the bits, which we get
using the kernel32 function RtlMoveMemory aliased as CopyMemory.
That's it! There
is more functionality to the application, but it all boils down to
state information such as playing the files back is the same as
playing the greeting except we use a different device to play them on,
unless using the remote message retrieval function.
Unfortunately if you
have any questions I probably can't help. My test lab isn't that
great and my time demands are high. You can give it a shot if
you are really frustrated and e-mail me, but I have gone for greater
than six months without checking this e-mail account.
Have fun!
Steve