Welcome to
Steven A. Frare's
GeoCities Web Page

VB-TAPI  The Code
 Download the App & Code

 

 

Home

VB-TAPI

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:

  1. The application calls lineInitialize/Ex and iterates through the line devices.
  2. It negotiates the version numbers for all of the lines in the application with lineNegotiateAPIVersion.
  3. It determines the line capabilities with lineGetDevCaps.
  4. It determines the address capabilities with lineGetAddressCaps. 
  5. It looks for appropriate media and bearer modes with lineGetCallInfo. 
  6. 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

 

Copyright (C) 2000, Steven A. Frare all rights reserved

Hosted by www.Geocities.ws

1