Chapter 14. Adding Digital Telephony

Introduction

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.

Note

You will need a voice modem and phone line to test the following examples. Access to a second phone (such as a mobile phone) is beneficial. Calls made from any phone line may incur charges if the line is opened.

Basic telephony

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

ATDT<phone number><enter>

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.

ATPT<phone number><enter>

Dials the specified number using pulse dialing.

AT S0=<number>

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

OK

The command has executed without errors.

CONNECT

A connection to the remote phone has been made.

RING

An incoming call is detected.

NO CARRIER

No carrier signal has been detected (in GSM modems, this can mean that there is no network).

ERROR

The command is not understood.

NO DIAL TONE

There is no dial tone on the phone line.

BUSY

The remote end is too busy to take the call.

NO ANSWER

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

Note

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.

Listening for incoming phone calls

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.

Note

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);
}

}

VB.NET

 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

lineAnswer

Picks up the phone when an incoming call is detected. This may incur phone charges.

lineInitialize

Indicates the name of the callback function to TAPI, and retrieves the number of modems (virtual and physical) installed on the system.

lineNegotiateAPIVersion

Determines whether a modem can support a specified version of TAPI (i.e., 1.4 in this case).

lineOpen

Indicates to TAPI that the callback should now start receiving events for a specified modem.

lineGetDevCaps

Retrieves a host of technical information about a specified modem (see the lineDevCaps structure listed above).

lineSetStatusMessages

Indicates which, if any, events should be passed to the callback.

lineDrop

Shuts down a modem temporarily, dropping any active call.

lineShutdown

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:

Note

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.

C#

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);
}

VB.NET

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.

C#

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.

Basic TAPI callreceiver application.

Figure 14.1. Basic TAPI callreceiver application.

DTMF tones

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);

VB.NET

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:

C#

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

Audio playback

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);
}
}

VB.NET

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.

waveOutPrepareHeader

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

mmioRead

Reads data from an audio source into memory

GlobalAlloc

Allocates a block of memory of a specified size

GlobalLock

Prevents other processes from using a specified block of memory

GlobalFree

Releases a block of memory

mmioOpen

Opens an audio source (e.g., a wave file)

mmioReadFormat

Retrieves the format of an audio source and details including bit rate, stereo/mono, quality, etc.

mioStringToFOURCC

Converts a null-terminated string to a four-character code

mmioDescend

Descends into a chunk of a RIFF file that was opened by using the mmioOpen function; can also search for a given chunk

waveOutOpen

Opens an audio output device

mmioAscend

Ascends out of a chunk in a RIFF file descended into with the mmioDescend function or created with the mmioCreateChunk function

mmioDescendParent

Descends into a chunk of a RIFF file that was opened by using the mmioOpen function; can also search for a given chunk

mmioClose

Closes an audio input or output device

waveOutWrite

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);
}

VB.NET

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);
}

VB.NET

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).

Wave sound player application.

Figure 14.2. Wave sound player application.

Audio playback over TAPI

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);

VB.NET

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;
}

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

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.

C#

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.

TAPI call receiver with DTMF and playback.

Figure 14.3. TAPI call receiver with DTMF and playback.

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.

Conclusion

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!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.163.175