My interest for TAPI was prompted when I decided to develop «home» applications that would provide my computer with dialing, callerID and answering machine capabilities. I decided to develop a TAPI application myself. As a result, in this series of three articles, I will review the basics of TAPI, devise a component, develop the recording and playback capabilities for the modem and develop an In-process automation server called gtroTelServer that would provide the required capabilities to calling applications. This second article documents the development of the TGtroTAPI component [the othertwo articles are in preparation.
Now that we know everything we need to know about TAPI (see Part I - Basics of TAPI) and that we have components that can play/record sound to the modem (see Part II - Two components to play/record sound with a voice modem), we can develop a component specially designed for the home which provides the basic functionalities of TAPI, i.e., making outbound calls, CallerID and minimal voice recording capabilities.
As this is a Delphi component, Object-Oriented Programming (OOP) needs to be used. First, I will declare the class TGtroCustomTAPI class as a descendant of TComponent. following the way most classes are declared in the VCL. It allows me to give properties of this class minimum visibility in this class The resulting class that I will use in the applications is TGtroTAPI which is declared as a descendant of TGtroCustomTAPI in which I give variables, properties and methods the needed visibility.
C programmers would use tapi.h to retrieve the constants et function declarations needed for this objective. Tanks to Project Jedi, a translation to Delphi of tapi.h is available for download from the API Conversion page of their web site as tapi.zip. This version of the header file is dated 11 September 2000 and is JEDI-certified. For convenience, the tapi.pas provided by the JEDI project has been renamed "gtrotapi.pas".
Development of the component
First, from within any project, I will do File|New|Unit and save this new unit as TAPIComponent.pas. Now that the skeleton of the component is saved, its unit can be put in any package. Compiling and installing the package will put the component GtroTAPI in the GTRO folder of the Component Palette (with a specific icon once gtroTAPIComponent.dcr is defined).
How does the component work?
Figure 1 shows how our component interacts with TAPI, the TSP (here: Unimodem /5 on Windows Vista) and the telephone hardware.
When the TgtroTAPI component is activated, the execution of the Initialize() method
- initializes TAPI,
- enumerates the devices available,
- selects the divice that is a modem and
- opens a line for monitoring.
Immediately after this is performed, the component waits for either outbound or inbound calls to occur.
The components operates in three modes: Calling, CallerID and Answering. When the component is initialized, it is initialized in the CallerID mode, i.e., the line is opened for monitoring the line.
Initialization of the component
The public method Init() is used to initialize TAPI, to negotiate the TAPI version, and to select the modem. Its code is as follows:
function TgtroCustomTAPI.Init: Boolean;
begin
if FTAPI_Initialized then Shutdown;
FTAPI_Initialized:= TapiInitialize; // Initialization of TAPI
Result:= FTAPI_Initialized;
if Result then
begin
NumDevices:= EnumerateDevices; // Enumerate devices and select one (modem)
if NumDevices > 0 then
begin
BearerMode:= LINEBEARERMODE_VOICE; // the only Bearer mode of interest here
...
Result:= True;
end;
end
else
begin
ShowMessage('TAPI could not initialize itself');
Application.Terminate;
end;
end;
As shown above, two private methods are called by Init(): TapiInitialize() and EnumerateDevices() and once they succeed, the Bearer mode is set to LINEBEARERMODE_VOICE (the only Bearer Mode of interest here). Their code follows:
function TgtroCustomTAPI.TapiInitialize: Boolean;
...
begin
...
with LineInitParams do // initialize parameters
begin
dwTotalSize:= SizeOf(LINEINITIALIZEEXPARAMS);
dwOptions:= LINEINITIALIZEEXOPTION_USEHIDDENWINDOW;
end;
ErrNo:= LineInitializeEx(@LineApp, System.MainInstance, CallbackProc,
nil, FLocalNumDevs, FVersion, LineInitParams);
case ErrNo of
0: Result:= True;
...
end; // case ErrNo
...
end; { TapiInitialize }
As shown in this code, the LineApp handle is returned upon successful execution of the lineInitializeEx() TAPI function. The initialization of the LineInitializeExParams structure sets the event notification mechanism to Callback (Hidden Window), the address of the callback function being given as the third argument of the function.
The private method EnumerateDevices() uses the FLocalNumDevs argument returned by lineInitializeEx() to examine the properties and capabilities of the line devices found by TAPI. As the code displayed below shows, each line device is identified and its properties are stored in Device objects. The line device that is linked to the modem is selected as the active device (as described in the Annexes)
function TgtroCustomTAPI.EnumerateDevices: Integer;
...
begin
for i:= 0 to MAXNUMDEV do fLineDevices[i].Free;
DeviceList:= TStringList.Create;
try
for i := 0 to FLocalNumDevs - 1 do // FLocalNumDev is evaluated in LineInitializeEx()
begin
fLineDevices[i]:= TLineDevice.Create(i);
rc:= LineNegotiateAPIVersion(LineApp, i, LoApiVer, HiApiVer,
fLineDevices[i].dwAPIVersion, ExtID);
if rc <> 0 then
begin // errors
case rc of
... //Error handling
end; // case
end
else
begin // device found
fLineDevices[i].pDevCaps:= AllocLineDevCap(LineApp, i, fLineDevices[i].dwAPIVersion, 0);
fLineDevices[i].CallIsActive:= False;
fLineDevices[i].rc:= rc;
fLineDevices[i].dwBearerModes:= fLineDevices[i].pDevCaps^.dwBearerModes;
fLineDevices[i].dwMediaModes:= fLineDevices[i].pDevCaps^.dwMediaModes;
// Initialize line states for SetStatusMessages
fLineDevices[i].dwLineStates:= fLineDevices[i].pDevCaps^.dwLineStates;
if (fLineDevices[i].pDevCaps^.dwStringFormat = STRINGFORMAT_ASCII) then
begin // Extract line name
DeviceName:= RecordGetStr(fLineDevices[i].pDevCaps^, @fLineDevices[i].pDevCaps^.dwLineNameSize);
DevID:= TestDevice(i);
if DevID <> LINEMAPPER then
begin // select the LineDevice that supports automated voice
fModemHandle:= THandle(DevID);
FActiveDevice:= i; // select the modem
...
end; // if DevID <> LINEMAPPER
end; // if (fLineDevices[i].pDevCaps^.dwStringFormat = STRINGFORMAT_ASCII)
TriggerNegotiationEvent(rc, S);
end; // if rc <> 0
end; // for i:= 0 to FLocalNumDevs - 1
fLineDevice:= GetDevice(FActiveDevice); // Select LineDevice of modem
Result:= DeviceList.Count;
TriggerEnumerateDevicesEvent(DeviceList); // produces the LineDevice list for the calling application
finally
DeviceList.Free;
end; // try...finally
end; { EnumerateDevices }
The decision ...
function TGtroCustomTAPI.TestDevice(i: Integer): DWORD;
var
...
begin
Result:= LINEMAPPER;
// Open line to get valid Port Handle
LineOpenResult := LineOpen(LineApp, i, fLineDevices[i].pHandle,
fLineDevices[i].dwAPIVersion, 0, 0, LINECALLPRIVILEGE_NONE, 0, nil);
if LineOpenResult = 0 then // the line is open
begin
Result:= GetDeviceID(LINECALLSELECT_LINE, 'comm', i); // get valid modem handle
LineClose(ALine);
end
end;
where
function TgtroCustomTAPI.GetDeviceID(LineCallSelect: DWORD;
DeviceClass: PChar; i: Integer): DWORD;
type
LPPort = ^TPort;
TPort = record
VarString: TVarString;
DeviceID: DWORD;
szDeviceName: array [1..1] of AnsiChar; // don't know why!!!
end;
var
...
begin
fLineDevice:= fLineDevices[i];
DeviceName:= 'Unknown';
DevID:= UINT(-1);
Result := LINEMAPPER; // so failure can be tested and defaulted
PortSize := sizeof(TPort);
GetMem(TempPort, PortSize); // reserve memory for TempPort
TempPort^.VarString.dwTotalSize := PortSize;
try
repeat
// lineGetIDW() returns a device identifier for the specified device class
// associated with the selected line, address, or call.
FError := lineGetIDW(fLineDevice.pHandle^, 0, fLineDevice.pCall^,
LineCallSelect, pVarString(TempPort), DeviceClass); // lpszDeviceClass: PChar
if (TempPort^.VarString.dwNeededSize >
TempPort^.VarString.dwTotalSize) then
begin
PortSize := TempPort^.VarString.dwNeededSize;
FreeMem(TempPort);
GetMem(TempPort, PortSize);
TempPort^.VarString.dwTotalSize := PortSize;
FError:= -1;
end;
if FError < -1 then
begin
TriggerDeviceIDEvent(DeviceClass, DeviceName, FError, DevID);
Exit;
end; // if FError
until FError = 0;
if (DeviceClass = 'wave/out') or (DeviceClass = 'wave/in') then ...
if DeviceClass = 'comm' then
if TempPort^.VarString.dwStringFormat = STRINGFORMAT_ASCII then
begin
if TempPort^.VarString.dwStringSize <> 0 then
DeviceName:= RecordGetStr(TempPort^, @TempPort^.VarString.dwStringSize);
DevID:= TempPort^.DeviceID;
Result:= DevID;
end;
TriggerDeviceIDEvent(DeviceClass, DeviceName, FError, DevID);
finally
FreeMem(TempPort);
end;
end
// PVarString: device identifier returned here
Opening the line
Up to now, TAPI has been initialized and the modem has been selected. This process has been executed without having to specify any purpose for the use of the component. With the opening of the line, this changes. Some parameters having to do with the intent one has to open the line have to be specified
- the Bearer Mode which in this case must be voice (regular 3.1 kHz analog voice-grade bearer service) since the modem supports only this mode and the passtrough (service provider gives direct access to the attached hardware for control by the application) mode;
- the Media Mode which in this case will either be interactivevoice (call is treated as an interactive call with humans on both ends) and automatedvoice (voice is locally handled by an automated application); and
- the CallPriviledges which can take the set of values of LINECALLPRIVILEDGE_NONE, LINECALLPRIVILEDGES_MONITOR or LINECALLPRIVILEDGE_OWNER, the meanings of which seems obvious.
As shown below, the selection of these modes is performed through the definition of the TAPIMode property that can take the following values:
- tmTesting - this value was defined for testing only by a TAPITestBed application;
- tmDialing - this value is defined to dial a phone number;
- tmCallerID - this value is defined to implement a CallerID application; and
- tmAnswering - this value is defined to implement a mail box application
The code of the OpenLine() method is the following:
function TgtroCustomTAPI.OpenLine(CallPriviledge: TTAPICallPriviledges): Boolean;
var
...
begin
Result:= False;
with FLineCallParams do // a TLineCallParams structure
begin
dwTotalSize := sizeof(FLineCallParams);
dwBearerMode := BearerMode;
dwMediaMode := MediaMode;
end;
case CallPriviledge of
cpNone: CallPriviledges:= LINECALLPRIVILEGE_NONE; // placing calls
cpOwner: CallPriviledges:= LINECALLPRIVILEGE_OWNER; // answering calls
cpMonitor: CallPriviledges:=
LINECALLPRIVILEGE_MONITOR or LINECALLPRIVILEGE_OWNER; // monitoring calls
end; // case
// Open the line now!
OpenResult := LineOpen(LineApp, FActiveDevice, fLineDevice.pHandle, fLineDevice.dwAPIVersion, 0, 0, // No data passed back
fLineDevice.dwCallPriviledges, fLineDevice.dwMediaMode, nil);
if OpenResult = 0 then
begin
fLineDevice.LineIsOpen:= True;
LineSetStatusMessages(fLineDevice.pHandle^, fLineDevice.dwLineStates, 0);// Enables LINE_LINEDEVSTATE messages
Result := True; // success so drop through
end
else
begin
case OpenResult of
...
end;
if OpenResult < 0 then
raise Exception.Create(OpenMsg);
end;
TriggerLineOpenEvent(OpenMsg);
end; { OpenLine }
Now that the line object is open, lets consider how TAPI communicates with the application.
Callback function
As a result of the selection of "Callback" as the notification mechanism when lineInitializeEx() was executed, I will use a callback function. In the context of object-oriented programming, this function must be global and cannot be a method of the class. As a consequence, I will use a global procedure as our callback function and use a pointer to our class in order for the callback procedure to communicate with our TGtroCustomTapi object. This pointer, called MyGtroTAPIComponent, is declared as a variable of the type of the class and it is initialized to "self" in the OnCreate() method of the class.
The callback procedure is implemented as follow:
procedure CallBackProc(hDevice, dwMessage, dwInstance, dwParam1, dwParam2, dwParam3 : DWORD); stdcall;
begin
MyGtroTAPIComponent.TapiCallBack(hDevice, dwMessage, dwInstance, dwParam1, dwParam2, dwParam3);
end;
where TapiCallBack() is a private method (a hack) of the TCustomGtroTapi class with the same arguments as CallbackProc(). This private methods is coded as follows:
procedure TgtroCustomTAPI.TapiCallback
(hDevice, dwMessage, dwInstance, dwParam1, dwParam2, dwParam3: DWORD);
begin
if FTAPI_Initialized then
begin
with CallbackResults do
begin // Write TAPI results to string list for use by Delphi applications
Clear;
case dwMessage of // hDevice is HLINE
LINE_ADDRESSSTATE,
LINE_APPNEWCALL, // dwParam2 is HCALL
LINE_CLOSE,
LINE_LINEDEVSTATE:
LineProc(hDevice, dwMessage, dwInstance, dwParam1, dwParam2, dwParam3);
// hDevice is HCALL
LINE_CALLINFO,
LINE_CALLSTATE,
LINE_GATHERDIGITS,
LINE_GENERATE,
LINE_MONITORDIGITS,
LINE_MONITORMEDIA:
CallProc(hDevice, dwMessage, dwInstance, dwParam1, dwParam2, dwParam3);
LINE_CREATE:
begin
// hDevice is zéro, dwParam1 identifies the device
Add(DDT + 'received LCB (LINE_CREATE): Line device created')
end; // LINE_CREATE
LINE_REMOVE:
begin
// hDevice is zero; dwParam1 identifies the device
Add(DDT + 'received LCB (LINE_REMOVE): Line device removed');
end; // LINE_REMOVE
LINE_REPLY:
begin
if (dwParam2 = 0) then
Add(DDT + 'received LCB (LINE_REPLY): Async. request 0x:' +
IntToHex(dwParam1, 8) + ' completed successfully')
else
Add(DDT + 'received LCB (LINE_REPLY): Async. request 0x:' +
IntToHex(dwParam1, 8) + ' failed');
TriggerLineReplyEvent;
end; // LINE_REPLY
LINE_REQUEST:
begin
// hDevice is zero; dwParam1 is the request mode
Add(DDT + 'received LCB (LINE_REQUEST): Request from another application');
end; // LINE_REQUEST
else
Add(DDT + 'received LCB (UNKNOWN): Unpredicted event 0x' + IntToHex(dwMessage, 8));
end; // case dwMessage
if Verbose then
begin
Add('Parameters: : ' + IntToHex(hDevice, 8) + ', ' + IntToHex(dwMessage, 8)
+ ', ' + IntToHex(dwInstance, 8) + ', ' + IntToHex(dwParam1, 8)
+ ', ' + IntToHex(dwParam2, 8) + ', ' + IntToHex(dwParam3, 8));
end;
end; // CallbackResults
TriggerCallbackEvent; // sends results to the calling application
end; // if FTAPI_Initialized
end;
Porting the component to Delphi 2009
The component has been used successfully with Delphi 6.0 and Delphi 7.0 but it cannot be used under Delphi 2009 because it does not recognize the modem. In order to do so, the component uses two methods that use strings and Char types: RecordGetString() and GetDeviceID() which in turn uses the lineGetID() TAPI call.
Where previous versions of Delphi used a String type based on ANSI Character types of only 1 byte long, Delphi 2009 defines a new string type based on Unicode data, with WideChar elements of 2 bytes long. Delphi 2009 is fully Unicode based, and defines a new type called UnicodeString which is the new equivalent for the String type. Previously, String was synonymous with AnsiString (a type which is also still available, just like AnsiChar and PAnsiChar). ASdditionally, Delphi 2009 Character types are Char, AnsiChar and WideChar, where Char defaults to WideChar. In previous versions of Delphi, a Char would be equivalent to an AnsiChar. In order to ensure existing code compiler without changes in behaviour, change Char to AnsiChar (as well as PChar to PAnsiChar). The most important Delphi 2009 String types are: UnicodeString, WideString, AnsiString, UTF8String (a AnsiString with UTF-8 encoding) and ShortString. The default String type is equivalent to UnicodeString, which consists of WideChar characters (like WideString), but is reference counted and memory managed by Delphi (instead of by Windows itself), so a lot faster than a WideString. [extracted from Delphi 2009 Unicode]
Tapi 2.0 and Unicode
According to MSDN, most TAPI functions are implemented in Unicode (W) and ANSI (A) versions whereas the entire Telephony Service Provider Interface (TSPI) is Unicode for version 2.0. Applications that explicitly call the generic (neither "W" or "A" suffix) version of a function will execute the ANSI version, for compatibility with previous versions of TAPI.
In the component, the only TAPI calls that are used are (listed alphabetically):
- lineAnswer() - This function has no Unicode version. It is used in the AnswerCall() method which is called by the Answer() method. Answer is called by the LineProc() method when the LINEDEVSTATE_RINGING event occurs when dwParam3 >= FNumRings, a predefined number of rings.
- lineClose() - This function has no Unicode version. It is used in the CloseLine() and TestDevice() methods. CloseLine is called by the TAPIShutdown() and the Reset() methods.
- lineDeallocateCall() - This function has no Unicode version. It is used in the DeallocateCall() method which is called by the ShutdownCall() method.
- lineDrop() - This function has no Unicode version.
- lineGenerateDigits() - This function has a "W" Unicode version which has been used in this component. It is used in the GenerateTone() method
- lineGetCallInfo() - This function has a "W" Unicode version. It is used in the AllocLineCallInfo() private method which is called by the GetMediaModeCapabilities() private method.
- lineGetDevCaps(() - This function has a "W" Unicode version. It is used in the AllocLineDevCaps() private method which is called by the EnumerateDevices() private method.
- lineGetID() - This function has a "W" Unicode version which has been used in this component. It is used in the GetDeviceID() private method which is called by the TestDevice() method, itself called by the EnumerateDevices() method which is called by the Init() public method. The help file notes that applications must handle conversion of strings in lpDeviceID, if these are directly manipulated.
- lineInitializeEx() - This function has a "W" Unicode version but it was not used in this component since the parameter lpszAppName: PChar is set to nil in this component. It is used in the TapiInitialize() method which is called by the Init() public method.
- lineMakeCall() - This function has a "W" Unicode version which has been used in this component. It is used in the Dial() public method.
- lineNegociateAPIVersion() - This function has no Unicode version. This call is used in the EnumerateDevices() private method which is called by the Init() public method.
- lineOpen() - This function has a "W" Unicode versioncwhich has not been used in the component since the parameter lpCallParams: PLineCallParams is set to nil when used. It is used in the OpenLine() and the TestDevice() private method.
- lineShutdown() - This function has no Unicode version.
Two instances of TAPI calls deserve a special attention.
GetDeviceID()
In the process of enumerating the TAPI devices available, each device is scrutinized to determine if it is a modem or not. This is done using the GetDeviceID() method whose code is given in part hereunder:
function TgtroCustomTAPI.GetDeviceID(LineCallSelect: DWORD; DeviceClass: PChar;
i: Integer): DWORD;
// Return the LineDevice ID for "comm", "wave/in" and "wave/out".
type
LPPort = ^TPort;
TPort = record
VarString: TVarString;
// good trick as this becomes the additional param provided by TAPI
DeviceID: DWORD;
szDeviceName: array [1..1] of AnsiChar; // don't know why!!!
end;
var
...
begin
fLineDevice:= fLineDevices[i];
DeviceName:= 'Unknown';
DevID:= UINT(-1);
Result := LINEMAPPER; // so failure can be tested and defaulted
//if not LineIsOpen then Exit; // no line open => bail out
PortSize := sizeof(TPort);
GetMem(TempPort, PortSize); // reserve memory for TempPort
TempPort^.VarString.dwTotalSize := PortSize;
try
repeat
// lineGetIDW() returns a device identifier for the specified device class
// associated with the selected line, address, or call.
FError := lineGetIDW( // unicode version of the generic lineGetID() API call
fLineDevice.pHandle^, // current value of the line handle
0,
fLineDevice.pCall^, // call exist
LineCallSelect,
pVarString(TempPort),// PVarString: DeviceID returned here
DeviceClass); // lpszDeviceClass: PChar
if (TempPort^.VarString.dwNeededSize >
TempPort^.VarString.dwTotalSize) then
begin
PortSize := TempPort^.VarString.dwNeededSize;
FreeMem(TempPort);
GetMem(TempPort, PortSize);
TempPort^.VarString.dwTotalSize := PortSize;
FError:= -1;
end;
if FError < -1 then
begin
TriggerDeviceIDEvent(DeviceClass, DeviceName, FError, DevID);
Exit;
end; // if FError
until FError = 0;
if (DeviceClass = 'wave/out') or (DeviceClass = 'wave/in') then
if TempPort^.VarString.dwStringFormat = STRINGFORMAT_BINARY then
begin
if TempPort^.VarString.dwStringSize <> 0 then
DeviceName:= RecordGetStr(TempPort^, @TempPort^.VarString.dwStringSize);
DevId:= TempPort^.DeviceID;
Result:= DevID;
end;
if DeviceClass = 'comm' then
if TempPort^.VarString.dwStringFormat = STRINGFORMAT_ASCII then
begin
if TempPort^.VarString.dwStringSize <> 0 then
DeviceName:= RecordGetStr(TempPort^, @TempPort^.VarString.dwStringSize);
DevID:= TempPort^.DeviceID;
Result:= DevID;
end;
TriggerDeviceIDEvent(DeviceClass, DeviceName, FError, DevID);
finally
FreeMem(TempPort);
end;
end;
After porting from Delphi 7.0, this part of the method returned the constant LINERR_INVALDEVCLASS with the result that the recognition of the modem failed. It took some time before finding that with TAPI 2.0, applications that explicitly call the generic (neither "W" or "A" suffix) version of a this function will execute the ANSI version, for compatibility with previous versions of TAPI. The solution was to use lineGetIDW() instead of lineGetID().
RecordGetString()
This is first method that I had to modify in order to execute the component correctly. It manipulates character data from the LINEDEVCAPS structure. Its code is as follows:
with is called as follows:
DeviceName:= RecordGetStr(fLineDevices[i].pDevCaps^, @fLineDevices[i].pDevCaps^.dwLineNameSize);
where
- the Data argument is fLineDevices[i].pDevCaps^ that points to the pDevCaps member (it points to a TLineDevCaps record) of the TLineDevice object; and
- the Field argument is @fLineDevices[i].pDevCaps^.dwLineNameSize. It is the address of the dwLineNameSize (a DWORD type) of the pDevCaps member of the TLineDevice object which contains the device capability record that is filled from the device earlier in the program.
First, the method processes the Field argument. Field is a pointer (an address) on the dwLineNameSize member of the pDevCaps member of the TLineDeviceRecord so that dereferencing it result in an Integer type assigned to the local variable Len.
In the next statement, PInt(Field) is increased so that it points to the integer next to Field which is the dwLineOffset member of the device capability record.
Note that
- dwLineNameSize is the size in bytes of the variably sized device field containing a user configurable name for this line device; and
- dwLineOffset is the offset in bytes from the beginning of this data structure;
This having been said, then, what does the statement
if (PInt(Field)^ <> 0) then
SetString(Result, PAnsiChar(Longint(@Data) + PInt(Field)^), Len - 1) // Delphi 2009
do? If the offset is different from zero, the content of this variably sized field containing the name of the line device is transformed into an PAnsiChar and is transfered into the Result of the method.
Conclusion
All this work was performed for a project called "The MailBox project" that aimed at developing an in-process server that would provide dialing, CallerID and answering machine capabilities to another of my applications. This project was abandonned later since I could not obtain reliable operation with my modem.
You can download the code from here.
Annexes
References
- "Delphi and TAPI Part III: Wrapping Up Telephony by Allen Moore and Ken Kyler, Delphi Informant, September 1998 (no more references to this article on the Web).
- "The Tomes of Delphi - Basic 32-bit Communications Programming" by Allen C. Moore and John C. Penman
Variable structures
Variable structures such as TLINEDEVCAPS (used in EnumerateDevices()) and TLINECALLINFO (used in LineCallBack() to handle the events LINECALLINFOSTATE_CALLERID and LINE_APPNEWCALL) can be very tricky in TAPI, since they often vary in size from one version to the next. It is quite possible to pass a size that is too small in the dwTotalSize parameter. When this happens, an error occurs and the situation can be handled by reallocating memory based on the dwNeededSize parameter.
Here follows an example using the TLINEDEVCAPS structure of how I have tackled such situation.The structure t is declared as follows:
TLineDevCaps = packed record
dwTotalSize, // The total size in bytes allocated to this data structure
dwNeededSize, // The size(bytes) for this structure that is needed to hold all the returned information.
dwUsedSize, // The size in bytes of the portion of this data structure that contains useful information.
... // all other fields removed
end;
and the memory allocation proceeds as follows.
function TgtroCustomTAPI.AllocLineDevCaps(hLineApp: HLINEAPP; dwDeviceID,
dwAPIVersion, dwExtVersion: DWORD): PLINEDEVCAPS;
// Called by EnumerateDevices
var
AllocSize: Integer;
rc: longint;
begin
AllocSize:= SizeOf(TLINEDEVCAPS);
Result:= AllocMem(AllocSize); // will be filled with zeros
Result^.dwTotalSize:= AllocSize;
rc:= lineGetDevCaps(hLineApp, dwDeviceID,
dwAPIVersion, dwExtVersion, Result); // get call info
if Result^.dwNeededSize > Result^.dwTotalSize then
begin
AllocSize:= Result^.dwNeededSize;
ReallocMem(Result, AllocSize);
Result^.dwTotalSize:= AllocSize;
rc:= lineGetDevCaps(hLineApp, dwDeviceID, dwAPIVersion, dwExtVersion, Result);
end;
TapiCheck(rc); // raise exception if TAPI failed
end;
As one can see, a first memory allocation is tried using the size of the TLINEDEVCAPS structure and assigning this size to the dwTotalSize member of the structure. After this memory allocation, the dwNeededSize member is read and compared to the size of the first allocation. Given that more memory is needed, the memory is reallocated with the value of the dwNeededSize. Finally, the value of the return value of the functionlineGetDevCaps() is checked for errors.
Selection of the modem
The EnumerateDevices() method does not only enumerate the line devices found by lineInitializeEx() and exhibits their capabilities, is selects the line device associated with the com port using the private method TestDevice().
function TGtroCustomTAPI.TestDevice(i: Integer): DWORD;
var
LineOpenResult : longint;
ALine: HLINE;
begin
Result:= LINEMAPPER;
// Open line to get valid Port Handle : June 27, 1998
LineOpenResult := LineOpen(LineApp, i, FDevices[i].Handle,
FDevices[i].dwAPIVersion, 0, 0, LINECALLPRIVILEGE_NONE, 0, nil);
if LineOpenResult = 0 then // the line is open
begin
// get valid modem handle
FDevices[i].LineIsOpen:= True;
Result:= GetDeviceID(LINECALLSELECT_LINE, 'comm', i);
LineClose(ALine);
FDevices[i].LineIsOpen:= False;
end
end;
This method tries to open a line and quits if it fails. If it succeeds, it calls the method GetDeviceID():
function TgtroCustomTAPI.GetDeviceID(LineCallSelect: DWORD; DeviceClass: PChar;
i: Integer): DWORD;
// to return the device ID for "comm", "wave/in" and "wave/out".
type
LPPort = ^TPort;
TPort = record
VarString: TVarString;
// good trick as this becomes the additional param provided by TAPI
DeviceID: DWORD;
szDeviceName: array [1..1] of Char; // don't know why!!!
end;
var
TempPort: LPPort; PortSize: LongInt; FError: longint;
DeviceName: string; DevID: DWORD; FDevice: TDevice;
begin
FDevice:= FDevices[i];
DeviceName:= 'Unknown';
DevID:= UINT(-1);
Result := LINEMAPPER; // so failure can be tested and defaulted
if not FDevices[i].LineIsOpen then Exit; // no line open => bail out
PortSize := sizeof(TPort);
GetMem(TempPort, PortSize);
TempPort^.VarString.dwTotalSize := PortSize;
try
repeat
FError := lineGetID(
FDevice.Handle^, // current value of the line handle
0, // ignored
FDevice.Call^, // ignored
LineCallSelect,
pVarString(TempPort), // pointer to a memory location of type VARSTRING, where the device ID is returned.
DeviceClass);
if FError < 0 then
begin
TriggerDeviceIDEvent(DeviceClass, DeviceName, FError, DevID); // added by me
Exit;
end; // if FError
if (TempPort^.VarString.dwNeededSize >
TempPort^.VarString.dwTotalSize) then
begin
PortSize := TempPort^.VarString.dwNeededSize;
FreeMem(TempPort);
GetMem(TempPort, PortSize);
TempPort^.VarString.dwTotalSize := PortSize;
FError:= -1;
end;
until FError = 0;
if (DeviceClass = 'wave/out') or (DeviceClass = 'wave/in') then
if TempPort^.VarString.dwStringFormat = STRINGFORMAT_BINARY then
begin
if TempPort^.VarString.dwStringSize <> 0 then
DeviceName:= RecordGetStr(TempPort^, @TempPort^.VarString.dwStringSize);
DevId:= TempPort^.DeviceID;
Result:= DevID;
end;
if DeviceClass = 'comm' then
if TempPort^.VarString.dwStringFormat = STRINGFORMAT_ASCII then
begin
if TempPort^.VarString.dwStringSize <> 0 then
DeviceName:= RecordGetStr(TempPort^, @TempPort^.VarString.dwStringSize);
DevID:= TempPort^.DeviceID;
Result:= DevID;
end;
TriggerDeviceIDEvent(DeviceClass, DeviceName, FError, DevID); // added by me
finally
FreeMem(TempPort);
end;
end;
The code of this function has been provided to Allen Moore and Ken Kyler (Reference 1) by Keith Anderson, Keith@PureScience.com and I have modified it to fit the purpose of this project. It opens a line for each line device (referred to by the argument i) et, in this case, it uses the 'comm' device class. Once the line is open, it uses the API function LineGetID() to get a device ID for the specified device class associated with the selected line (I used LINECALLSELECTLINE in the call to the function).
Upon successful completion of the function call, the pVarString(TempPort) argument is filled with the name of the device and its device ID. The format of the returned information depends on the method used by the device class API for naming devices. Prior to calling lineGetID(), I have set the dwTotalSize field of this structure to indicate the amount of memory available to TAPI for returning information. Since the structure has variable size, room has been left for an investigation of the dwNeededSize field and a retry of lineGetID() once the right size of the structure has been determined.
Obviously, the method fails when there is no true line device attached to the line (the DevID is then set to LINEMAPPER). When it succeeds, it returns a DWORD that can be cast to the modem handle as THandle(DevID). Note that this method can also be used for other device classes as it does for 'wave/in" and 'wave/out' in other circumstances in the component.
The code of the RecordGetStr() method used to extract the the DeviceName reads as follows:
function RecordGetStr(var Data; Field : Pointer) : string;
var
Len: Longint;
begin
Len := PInt(Field)^; // PInt is defined in Windows.pas
Inc(PInt(Field));
if (PInt(Field)^ <> 0) then
SetString(Result, PChar(Longint(@Data) + PInt(Field)^), Len - 1)
else
Result:='';
end;