If you call any large cinema looking for times for films, you will undoubtedly be forwarded to an automated system that tells you when each film is on. This system is made possible by digital telephony.
Computer Telephony Integration, or CTI, systems routinely cost $10,000 and upward for enterprise-scale systems. The high cost is largely a result of the misconceived idea that any telephony system requires loads of specialized hardware and, thus, is out of reach for the humble developer. In fact, you can put a simple system together using no more than a cheap modem.
Any company that employs staff to answer phone calls can save money by implementing a CTI system. Such a system can be used to route calls to different departments automatically or to match a caller with customer ID and associated purchase history.
This chapter is mainly devoted to one rather large code example built up in three sections. The first section explains how to pick up and drop a call. The following section explains how to detect key presses on the remote handset, and the chapter concludes with a demonstration of how to play back audio to the caller.
This chapter is focused on using the telephony API, but it is possible to control a modem by issuing COM port commands. These will provide the ability to dial telephone numbers and control the physical connection to the phone line.
Even if your modem is internal or connected via USB, it will always be mapped to a COM port. To discover the number of this COM port, you can look at Start→Control Panel→phone and Modem options→Modems. Under the Attached To tab will be the number of the COM port to which the modem is attached.
Any command that is sent to this COM port will be interpreted by the modem. A list of common AT commands shown in Table 14.1.
Table 14.1. AT commands.
AT Command | Purpose |
---|---|
| Dials the specified phone number using touch-tone dialing. A comma in the number represents a pause, a W waits for a second dial tone, and an @ waits for a five-second silence. |
| Dials the specified number using pulse dialing. |
| Picks up the line after the specified number of rings. |
| Drop line. |
The responses the modem will send back shown in Table 14.2.
Table 14.2. Modem responses.
Response | Meaning |
---|---|
| The command has executed without errors. |
| A connection to the remote phone has been made. |
| An incoming call is detected. |
| No carrier signal has been detected (in GSM modems, this can mean that there is no network). |
| The command is not understood. |
| There is no dial tone on the phone line. |
| The remote end is too busy to take the call. |
| The remote end did not take the call. |
To implement a simple phone dialer in .NET, open Visual Studio .NET and start a new Windows forms project. Right-click on the toolbox and click Customize Toolbox (or Add/Remove Items in Visual Studio .NET 2003). Click on the COM Controls tab, and then add the Microsoft Communications control (MSCOMM.OCX
). Drag this onto the form, and set the comport
property to the COM port number to which your modem is connected. Add a button to the form, named btnPhone
, click it, and add this code:
C#
private void btnPhone_Click(object sender, System.EventArgs e) { axMSComm1.PortOpen=true; axMSComm1.Output="ATDT00353877519575 "; }
VB.NET
Private Sub btnPhone_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) axMSComm1.PortOpen=True axMSComm1.Output="ATDT00353877519575" + vbcrlf End Sub
Running the code listed above may incur phone charges. It is advisable to change the phone number listed (00353877519575) to some other, less expensive number.
Only one program can control each COM port at a time. This code will fail if you are using the modem at the time. Several settings are associated with a COM port; in this case, however, the default parameters (9600 baud, no parity, 8 data bits, 1 stop bit—or “9600,n,8,1”) are suitable for communication with a modem. When the modem begins communication at full speed, it will use a baud rate of 56 Kbps. This can be set using the settings
property of the Microsoft communications control.
You can only do a certain number of things with a modem by sending commands back and forth via a COM port. In order to develop serious applications, you have to use the Telephony Application Programming Interface (TAPI). The TAPI libraries were designed with C++ in mind, not .NET, so there is a steep learning curve. It is worthwhile to evaluate the various commercial components available before tinkering with low-level TAPI code. A few interesting Web sites, such as www.shrinkwrapvb.com and www.pronexus.com, contain a wealth of information on TAPI.
The overall architecture of TAPI is modeled on a collection of phone lines that are connected to the computer. Not all of these phone lines are physical connections. Some of them are software representations of phone lines used for various internal processes. Each phone line may be opened or closed, which is analogous to a phone being on or off hook. An open phone line does not necessarily incur charges, unless a call is active.
When a phone line is open (off hook), it generates callbacks detailing any event that has happened on the line, such as an incoming call. A callback is simply a function that is called asynchronously by an underlying process.
When an incoming call is detected, the callback will contain a handle that can be passed to a function that accepts the call. At this point, call charges are applied to the line by the phone operator. Once the call is open, the modem behaves like a rudimentary audio device, which can play and receive basic audio. The line can still generate callbacks, such as a line dropping or the detection of the remote user pressing digits on the phone’s keypad.
When the call is dropped, the line remains open, but the modem can no longer function as an audio device. Phone charges will no longer be applied when the call is dropped. Callbacks will be generated until the line is closed.
Warning: If a line is not closed before the application exits, the computer may need to be restarted before the line can be reopened.
Without further ado, here is the first example of TAPI. This sample application will enable you to open and close a phone line, as well as detect and accept incoming calls.
Open a new project in Visual Studio .NET. Name the form frmTapi
, and add to it three buttons: btnStart
, btnStop
, and btnAccept
. You should also include a textbox named tbStatus
with multiline
set to true
.
Add a module named TAPI
, and add the following code. In C#, you add a class file instead of a module. Note that in C#, the class namespace is assumed to be tapi1_cs
, so substitute this for the name of your project.
C#
using System; using System.Runtime.InteropServices; namespace tapi1_cs { public class TAPI { public static int hCall; public static int hTAPI; public static int lNumLines; public static int hLine; public static linedevcaps lpLineDevCaps; public static frmTAPI userInterface; public const int TAPIVERSION = 0x10004; public const short LINECALLPRIVILEGE_OWNER = 0x4; public const short LINECALLPRIVILEGE_MONITOR = 0x2; public const short LINEMEDIAMODE_AUTOMATEDVOICE = 0x8; public const int LINE_LINEDEVSTATE = 8; public const int LINE_CALLSTATE = 2; public const int LINECALLSTATE_OFFERING = 0x2; public const int LINECALLSTATE_ACCEPTED = 0x4; public const int LINECALLSTATE_DISCONNECTED = 0x4000; public struct linedialparams { int dwDialPause; int dwDialSpeed; int dwDigitDuration; int dwWaitForDialtone; } public struct lineextensionid { int dwExtensionID0; int dwExtensionID1; int dwExtensionID2; int dwExtensionID3; } public struct linedevcaps { public int dwTotalSize; public int dwNeededSize; public int dwUsedSize; public int dwProviderInfoSize; public int dwProviderInfoOffset; public int dwSwitchInfoSize; public int dwSwitchInfoOffset; public int dwPermanentLineID; public int dwLineNameSize; public int dwLineNameOffset; public int dwStringFormat; public int dwAddressModes; public int dwNumAddresses; public int dwBearerModes; public int dwMaxRate; public int dwMediaModes; public int dwGenerateToneModes; public int dwGenerateToneMaxNumFreq; public int dwGenerateDigitModes; public int dwMonitorToneMaxNumFreq; public int dwMonitorToneMaxNumEntries; public int dwMonitorDigitModes; public int dwGatherDigitsMinTimeout; public int dwGatherDigitsMaxTimeout; public int dwMedCtlDigitMaxListSize; public int dwMedCtlMediaMaxListSize; public int dwMedCtlToneMaxListSize; public int dwMedCtlCallStateMaxListSize; public int dwDevCapFlags; public int dwMaxNumActiveCalls; public int dwAnswerMode; public int dwRingModes; public int dwLineStates; public int dwUUIAcceptSize; public int dwUUIAnswerSize; public int dwUUIMakeCallSize; public int dwUUIDropSize; public int dwUUISendUserUserInfoSize; public int dwUUICallInfoSize; public linedialparams MinDialParams; public linedialparams MaxDialParams; public linedialparams DefaultDialParams; public int dwNumTerminals; public int dwTerminalCapsSize; public int dwTerminalCapsOffset; public int dwTerminalTextEntrySize; public int dwTerminalTextSize; public int dwTerminalTextOffset; public int dwDevSpecificSize; public int dwDevSpecificOffset; public int dwLineFeatures; // TAPI v1.4 public string bBytes; } [DllImport("Tapi32.dll",SetLastError=true)] public static extern int lineAnswer (int hCall, ref string lpsUserUserInfo, int dwSize); [DllImport("Tapi32.dll",SetLastError=true)] public static extern int lineInitialize (ref int hTAPI,int hInst, LineCallBackDelegate fnPtr , ref int szAppName, ref int dwNumLines); [DllImport("Tapi32.dll",SetLastError=true)] public static extern int lineNegotiateAPIVersion(int hTAPI, int dwDeviceID, int dwAPILowVersion, int dwAPIHighVersion, ref int lpdwAPIVersion, ref lineextensionid lpExtensionID); [DllImport("Tapi32.dll",SetLastError=true)] public static extern int lineOpen (int hLineApp, int dwDeviceID, ref int lphLine, int dwAPIVersion, int dwExtVersion, ref int dwCallbackInstance, int dwPrivileges, int dwMediaModes, ref int lpCallParams); [DllImport("Tapi32.dll",SetLastError=true)] public static extern int lineGetDevCaps (int hLineApp, int dwDeviceID, int dwAPIVersion, int dwExtVersion, ref linedevcaps lpLineDevCaps); [DllImport("Tapi32.dll",SetLastError=true)] public static extern int lineSetStatusMessages (int hLine, int dwLineStates, int dwAddressStates); [DllImport("Tapi32.dll",SetLastError=true)] public static extern int lineDrop (int hCall, string lpsUserUserInfo, int dwSize); [DllImport("Tapi32.dll",SetLastError=true)] public static extern int lineShutdown(int hLineApp); } }
Option Strict Off Option Explicit On Module VB_TAPI Public LastTAPIEvent As Object Public aditionalTAPIEventInfo As Object Public hCall As Integer Public hTAPI As Integer Public lNumLines As Integer Public hLine As Integer Public lpLineDevCaps As linedevcaps Public userInterface As frmTAPI Public Const TAPIVERSION As Integer = &H10004 Public Const LINECALLPRIVILEGE_OWNER As Short = &H4S Public Const LINECALLPRIVILEGE_MONITOR As Short = &H2S Public Const LINEMEDIAMODE_AUTOMATEDVOICE As Short = &H8S Public Const LINE_LINEDEVSTATE = 8 Public Const LINE_CALLSTATE = 2 Public Const LINECALLSTATE_OFFERING = &H2 Public Const LINECALLSTATE_ACCEPTED = &H4 Public Const LINECALLSTATE_DISCONNECTED = &H4000 Structure linedialparams Dim dwDialPause As Integer Dim dwDialSpeed As Integer Dim dwDigitDuration As Integer Dim dwWaitForDialtone As Integer End Structure Structure lineextensionid Dim dwExtensionID0 As Integer Dim dwExtensionID1 As Integer Dim dwExtensionID2 As Integer Dim dwExtensionID3 As Integer End Structure Structure linedevcaps Dim dwTotalSize As Integer Dim dwNeededSize As Integer Dim dwUsedSize As Integer Dim dwProviderInfoSize As Integer Dim dwProviderInfoOffset As Integer Dim dwSwitchInfoSize As Integer Dim dwSwitchInfoOffset As Integer Dim dwPermanentLineID As Integer Dim dwLineNameSize As Integer Dim dwLineNameOffset As Integer Dim dwStringFormat As Integer Dim dwAddressModes As Integer Dim dwNumAddresses As Integer Dim dwBearerModes As Integer Dim dwMaxRate As Integer Dim dwMediaModes As Integer Dim dwGenerateToneModes As Integer Dim dwGenerateToneMaxNumFreq As Integer Dim dwGenerateDigitModes As Integer Dim dwMonitorToneMaxNumFreq As Integer Dim dwMonitorToneMaxNumEntries As Integer Dim dwMonitorDigitModes As Integer Dim dwGatherDigitsMinTimeout As Integer Dim dwGatherDigitsMaxTimeout As Integer Dim dwMedCtlDigitMaxListSize As Integer Dim dwMedCtlMediaMaxListSize As Integer Dim dwMedCtlToneMaxListSize As Integer Dim dwMedCtlCallStateMaxListSize As Integer Dim dwDevCapFlags As Integer Dim dwMaxNumActiveCalls As Integer Dim dwAnswerMode As Integer Dim dwRingModes As Integer Dim dwLineStates As Integer Dim dwUUIAcceptSize As Integer Dim dwUUIAnswerSize As Integer Dim dwUUIMakeCallSize As Integer Dim dwUUIDropSize As Integer Dim dwUUISendUserUserInfoSize As Integer Dim dwUUICallInfoSize As Integer Dim MinDialParams As linedialparams Dim MaxDialParams As linedialparams Dim DefaultDialParams As linedialparams Dim dwNumTerminals As Integer Dim dwTerminalCapsSize As Integer Dim dwTerminalCapsOffset As Integer Dim dwTerminalTextEntrySize As Integer Dim dwTerminalTextSize As Integer Dim dwTerminalTextOffset As Integer Dim dwDevSpecificSize As Integer Dim dwDevSpecificOffset As Integer Dim dwLineFeatures As Integer ' TAPI v1.4 Dim bBytes As String End Structure Public Declare Function lineAnswer Lib "Tapi32" _ (ByVal hCall As Integer, ByRef lpsUserUserInfo _ As String, ByVal dwSize As Integer) As Integer Public Declare Function lineInitialize Lib "Tapi32" _ (ByRef hTAPI As Integer, ByVal hInst As Integer, _ ByVal fnPtr As LineCallBackDelegate, ByRef _ szAppName As Integer, ByRef dwNumLines As _ Integer) As Integer Public Declare Function lineNegotiateAPIVersion Lib _ "Tapi32" (ByVal hTAPI As Integer, ByVal _ dwDeviceID As Integer, ByVal dwAPILowVersion _ As Integer, ByVal dwAPIHighVersion As Integer, _ ByRef lpdwAPIVersion As Integer, ByRef _ lpExtensionID As lineextensionid) _ As Integer Public Declare Function lineOpen Lib "Tapi32" _ (ByVal hLineApp As Integer, ByVal dwDeviceID _ As Integer, ByRef lphLine As Integer, ByVal _ dwAPIVersion As Integer, ByVal dwExtVersion _ As Integer, ByRef dwCallbackInstance _ As Integer, ByVal dwPrivileges As Integer, _ ByVal dwMediaModes As Integer, ByRef _ lpCallParams As Integer) As Integer Public Declare Function lineGetDevCaps Lib "Tapi32" _ (ByVal hLineApp As Integer, ByVal dwDeviceID _ As Integer, ByVal dwAPIVersion As Integer, _ ByVal dwExtVersion As Integer, ByRef _ lpLineDevCaps As linedevcaps) As Integer Public Declare Function lineSetStatusMessages Lib _ "Tapi32" (ByVal hLine As Integer, ByVal _ dwLineStates As Integer, ByVal _ dwAddressStates As Integer) As Integer Public Declare Function lineDrop Lib "Tapi32" _ (ByVal hCall As Integer, ByVal lpsUserUserInfo _ As String, ByVal dwSize As _ Integer) As Integer Public Declare Function lineShutdown Lib "Tapi32" _ (ByVal hLineApp As Integer) As Integer End Module
The code for the module may look daunting because these function definitions are ported directly from the TAPI.H
C++ code from the Windows platform SDK. It is not important to understand every parameter sent to these API calls, but for the moment, Table 14.3 gives an overview of all the API calls involved.
Table 14.3. Telephony API functions.
API Function | Purpose |
---|---|
| Picks up the phone when an incoming call is detected. This may incur phone charges. |
| Indicates the name of the callback function to TAPI, and retrieves the number of modems (virtual and physical) installed on the system. |
| Determines whether a modem can support a specified version of TAPI (i.e., 1.4 in this case). |
| Indicates to TAPI that the callback should now start receiving events for a specified modem. |
| Retrieves a host of technical information about a specified modem (see the |
| Indicates which, if any, events should be passed to the callback. |
| Shuts down a modem temporarily, dropping any active call. |
| Shuts down a modem permanently, cleaning up any resources. |
The core element of every TAPI application is the callback function LineCallBack
. This is used to detect changes in the phone line, such as incoming calls, dropped calls, or key presses on the remote telephone keypad.
Add the following code to the TAPI module:
The purpose of the LineCallBackDelegate
delegate is to ensure that the underlying telephony processes have something to call back to even after the program closes. This prevents Windows from crashing if your application does not shut down cleanly.
public delegate int LineCallBackDelegate(int dwDevice, int dwMessage, int dwInstance, int dwParam1, int dwParam2, int dwParam3); public static int LineCallBack(int dwDevice, int dwMessage, int dwInstance, int dwParam1, int dwParam2, int dwParam3) { string msgEvent=""; msgEvent = Convert.ToString(dwMessage); switch (dwMessage) { case LINE_CALLSTATE: switch(dwParam1) { case LINECALLSTATE_OFFERING: msgEvent = "Incomming call"; hCall = dwDevice; break; case LINECALLSTATE_ACCEPTED: msgEvent = "Call accepted"; break; case LINECALLSTATE_DISCONNECTED: msgEvent = "Call disconnected"; break; } break; case LINE_LINEDEVSTATE: msgEvent = "Ringing"; break; } userInterface.showMessage("Event: " + msgEvent + " Data:" + dwParam1 + " "); return 1; }
VB.NET
Delegate Function LineCallBackDelegate(ByVal dwDevice _ As Integer, ByVal dwMessage As Integer, ByVal _ dwInstance As Integer, ByVal dwParam1 As _ Integer, ByVal dwParam2 As Integer, ByVal dwParam3 _ As Integer) As Integer Public Function LineCallBack(ByVal dwDevice As _ Integer, ByVal dwMessage As Integer, ByVal dwInstance _ As Integer, ByVal dwParam1 As Integer, ByVal dwParam2 _ As Integer, ByVal dwParam3 As Integer) As Integer Dim msgEvent As String msgEvent = CStr(dwMessage) Select Case dwMessage Case LINE_CALLSTATE Select Case dwParam1 Case LINECALLSTATE_OFFERING msgEvent = "Incomming call" hCall = dwDevice Case LINECALLSTATE_ACCEPTED msgEvent = "Call accepted" Case LINECALLSTATE_DISCONNECTED msgEvent = "Call disconnected" End Select Case LINE_LINEDEVSTATE msgEvent = "Ringing" Case Else msgEvent = dwMessage.ToString() End Select userInterface.tbStatus.Text += "Event: " & _ msgEvent & " Data:" & dwParam1 & vbCrLf End Function
To explain the above code briefly: Once a line has been opened, every event on that line will cause TAPI to make a call to this function. The parameter dwMessage
indicates broadly what has happened on the line, and dwParam1
defines the event more concisely.
The most important message type is LINE_CALLSTATE
. This indicates significant state changes on the line. To determine the exact nature of the event, it is necessary to drill-down and look at dwParam1
. When this parameter is set to LINECALLSTATE_OFFERING
(0×2), a call has just been detected, and the handle to that call has been passed in dwDevice
. This handle can be later passed to lineAnswer
to pick up the phone. Other events such as LINECALLSTATE_ACCEPTED
(0×4) and LINECALLSTATE_DISCONNECTED
(0×4000) determine when a call becomes active and when the call is terminated.
In some cases, the event can be assumed by looking at the dwMessage
parameter only. A LINE_LINEDEVSTATE
(0×8) event is most likely to be the ringing sound from an incoming call, but it could also be that the phone line is out of service, indicated by a dwParam1
of LINEDEVSTATE_OUTOFSERVICE
(0×80), or that the phone line is under maintenance, indicated by LINEDEVSTATE_MAINTENANCE
(0×100). Because this type of occurrence is rare, and a computer program can hardly resolve the problem, the event can be ignored.
At this point, the user interface should have already been prepared with three buttons named btnStart
, btnStop
, and btnAccept
on the form. A large textbox named tbStatus
is required. The multiline
property should be set to true
.
Click the Start button and enter the following code:
C#
private void btnStart_Click(object sender, System.EventArgs e) { startModem(); }
VB.NET
Private Sub btnStart_Click(ByVal eventSender As _ System.Object, ByVal eventArgs As System.EventArgs) _ Handles btnStart.Click startModem() End Sub
Click the Stop button and enter the following code:
C#
private void btnStop_Click(object sender, System.EventArgs e) { stopModem(); }
VB.NET
Private Sub btnStop_Click(ByVal eventSender As _ System.Object, ByVal eventArgs As System.EventArgs) _ Handles btnStop.Click stopModem() End Sub
Click the Accept button and enter the following code:
C#
private void btnAcceptCall_Click(object sender, System.EventArgs e) { acceptCall(); }
VB.NET
Private Sub btnAccept_Click(ByVal eventSender As _ System.Object, ByVal eventArgs As System.EventArgs) _ Handles btnAccept.Click acceptCall() End Sub
C# developers will also require the following function:
C#
public void showMessage(string message) { tbStatus.Text += message; }
The reason for the extra function is that in VB.NET the TAPI module exposes functions and types contained within it globally. In C#, a class is used to hold the functions and types; therefore, any calls to these functions must be through a reference to the class. Because the functions are static, the only programmatic difference is the TAPI
prefix; however, the class needs to have a reference to the form so that it can display text on the screen when the TAPI callback occurs.
A computer may have more than one modem attached and will almost certainly have a few virtual modems, which are used for various other internal purposes. Voice modems are much more useful when it comes to telephony applications, but a data modem can still pick up and drop calls, even if it cannot communicate with a human user once the line is active. This limited functionality may be all that is required, however, if, for instance, the computer needs to do only one task in response to an incoming phone call, such as connecting to the Internet or rebooting.
This code is designed to open the first line it can find that is capable of detecting incoming calls. A more advanced system would select a voice modem over a data modem by selecting a modem with the lowest acceptable lMediaMode
. A voice modem can work with a media mode set to LINEMEDIAMODE_INTERACTIVEVOICE
(4 hex), whereas a data modem will generally only use LINEMEDIAMODE_DATAMODEM
(10 hex). Hybrid modems do exist, so the code below will scan all media modes from 1 to 100.
C#
public void startModem() { int nError=0; TAPI.lineextensionid lpExtensionID = new TAPI.lineextensionid(); int lUnused=0; int lLineID=0; int lNegVer=0; long lPrivilege=0; long lMediaMode=0; IntPtr HInstance=(IntPtr)0; lPrivilege = TAPI.LINECALLPRIVILEGE_OWNER + TAPI.LINECALLPRIVILEGE_MONITOR; lMediaMode = 4; Module thisModule; thisModule = Assembly.GetExecutingAssembly().GetModules()[0]; HInstance = Marshal.GetHINSTANCE(thisModule); TAPI.LineCallBackDelegate callback = new TAPI.LineCallBackDelegate(TAPI.LineCallBack); int Unused = 0; nError = TAPI.lineInitialize(ref TAPI.hTAPI, HInstance.ToInt32(), callback, ref Unused, ref TAPI.lNumLines); for (lLineID = 0;lLineID<TAPI.lNumLines;lLineID++) { nError = TAPI.lineNegotiateAPIVersion(TAPI.hTAPI, lLineID, TAPI.TAPIVERSION,TAPI.TAPIVERSION, ref lNegVer, ref lpExtensionID); do { nError = TAPI.lineOpen(TAPI.hTAPI, lLineID, ref TAPI.hLine, lNegVer, lUnused, ref lUnused, (int)lPrivilege, (int)lMediaMode, ref lUnused); lMediaMode ++; } while (nError < 0 && lMediaMode < 100); if (nError == 0) break; } TAPI.lpLineDevCaps.dwTotalSize = Marshal.SizeOf(TAPI.lpLineDevCaps); TAPI.lpLineDevCaps.bBytes = new StringBuilder().Append(' ',2000).ToString(); TAPI.lineGetDevCaps(TAPI.hTAPI, lLineID, lNegVer, lUnused, ref TAPI.lpLineDevCaps); TAPI.lineSetStatusMessages(TAPI.hLine, TAPI.lpLineDevCaps.dwLineStates, 0); }
Public Sub startModem() Dim nError As Integer Dim lpExtensionID As lineextensionid Dim lUnused As Integer Dim lLineID As Integer Dim i As Short Dim lNegVer As Integer Dim lPrivilege As Long Dim lMediaMode As Long lPrivilege = LINECALLPRIVILEGE_OWNER + _ LINECALLPRIVILEGE_MONITOR lMediaMode = 4 nError = lineInitialize(hTAPI, _ Microsoft.VisualBasic.Compatibility.VB6.GetHInstance.ToInt32, _ AddressOf LineCallBack, 0, lNumLines) For lLineID = 0 To lNumLines nError = lineNegotiateAPIVersion(hTAPI, _ lLineID,TAPIVERSION,TAPIVERSION, _ lNegVer, lpExtensionID) Do nError = lineOpen(hTAPI, lLineID, hLine, lNegVer, _ lUnused, lUnused, lPrivilege, lMediaMode, 0) lMediaMode = lMediaMode + 1 Loop Until nError >= 0 Or lMediaMode = 100 If nError = 0 Then Exit For Next lpLineDevCaps.dwTotalSize = Len(lpLineDevCaps) lpLineDevCaps.bBytes = Space(2000) lineGetDevCaps(hTAPI, lLineID, lNegVer, lUnused, _ lpLineDevCaps) lineSetStatusMessages(hLine, lpLineDevCaps.dwLineStates, 0) End Sub
It is important to shut down the line after use because no other program can use the modem until the line has been closed. If you close your program before the line is closed, there may be problems reopening the line, and you may have to restart your computer.
C#
public void stopModem() { int nError; nError = TAPI.lineShutdown(TAPI.hTAPI); }
VB.NET
Public Sub stopModem() Dim nError As Integer nError = lineShutdown(hTAPI) End Sub
Whenever an incoming call is detected, the callback function will set a public variable named hCall
to a reference number (a handle) that TAPI recognizes. When this handle is passed to lineAnswer
, the phone line is opened. The modem is then in a position to send and receive audio data from the remote user, provided the modem supports that functionality.
C#
public void acceptCall() { int nError; string szUnused=""; nError = TAPI.lineAnswer(TAPI.hCall, ref szUnused, 0); }
VB.NET
Public Sub acceptCall() Dim nError As Integer nError = lineAnswer(hCall, "", 0) End Sub
Because this is a demonstration program, it is worthwhile to display in real time what is happening to the callback function. A reference to the form is stored in a public variable so that the callback function can use that reference to display status messages in tbStatus
.
C#
private void frmTAPI_Load(object sender, System.EventArgs e) { TAPI.userInterface = this; }
VB.NET
Private Sub frmTAPI_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load userInterface = Me End Sub
VB.NET developers will need to set option strict off
at the top of their code and include a reference to Microsoft Visual Basic .NET Compatibility Runtime.
C# developers will require the following namespaces, whereas VB.NET developers will need to add a reference to the Microsoft.VisualBasic.Compatibility
assembly in Project→Add References.
using System.Runtime.InteropServices; using System.Text; using System.Reflection;
To test this program, run it from Visual Studio .NET and press startModem (see Figure 14.1). Connect your modem to a phone line. With a second phone, dial the number of the phone line that is connected to your modem. When an incoming call is detected and displayed –on-screen, you can press acceptCall. You will hear the ringing stop once the line is open. Hang up, or press stopModem to disconnect the call.
Dual-tone modulated frequency (DTMF) is a way of encoding a number into an audible sound composed of two sine waves played simultaneously. These sounds are generated when someone presses a digit on a phone’s keypad. This is particularly useful for automated phone conversations, such as “Press 1 if you have a billing inquiry. Press 2 if you require technical support,” and so on.
These sounds are decoded by the modem hardware and passed up to the TAPI callback as an event with dwMessage
set to LINE_MONITORDIGITS
(9 hex). The digit pressed is being held in dwParam1
.
To use DTMF within a TAPI application, a few small changes need to be made. First, add a new API definition and two new constants to the TAPI module thus:
C#
public const short LINEDIGITMODE_DTMF = 0x2; public const short LINE_MONITORDIGITS = 9; [DllImport("Tapi32.dll",SetLastError=true)] public static extern int lineMonitorDigits(int hCall,int dwDigitModes);
Public Const LINEDIGITMODE_DTMF As Short = &H2S Public Const LINE_MONITORDIGITS = 9 Public Declare Function lineMonitorDigits Lib "Tapi32" _ (ByVal hCall As Integer, ByVal dwDigitModes As _ Integer) As Integer
Then add a new case to the callback function:
C#
public static int LineCallBack(...) { ... switch (dwMessage) { ... case LINE_MONITORDIGITS: msgEvent = "DTMF"; break; } ... }
VB.NET
Public Function LineCallBack(...) As Integer ... Select Case dwMessage ... Case LINE_MONITORDIGITS MsgEvent = "DTMF" End Select
Then add a call to lineMonitorDigits
to acceptCall
:
public void acceptCall() { int nError; string szUnused=""; nError = TAPI.lineAnswer(TAPI.hCall, ref szUnused, 0); TAPI.lineMonitorDigits(TAPI.hCall, TAPI.LINEDIGITMODE_DTMF); }
VB.NET
Public Sub acceptCall() Dim nError As Integer nError = lineAnswer(hCall, "", 0) lineMonitorDigits(hCall, LINEDIGITMODE_DTMF) End Sub
Playing audio back through a voice modem is the core feature of any CTI system. The following example demonstrates how to send a prerecorded wave file as audio to a standard telephone handset. Using prerecorded messages should be adequate in most situations, where even dynamic data such as times, dates, and prices can be composed of snippets of audio like “one,” “two,” “three,” “four,” ... “thirteen,” “teen”, “twenty,” “thirty,” “fourty,” etc.
When recordings are so varied that it would be impossible to prerecord audio snippets, a speech synthesizer such as such as the text-to-speech application contained in the SamplesCSharpSimpleTTS
folder of Microsoft SAPI 5.1 (Speech Application Programming Interface) could be used. This, however, is beyond the scope of this book.
To illustrate the principle of audio playback, the first example demonstrates how to play a wave (.wav
) file through your sound card. The same technique is then applied to playing audio over an active phone call. The code required to play a simple wave file may seem like overkill. It is true that if all you require is to play a sound through the sound card, you should look at API calls like sndPlaySound
, or if sound recording is required, then the mciSendString
API should be of interest. The reason behind using low-level code to play a wave file though a sound card is that this method can be easily adapted to play audio directly through the phone line, albeit at lesser quality.
Open a new project in Visual Studio .NET, and add a new module. Type the following code into it. In C#, you will create a new class. Ensure that the namespace is the same as that used in your form; here it is assumed to be audio
. You may replace this as necessary.
C#
namespace audio { public class audio { public static WAVEHDR whdr; public static WAVEFORMAT format_wave; public static WAVEHDR outHdr; public static int bufferIn; public static int numSamples; public static int hWaveOut; public const short MMIO_READ = 0x0; public const int CALLBACK_FUNCTION = 0x30000; public const short WAVE_MAPPED = 0x4; public const short MMIO_FINDCHUNK = 0x10; public const short MMIO_FINDRIFF = 0x20; public struct MMCKINFO { public int ckid; public int ckSize; public int fccType; public int dwDataOffset; public int dwFlags; } public struct mmioinfo { public int dwFlags; public int fccIOProc; public int pIOProc; public int wErrorRet; public int htask; public int cchBuffer; public string pchBuffer; public string pchNext; public string pchEndRead; public string pchEndWrite; public int lBufOffset; public int lDiskOffset; public string adwInfo; public int dwReserved1; public int dwReserved2; public int hmmio; } public struct WAVEFORMAT { public short wFormatTag; public short nChannels; public int nSamplesPerSec; public int nAvgBytesPerSec; public short nBlockAlign; public short wBitsPerSample; public short cbSize; } public struct WAVEHDR { public int lpData; public int dwBufferLength; public int dwBytesRecorded; public int dwUser; public int dwFlags; public int dwLoops; public int lpNext; public int Reserved; } [DllImport("winmm.dll",SetLastError=true)] public static extern int waveOutWrite(int hWaveOut, ref WAVEHDR lpWaveOutHdr, int uSize); [DllImport("winmm.dll",SetLastError=true)] public static extern int waveOutPrepareHeader(int hWaveIn, ref WAVEHDR lpWaveInHdr, int uSize); [DllImport("winmm.dll",SetLastError=true)] public static extern int mmioRead (int hmmio, int pch, int cch); [DllImport("winmm.dll",SetLastError=true)] public static extern int waveOutOpen(ref int lphWaveIn, int uDeviceID, ref WAVEFORMAT lpFormat, int dwCallback, int dwInstance,int dwFlags); [DllImport("kernel32.dll",SetLastError=true)] public static extern int GlobalAlloc (int wFlags, int dwBytes); [DllImport("kernel32.dll",SetLastError=true)] public static extern int GlobalLock (int hmem); [DllImport("winmm.dll",SetLastError=true)] public static extern int mmioAscend (int hmmio, ref MMCKINFO lpck, int uFlags); [DllImport("kernel32.dll",SetLastError=true)] public static extern int GlobalFree (int hmem); [DllImport("winmm.dll",SetLastError=true)] public static extern int mmioOpenA (string szFileName, ref mmioinfo lpmmioinfo, int dwOpenFlags); [DllImport("winmm.dll",SetLastError=true)] public static extern int mmioDescend (int hmmio, ref MMCKINFO lpck, int x, int uFlags); [DllImport("winmm.dll",SetLastError=true)] public static extern int mmioRead(int hmmio, ref WAVEFORMAT pch, int cch); [DllImport("winmm.dll",SetLastError=true)] public static extern int mmioClose(int hmmio, int uFlags); [DllImport("winmm.dll",SetLastError=true)] public static extern int mmioStringToFOURCCA (string sz, int uFlags); [DllImport("winmm.dll",SetLastError=true)] public static extern int mmioDescend (int hmmio, ref MMCKINFO lpck, ref MMCKINFO lpckParent, int uFlags); } }
Option Strict Off Option Explicit On Module modAudio Public whdr As WAVEHDR Public format_wave As WAVEFORMAT Public outHdr As WAVEHDR Public bufferIn As Integer Public numSamples As Integer Public hWaveOut As Integer Public Const MMIO_READ As Short = &H0s Public Const CALLBACK_FUNCTION As Integer = &H30000 Public Const WAVE_MAPPED As Short = &H4s Public Const MMIO_FINDCHUNK As Short = &H10s Public Const MMIO_FINDRIFF As Short = &H20s Structure MMCKINFO Dim ckid As Integer Dim ckSize As Integer Dim fccType As Integer Dim dwDataOffset As Integer Dim dwFlags As Integer End Structure Structure mmioinfo Dim dwFlags As Integer Dim fccIOProc As Integer Dim pIOProc As Integer Dim wErrorRet As Integer Dim htask As Integer Dim cchBuffer As Integer Dim pchBuffer As String Dim pchNext As String Dim pchEndRead As String Dim pchEndWrite As String Dim lBufOffset As Integer Dim lDiskOffset As Integer Dim adwInfo As String Dim dwReserved1 As Integer Dim dwReserved2 As Integer Dim hmmio As Integer End Structure Structure WAVEFORMAT Dim wFormatTag As Short Dim nChannels As Short Dim nSamplesPerSec As Integer Dim nAvgBytesPerSec As Integer Dim nBlockAlign As Short Dim wBitsPerSample As Short Dim cbSize As Short End Structure Structure WAVEHDR Dim lpData As Integer Dim dwBufferLength As Integer Dim dwBytesRecorded As Integer Dim dwUser As Integer Dim dwFlags As Integer Dim dwLoops As Integer Dim lpNext As Integer Dim Reserved As Integer End Structure Declare Function waveOutWrite Lib "winmm.dll" (ByVal _ hWaveOut As Integer, ByRef lpWaveOutHdr As WAVEHDR, _ ByVal uSize As Integer) As Integer Declare Function waveOutPrepareHeader Lib "winmm.dll" _ (ByVal hWaveIn As Integer, ByRef lpWaveInHdr As _ WAVEHDR, ByVal uSize As Integer) As Integer Declare Function mmioRead Lib "winmm.dll" (ByVal hmmio _ As Integer, ByVal pch As Integer, ByVal cch As _ Integer) As Integer Declare Function waveOutOpen Lib "winmm.dll" (ByRef _ lphWaveIn As Integer, ByVal uDeviceID As Integer, _ ByRef lpFormat As WAVEFORMAT, ByVal dwCallback As _ Integer, ByVal dwInstance As Integer, ByVal dwFlags _ As Integer) As Integer Declare Function GlobalAlloc Lib "kernel32" (ByVal _ wFlags As Integer, ByVal dwBytes As Integer) As Integer Declare Function GlobalLock Lib "kernel32" (ByVal hmem _ As Integer) As Integer Declare Function mmioAscend Lib "winmm.dll" (ByVal _ hmmio As Integer, ByRef lpck As MMCKINFO, ByVal uFlags _ As Integer) As Integer Declare Function GlobalFree Lib "kernel32" (ByVal hmem _ As Integer) As Integer Declare Function mmioOpen Lib "winmm.dll" Alias _ "mmioOpenA"(ByVal szFileName As String, ByRef _ lpmmioinfo As mmioinfo, ByVal dwOpenFlags As _ Integer) As Integer Declare Function mmioDescendParent Lib "winmm.dll" _ Alias "mmioDescend"(ByVal hmmio As Integer, ByRef lpck _ As MMCKINFO, ByVal x As Integer, ByVal uFlags As _ Integer) As Integer Declare Function mmioReadFormat Lib "winmm.dll" Alias _ "mmioRead"(ByVal hmmio As Integer, ByRef pch As _ WAVEFORMAT, ByVal cch As Integer) As Integer Declare Function mmioClose Lib "winmm.dll" (ByVal _ hmmio As Integer, ByVal uFlags As Integer) As Integer Declare Function mmioStringToFOURCC Lib "winmm.dll" _ Alias "mmioStringToFOURCCA"(ByVal sz As String, ByVal _ uFlags As Integer) As Integer Declare Function mmioDescend Lib "winmm.dll" (ByVal _ hmmio As Integer, ByRef lpck As MMCKINFO, ByRef _ lpckParent As MMCKINFO, ByVal uFlags As Integer) As Integer End Module
This code is ported from the C++ prototypes, so it may appear to be complex. Again, it is not necessary to know every parameter passed to each of these API calls, but Table 14.4 provides a synopsis of the functions involved.
Table 14.4. Windows Multimedia API functions.
| Indicates the format of the raw audio data to the wave-out device, so that it can play the sound at the correct speed and knows its format |
| Reads data from an audio source into memory |
| Allocates a block of memory of a specified size |
| Prevents other processes from using a specified block of memory |
| Releases a block of memory |
| Opens an audio source (e.g., a wave file) |
| Retrieves the format of an audio source and details including bit rate, stereo/mono, quality, etc. |
| Converts a null-terminated string to a four-character code |
| Descends into a chunk of a RIFF file that was opened by using the |
| Opens an audio output device |
| Ascends out of a chunk in a RIFF file descended into with the |
| Descends into a chunk of a RIFF file that was opened by using the |
| Closes an audio input or output device |
| Tells the audio output device to begin playing the sound |
This application will load a wave file from disk into memory and then play it through the sound card on request. Loading a wave file into memory is done in two stages. The first is where the format of the audio is extracted from the wave file. The audio format includes details about the quality (16-bit or 8-bit), bit rate (44 kbps for CD quality), and whether the audio is mono or stereo. The audio format is stored in a public variable named format_wave
.
The next step is to pull the data segment of the wave file into memory. A wave file can be several megabytes in size, so for better performance, the memory is allocated directly from the heap using GlobalAlloc
. The wave file is then read into this memory using mmioRead
. Once the operation is complete, the file is closed.
Add the following code to the module:
C#
public static void LoadFile(ref string inFile) { int hmem = 0; MMCKINFO mmckinfoParentIn = new MMCKINFO(); MMCKINFO mmckinfoSubchunkIn = new MMCKINFO(); int hmmioIn = 0; mmioinfo mmioinf = new mmioinfo(); mmioinf.adwInfo = (new StringBuilder()).Append(' ',4).ToString(); hmmioIn = mmioOpenA(inFile, ref mmioinf, MMIO_READ); if (hmmioIn == 0) return; mmioDescend(hmmioIn, ref mmckinfoParentIn, 0, MMIO_FINDRIFF); mmckinfoSubchunkIn.ckid = mmioStringToFOURCCA("fmt", 0); mmioDescend(hmmioIn, ref mmckinfoSubchunkIn, ref mmckinfoParentIn, MMIO_FINDCHUNK); mmioRead(hmmioIn, ref format_wave, Marshal.SizeOf(format_wave)); mmioAscend(hmmioIn, ref mmckinfoSubchunkIn, 0); mmckinfoSubchunkIn.ckid = mmioStringToFOURCCA("data", 0); mmioDescend(hmmioIn, ref mmckinfoSubchunkIn, ref mmckinfoParentIn, MMIO_FINDCHUNK); GlobalFree(hmem); hmem = GlobalAlloc(0x40, mmckinfoSubchunkIn.ckSize); bufferIn = GlobalLock(hmem); mmioRead(hmmioIn, bufferIn, mmckinfoSubchunkIn.ckSize); numSamples = mmckinfoSubchunkIn.ckSize / format_wave.nBlockAlign; mmioClose(hmmioIn, 0); }
Sub LoadFile(ByRef inFile As String) Dim hmem As Integer Dim mmckinfoParentIn As MMCKINFO Dim mmckinfoSubchunkIn As MMCKINFO Dim hmmioIn As Integer Dim mmioinf As mmioinfo mmioinf.adwInfo = Space(4) hmmioIn = mmioOpen(inFile, mmioinf, MMIO_READ) If hmmioIn = 0 Then Exit Sub mmioDescendParent(hmmioIn, mmckinfoParentIn, 0, _ MMIO_FINDRIFF) mmckinfoSubchunkIn.ckid = mmioStringToFOURCC("fmt", 0) mmioDescend(hmmioIn, mmckinfoSubchunkIn, _ mmckinfoParentIn, MMIO_FINDCHUNK) mmioReadFormat(hmmioIn, format_wave, Len(format_wave)) mmioAscend(hmmioIn, mmckinfoSubchunkIn, 0) mmckinfoSubchunkIn.ckid = mmioStringToFOURCC("data", 0) mmioDescend(hmmioIn, mmckinfoSubchunkIn, _ mmckinfoParentIn, MMIO_FINDCHUNK) GlobalFree(hmem) hmem = GlobalAlloc(&H40S, mmckinfoSubchunkIn.ckSize) bufferIn = GlobalLock(hmem) mmioRead(hmmioIn, bufferIn, mmckinfoSubchunkIn.ckSize) numSamples = mmckinfoSubchunkIn.ckSize / _ format_wave.nBlockAlign mmioClose(hmmioIn, 0) End Sub
Once the wave file is in memory, the sound card can be instructed to play the audio with a call to this next function, named Play
. This function is asynchronous and can be called more than once during the playing of a sound clip, provided the hardware supports it. The sound card will fetch the audio from memory as required using a process known as direct memory access (DMA).
Because the audio format is stored in public variables, that data needs to be transferred to the sound card such that it can correctly play back the sounds at the right speed and quality. Once waveOutPrepareHeader
has set the sound card up, waveOutWrite
then starts the sound playing.
C#
public static void Play(short soundcard) { int rc = 0; int lFlags = 0; lFlags = CALLBACK_FUNCTION; if (soundcard != -1) lFlags = lFlags | WAVE_MAPPED; rc = waveOutOpen(ref hWaveOut, soundcard, ref format_wave, 0, 0, lFlags); if (rc != 0) return; outHdr.lpData = bufferIn; outHdr.dwBufferLength = numSamples * format_wave.nBlockAlign; outHdr.dwFlags = 0; outHdr.dwLoops = 0; waveOutPrepareHeader(hWaveOut, ref outHdr, Marshal.SizeOf(outHdr)); waveOutWrite(hWaveOut, ref outHdr, Marshal.SizeOf(outHdr)); }
VB.NET
Sub Play(ByVal soundcard As Short) Dim rc As Integer Dim lFlags As Integer lFlags = CALLBACK_FUNCTION If soundcard <> -1 Then lFlags = lFlags Or WAVE_MAPPED rc = waveOutOpen(hWaveOut, soundcard, format_wave, 0, _ 0, lFlags) If (rc <> 0) Then Exit Sub outHdr.lpData = bufferIn outHdr.dwBufferLength = numSamples * format_wave.nBlockAlign outHdr.dwFlags = 0 outHdr.dwLoops = 0 waveOutPrepareHeader(hWaveOut, outHdr, Len(outHdr)) waveOutWrite(hWaveOut, outHdr, Len(outHdr)) End Sub
C# developers will also require the following namespaces:
C#
using System.Runtime.InteropServices; using System.Text;
The next step is to design the user interface. Open the form and drag on two buttons named btnBrowse
and btnPlaySound
. Add a textbox name tbWave
and a File Open Dialog control named OpenFileDialog
.
Click on the Browse button and add the following code:
C#
private void btnBrowse_Click(object sender, System.EventArgs e) { openFileDialog.ShowDialog(); tbWave.Text = openFileDialog.FileName; }
VB.NET
Private Sub btnBrowse_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnBrowse.Click OpenFileDialog.ShowDialog() tbWave.Text = OpenFileDialog.FileName End Sub
The –1
in the code below signifies that we are using the default output device and not a modem.
Click on the Play sound button, and add the following code:
C#
private void btnPlaySound_CSlick(object sender, System.EventArgs e) { string filename = tbWave.Text; audio.LoadFile(ref filename); audio.Play(-1); }
Private Sub btnPlaySound_Click(ByVal eventSender As _ System.Object, ByVal eventArgs As System.EventArgs) _ Handles btnPlaySound.Click LoadFile(tbWave.Text) Play(-1) End Sub
You will need to set option strict off
at the top of your code and include a reference to Microsoft Visual Basic .NET Compatibility Runtime.
To test the application, run it from Visual Studio .NET, press Browse, and locate a wave file on your hard disk. Press Play sound, and you should hear the audio being played (Figure 14.2).
By combining the previous two example programs, and with the addition of a few extra lines of code, we can now send audio down the phone line, completing this introduction to CTI in .NET.
Open the first example program and include the module from the second example program. Copy the user interface from the second example program (including openFileDialog
) and place the buttons and textbox on the form.
The only hurdle in combining these two programs is to find a way to map a handle to a line to a handle to an output device. Luckily, an API call does that for us: lineGetID
. Open the TAPI module and enter the following code:
C#
public const short LINECALLSELECT_CALL = 0x4; public struct varString { public long dwTotalSize; public long dwNeededSize; public long dwUsedSize; public long dwStringFormat; public long dwStringSize; public long dwStringOffset; public string bBytes; } [DllImport("Tapi32.dll",SetLastError=true)] public static extern long lineGetID (long hLine, long dwAddressID, long hCall, long dwSelect, varString lpDevice, string lpszDeviceClass);
Public Const LINECALLSELECT_CALL = &H4 Structure varString Dim dwTotalSize As Long Dim dwNeededSize As Long Dim dwUsedSize As Long Dim dwStringFormat As Long Dim dwStringSize As Long Dim dwStringOffset As Long Dim bBytes As String End Structure Public Declare Function lineGetID Lib "Tapi32" _ (ByVal hLine As Long, ByVal dwAddressID As _ Long, ByVal hCall As Long, ByVal dwSelect As Long, _ ByRef lpDevice As varString, ByVal lpszDeviceClass As _ String) As Long
Go to the user interface, click on the Browse button, and add the following code:
C#
private void btnBrowse_Click(object sender, System.EventArgs e) { openFileDialog.ShowDialog(); tbWave.Text = openFileDialog.FileName; }
Private Sub btnBrowse_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnBrowse.Click OpenFileDialog.ShowDialog() tbWave.Text = OpenFileDialog.FileName End Sub
Because we are not playing through the default audio output device, we can no longer specify –1
for the sound card parameter in Play
; we have to use GetLineID
.
C#
private void btnPlaySound_Click(object sender, System.EventArgs e) { string filename = tbWave.Text; audio.LoadFile(ref filename); audio.Play((short)Convert.ToInt32 (TAPI.GetLineID("wave/out"))); }
VB.NET
Private Sub btnPlaySound_Click(ByVal sender As _ System.Object, ByVal e As System.EventArgs) Handles _ btnPlaySound.Click LoadFile(tbWave.Text) Play(GetLineID("wave/out")) End Sub
The final step is to implement the GetLineID
function. This retrieves the audio output device from the current call handle. As the parameters imply, this function is only valid when an active call is in progress (i.e., you can’t send audio down a phone line if no one is listening).
The string manipulation is used to convert a C++ representation of a variable-length string to the .NET string type.
public static string GetLineID(string sWave) { long nError = 0; string sTemp = ""; TAPI.varString oVar = new TAPI.varString(); System.Text.StringBuilder sb = new System.Text.StringBuilder(); oVar.bBytes = sb.Append(' ',2000).ToString(); oVar.dwTotalSize = Marshal.SizeOf(oVar); nError = lineGetID(hLine, 0, hCall, LINECALLSELECT_CALL, oVar, sWave); if (oVar.dwStringOffset == 0) return "-1"; sTemp = oVar.bBytes.Substring(0, (int)oVar.dwStringSize).Trim(); return sTemp; }
VB.NET
Public Function GetLineID(ByVal sWave As String) as String Dim nError As Long Dim sTemp As String Dim oVar As varString oVar.bBytes = Space(2000) oVar.dwTotalSize = Len(oVar) nError = lineGetID(hLine, 0, hCall, _ LINECALLSELECT_CALL, oVar, sWave) If oVar.dwStringOffset = 0 Then Return -1 sTemp = Trim(oVar.bBytes.Substring(0, oVar.dwStringSize)) Return sTemp End Function
To test this program, run it from Visual Studio .NET and press startModem (Figure 14.3). Connect your modem to a phone line. With a second phone, dial the number of the phone line that is connected to your modem. When an incoming call is detected and displayed on-screen, you can press acceptCall. You will hear the ringing stop once the line is open. Press Browse, and locate a file on your hard drive, press Play Sound, and you should hear it through your phone.
You may notice a distinct loss in sound quality when audio is sent over the phone line. Choosing different file types can lessen this effect. The official format for TAPI is u-Law 56 Kbps (7 KHz, 8-bit mono) in the United States and a-law 64 Kbps (8 KHz, 8-bit mono) in Europe; however, from personal experience, I have found that 22,050 Hz is clearer, even over TAPI connections.
This chapter detailed the technology involved in making a computer perform a task that most of us do every day—answering the phone. Systems like these can be used to assist any organization’s customer service activities, providing scalable call routing, and can answer simple queries without requiring full-time phone operators.
The applications of such a system are virtually unlimited because it can be used to provide information and services to people who can’t or don’t have time to log into the Internet. They are used in cinemas, ticket booking agencies, and mobile phone top-up centers.
The next chapter deals with an interesting technology that solves the problem of reliably sending data between a client and server that are not always connected to each other. Say hello to MSMQ!
18.227.102.50