首页  编辑  

语音Modem拨号和来电显示

Tags: /超级猛料/Network.网络通讯/FTP和拨号/   Date Created:

不管是内猫,外猫,只要它支持来电显示,接在电脑上,就可以装一个软件

,让打来的电话,显示的不仅是一个电话号码,还可以显示人名,比如:王小燕,张锦华,小明华,这样,比在电话机上只有电话号码要明确的多。功能强大的软件,还可以把某些电话打进来后自动挂机,播放语音,让对方留言。总之功能可以有许多种。但MODEM支持来电显示是最关键也是最重要的要求。其次,它还要安装有TAPI的支持。

  那怎么判断猫是不是支持来电显示:

  1,打开超级终端,随便输入一个连接名称,比如TEST。

  2,按确定后,下个窗口中,看到你的猫,不要输入电话号码。再下一步。

  3,在这个窗口中按取消。

  4,这样一个可以输入的空白窗口就有了。

  输入:AT回车

  如果出现OK,说明MODEM支持AT指令,不然,其他也不用试了。

  然后输入下面的命令(每条前面加上AT),只要一条反应有OK,就说明MODEM本身芯片支持来电显示。

  #CID=1

  %CCID=1

  +VCID=1

  #CC1

  *ID1

  5。如果有一条有OK,接下来,你打入电话,(事先接好电话线到MODEM)。如果你的猫真的支持来电显示,则会出现如下类似的内容:

  RING

  DATE = 0309

  TIME = 1800

  NMBR = 8005551212

  RING

  如果你的猫不支持来电显示,则此时可能会显示:

  RING

  RING

  RING

  还有的猫,什么也不显示。

  为什么有的猫支持,有的猫不支持?

  因为来电显示,不仅需要芯片组支持,还需要有外围电路的支持。现在厂商为了降低成本,大多数用户不知道有来电显示功能,商家也不知道,很少有人用,所以他们就把这些电路去掉,让猫成了一个只能上网,其他不能做什么的"笨猫","简化猫"。

---------------------------------------

用MODEM实现来电显示的方法

1、首先必须要求MODEM支持来电显示(即:CallerID),若已经安装好MODEM驱动软件(即MODEM

  AT命令集的解释表),就可以到系统注册表中去查找到激活来电显示的命令:

  HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Class\Modem\0000\EnableCallerID\1

  其中0000为WINDOWS给MODEM编的号,不同的MODEM在这里可能为0001,0002等。当然你可以查看

  HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Class\Modem\0000\Model

  是否为你所使用的MODEM。

  一般来说激活来电显示的AT命令为at#cid=1<cr>或at+vcid=1<cr>(<cr>代表回车键CHR(13))

  由于激活来电显示的命令不是一个普通的AT命令,MODEM一般不保存在缺省配置(即at&w0,at&w1)中

  故每次运行软件时均需要发此命令到MODEM。

2、当有来电时,在第一次振铃与第二次振铃之间,交换机会发送来电号码到MODEM:格式如下

Date = 0522

Time = 1632

NMBR = 13951982895

  你所要做的来电显示就是把NMBR = 后的号码截下来并显示出来,不用我说前面就是来电的日期与

  时间了。

---------------------------------------

可以做到,使用TAPI编程即可.

这是一个VB的例子:

Imports TAPI3Lib

Public Class cTAPI

Dim gobjTapi As TAPI

Dim WithEvents gobjTapiWithEvents As TAPI

Dim glRegistrationToken As Long

Dim gobjReceivedCallInfo As ITCallInfo

Dim gbSupportedCall As Boolean

Const TAPI3_CALL_EVENTS = TAPI_EVENT.TE_CALLMEDIA Or

TAPI_EVENT.TE_CALLNOTIFICATION Or TAPI_EVENT.TE_CALLSTATE

Public Sub New()

gobjTapi = New TAPI()

Call gobjTapi.Initialize()

Dim gobjAddress As ITAddress

Dim objCollAddresses As ITCollection

Dim bfound As Boolean

Dim indexAddr As Integer

Dim objCrtAddress As ITAddress

Dim objMediaSupport As ITMediaSupport

Dim objAddressCapabilities As ITAddressCapabilities

objCollAddresses = gobjTapi.Addresses

bfound = False

For indexAddr = 1 To objCollAddresses.Count

objCrtAddress = objCollAddresses.Item(indexAddr)

objMediaSupport = objCrtAddress

objAddressCapabilities = objCrtAddress

If objMediaSupport.QueryMediaType(TapiConstants.TAPIMEDIATYPE_AUDIO)

Then

bfound = True

End If

objAddressCapabilities = Nothing

objMediaSupport = Nothing

objCrtAddress = Nothing

If bfound = True Then Exit For

Next indexAddr

'gobjAddress = objCollAddresses.Item(indexAddr)

'musste ich rausnehmen, da sonst gobjTapi.RegisterCallNotifications

(ca Line

57) abraucht - bei mir wird bei

'indexAddr = 1 = 7 = 8 bfound = true, aber nur bei indexAddr = 8 wird

der

Handler registriert

gobjAddress = objCollAddresses.Item(8)

gobjTapi.EventFilter = TAPI3_CALL_EVENTS

gobjTapiWithEvents = gobjTapi

Dim fOwner As Boolean, fMonitor As Boolean

Dim lMediaTypes As Long, lCallbackInstance As Long

fOwner = True

fMonitor = False

lMediaTypes = TapiConstants.TAPIMEDIATYPE_AUDIO

lCallbackInstance = 1

glRegistrationToken = gobjTapi.RegisterCallNotifications( _

gobjAddress, _

fMonitor, _

fOwner, _

lMediaTypes, _

lCallbackInstance)

End Sub

Public Sub InstallListener()

End Sub

Private Sub gobjTapiWithEvents_Event(ByVal TapiEvent As

TAPI3Lib.TAPI_EVENT,

ByVal pEvent As Object) Handles gobjTapiWithEvents.Event

If TapiEvent = TAPI_EVENT.TE_CALLNOTIFICATION Then

Dim objCallNotificationEvent As ITCallNotificationEvent

objCallNotificationEvent = pEvent

Dim gobjReceivedCallInfo As ITCallInfo

gobjReceivedCallInfo = objCallNotificationEvent.Call

Dim objCallControl As ITBasicCallControl

objCallControl = gobjReceivedCallInfo

objCallControl.Answer()

End If

End Sub

End Class

还有一个通过回调实现

VOID FAR PASCAL MessageCallback(DWORD hDevice,            DWORD dwMsg,

                               DWORD dwCallbackInstance, DWORD dwParam1,

                               DWORD dwParam2,           DWORD dwParam3)

{

 bMessageCallBack = true;

 printf("%u\t%u\t%u\n",dwMsg,dwCallbackInstance,dwParam1);

 if (dwMsg == LINE_CALLINFO)

 {

   if (dwParam1 == LINECALLINFOSTATE_CALLERID)

   {

     LONG Res;

     char         *pCallInfo = new char[1024];

     LINECALLINFO *CallInfo  = reinterpret_cast<LINECALLINFO*>(pCallInfo);

     CallInfo->dwTotalSize = 1024;

     //Res = lineGetCallInfo(reinterpret_cast<HCALL>(hDevice), CallInfo);

     Res = lineGetCallInfo((HCALL)hDevice, CallInfo);

     char *CalledID = &pCallInfo[CallInfo->dwCallerIDOffset];

     printf("CallerID             : %s\n",

&pCallInfo[CallInfo->dwCallerIDOffset]);

     delete[] CallInfo;

   }

 }

}

以上程序都是在TAPI3.0下做的。

---------------------------------------

有一段时间没有更新网站了,最近挺忙的,所以写书的进度慢了一些,两周只写了10多页设计模式相关的内容。希望在接下来的几周能加快进度,赶紧弄完。另外前两天,我被评为了Borland Delphi产品专家,加上这两天北京的非典形势也缓和多了,很高兴。为此公开很久以前写的一篇文章,与大家分享一下我的快乐。

偶然的起因

记得还是在去年情人节的时候,当时一直在为给女朋友送什么礼物而发愁,觉得送花实在没有什么创意,可又不知道什么样的礼物即能给她一个惊喜同事又不昂贵。这时,我的一个好朋友出了一个主意,说不如电话点歌吧,还比较特别。可是如果是通过电台点歌后,再告诉她收听的话就起不到意外的效果了。

就在没有什么好办法的时候,我在Delphi论坛上瞎逛的时候,一个人提出的问题突然启发了我,问题是关于如果编程实现语音留言和电话按键的记录功能的。我突然想为什么我不能写一个程序来控制电话,然后再给女友打一个传呼,让她回电话,当电话接通后,我的程序先播放一段事先录制好的话,提示她通过电话按键来选歌,并能提供留言的功能呢。主意一定,我就赶忙查阅这方面的资料了,一开始朋友们告诉可以通过语音卡来实现这些功能,可是语音卡比较贵,而且我买了后,除了用一次以外以后也不会经常用到,实在是有点浪费,后来网友cced提到他听人说TurboPower公司出的Async Professional控件提供了一组基于Telephone Api的控件可以通过语音Modem来实现类似的功能。这个看来成本就低多了,我的Modem正好是语音Modem,于是我就下载了Async Professional(官方网址www.turbopower.com)试验了一下,果然不同反响,便宜且简单。

开始设计

下面我们就来看看如何利用这组控件实现语音功能,对于我们程序的应用来说,只需要使用两个TAPI控件TApdComPort和TApdTapiDevice即可,其中TApdComPort控件是一个串口通讯控件,因为Modem是同串口相连接的,因此需要串口通讯控件来进行控制。而TapdTapiDevice则是提供语音功能的核心控件。

首先,新建一个程序项目,在窗体上放置一个TApdComport控件,设置其属性为AutoOpen:=False;TapiMode=tmOn;这里TapiMode 设定为tmOn 表明TApdComPort 将由同其关联的TApdTapiDevice.控件来控制,而将AutoOpen设定为False 是因为串口的打开和关闭现在可以完全由TAPI来控制了。

然后,在窗体上放置一个TApdTapiDevice控件,设定其Comport属性为前面的TApdComPort控件。设定AnswerOnRing属性为1,表明第一次振铃后就开始由程序控制电话的应答。设定ShowTapiDevices为True表明当调用控件的SelectDevice方法时,会显示一个选择TAPI设备的对话框。ShowPorts属性为false,表明调用SelectDevice方法不会显示串行口列表。

接下来,本程序主要是采用有限状态机来控制流程的,下面我们来定义枚举状态

Type

TCurrentState = (csIdle, csWaiting, csConnected, csPlaying, csRecording, sDisconnected);

其中csIdle状态表示电话处于空闲状态,正等待接入。csWaiting则表示电话处于程序控制下,等待接入,如果有电话打入,程序会自动应答。csConnected则表示有电话打入,处于连接状态,csRecording则用来表示当前处于记录电话留言状态。csDisconnected则表示当前连接挂断了。

程序初始化

下面就是程序的OnCreate的事件处理函数,非常简单,就是先设置当前状态为csIdle,并设置ApdTapiDevice控件的TrimSeconds属性为5,表示当录音时如果有5秒的沉默时间就挂断。

procedure TFrmMain.FormCreate(Sender: TObject);

var

TeleIni: TIniFile;

begin

CurrentState := csIdle;

ApdTapiDevice.TrimSeconds := 5; //录音时有5秒静音就挂断

CommandList := TStringList.Create;

TeleIni := TIniFile.Create(ExtractFilePath(ParamStr(0)) + 'Tele.ini');

TeleIni.ReadSectionvalues('Commands', CommandList);

TeleIni.Free;

WindowState := wsMaximized;

end;

然后是将定义在Tele.Ini文件中的将要播放的声音列表文件目录加载到CommandList中。Tele.Ini的示例如下:

[Commands]

1#=1.wav

2#=2.wav

3#=3.wav

123#=E:Program FilesAPROExamplesBeep.wav

其中1#,表示当用户按下1和#号按键后,程序会播放其对应的1.wav文件。接下来就是我们要提供两个命令,一个是监控电话,一个是挂断电话,先在窗体上添加一个TlistBox,起名为LBSysInfo,然后添加两个菜单项,并同两个Action连接,编写Action的OnExecute事件处理函数:

//监控电话

procedure TFrmMain.ActionAnswerExecute(Sender: TObject);

begin

try

ApdTapiDevice.EnableVoice := True;

except

Application.MessageBox('当前设备不支持语音扩展', '错误', MB_OK);

end;

if ApdTapiDevice.EnableVoice then

begin

ApdTapiDevice.AutoAnswer;

LBSysInfo.Items.Add('answer:接听对方电话');

CurrentState := csWaiting;

end

end;

因为不是所有的Modem都支持语音功能,因此在监控电话接入前应该先判断设置ApdTapiDevice.EnableVoice := True;,如果出现异常,表明Modem不支持语音功能。如果支持的话,就调用AutoAnswer方法等待接入同时设置状态为csWaiting,并在列表框中写入日志。

//挂断电话

procedure TFrmMain.ActionCancelExecute(Sender: TObject);

begin

ApdTapiDevice.CancelCall;

LBSysInfo.Items.Add('cancel:挂断对方电话');

CurrentState := csIdle;

end;

挂断电话就简单多了,只要简单的调用TApdTapiDevice控件的CancelCall方法就可以了,还需要设置当前状态为csIdle。

如果系统中存在多个TAPI设备的时候,我们还可以选择使用哪一个来接听电话,下面是选择设备的方法:

//选择设备

procedure TFrmMain.ActionSelDevExecute(Sender: TObject);

begin

ApdTapiDevice.SelectDevice;

ApdTapiDevice.EnableVoice := True;

end;

事件驱动

Telephone API是基于事件驱动的,因此核心功能需要在事件处理函数中实现,先来看程序的TApdTapiDevice的OnConnect事件处理函数代码:

procedure TFrmMain.ApdTapiDeviceTapiConnect(Sender: TObject);

begin

CurrentState := csConnected;

LBSysInfo.Items.Add('Connect:连接成功');

ApdTapiDevice.PlayWaveFile('Greeting.wav');//播放功能提示语音

LBSysInfo.Items.Add('connect:播放greeting.wav');

end;

当用户打入被监控的电话后,会激发这个事件,程序应该在用户接入后播放提示语音,提示用户按不同功能键来点歌或留言。程序设置当前状态为csConnected,然后调用ApdTapiDevice的PlayWaveFile方法播放提示语音波文件。

要注意的是:不同Modem支持播放的波文件的格式是不同的,但它们都支持PCM 8位单声道的波文件,但这种类型波文件的音质非常差,用来播放歌曲效果实在糟糕,不过大多数语音Modem都支持音质更好的波文件格式,但通常都是 PCM格式的,比如我的Lucent Voice Modem就支持PCM 16位 单声道的波文件的播放。歌曲转化为波文件非常简单,我用Winamp将mp3文件通过Winamp本身的Disk Writer Plug-in插件直接将mp3转化成44位的波文件(通常为40-70M大小),然后在用一个叫goldwave的软件(我忘了从什么地方下载的了)将其转化为16位的单声道波文件(通常4-7M大小)。至于提示语音,我则是使用windows自带的录音机程序通过麦克风录制的。

当用户听完提示语音后,他们会按键来点歌或留言,而用户的按键会激发TApdTapiDevice的OnDTMF事件,我们就可以在这个事件中对按键进行处理,下面就是处理过程代码:

procedure TFrmMain.ApdTapiDeviceTapiDTMF(CP: TObject; Digit: Char;

ErrorCode: Integer);

begin

if (Digit = '') or (Digit = ' ') then

Exit;

LBSysInfo.Items.Add('dtmf:按键=' + Digit);

CurrentCommand := CurrentCommand + Digit;

{简单状态机}

if Digit = '#' then

begin

if CurrentCommand = '*#' then

begin

CurrentCommand := '';

ApdTapiDevice.MaxMessageLength := 30; //最长记录时间30秒

ApdTapiDevice.InterruptWave := False; //按键不能中断提示语音的播放

ApdTapiDevice.PlayWaveFile('recordhint.wav');//播放录音提示语音

CurrentState := csRecording;

Exit;

end;

if CommandList.values[CurrentCommand] <> '' then

begin

ApdTapiDevice.PlayWaveFile(CommandList.values[CurrentCommand]);

LBSysInfo.Items.Add(Format('%s %s 正在播放 %s',

[ApdTapiDevice.calleridname, apdtapidevice.callerid,

CommandList.values[CurrentCommand]]));

end

else

begin

//播放错误提示语音,并要求用户重新输入命令

ApdTapiDevice.PlayWaveFile('errorno.wav');

LBSysInfo.Items.Add(Format('%s %s 输入了错误的号码',

[ApdTapiDevice.calleridname, apdtapidevice.callerid]));

end;

//重置命令为空

CurrentCommand := '';

end;

end;

程序对按键进行判断(按键对应于digit参数),如果输入的为'*#'键,就进入录音功能,在录音前先播放提示语音,可以告诉用户留言长度为30秒,然后设置当前状态为csRecording,有人可能要问,没看到用来录音的代码呀,这部分其实是实现在另外的事件中的,我们稍后就会讲到。再来看点歌部分,同样的根据按键的组合在先前加载进CommandList的字符串列表中查找相匹配的歌曲,如果有相应的歌曲就播放,否则播放错误提示语音,提示用户重新输入命令,然后将按键清空等待重新输入。另外注意在事件的日志记录中我记录了ApdTapiDevice.calleridname和CallerID的属性,它们对应的是打入电话的号码,不过这项功能只对开通了来电显示功能的电话号码才有效,通过对打入电话号码信息的处理,我们可以提供一些额外的功能,不过这是题外话了。

前面提到了在按键处理事件中我们并没有进行留言的录制功能,这主要是因为我们要保证留言提示语音不被按键中断(设定Interruptwave:=false),因此把留言录制功能放到了TApdTapiDevice的OnWaveNotify事件中了,这个事件可以提示波文件播放的状态,比如播放结束和录音所需声音数据准备状态等,在本程序中我们需要在提示语音播放结束后,开始记录留言,并在留言声音数据准备好后,将其保存到磁盘文件中。下面是处理过程的流程:

procedure TFrmMain.ApdTapiDeviceTapiWaveNotify(CP: TObject;

Msg: TWaveMessage);

var

TimeStr: string;

FileName: string;

begin

//决不能在case外做耗时的操作

case Msg of

waPlayOpen: LBSysInfo.Items.Add('wavnotify:播放开始');

waPlayDone:

begin

LBSysInfo.Items.Add('wavnotify:播放结束');

if CurrentState = csRecording then

begin

try

      //等待波设备状态为wsIdle再开始录音

while ApdTapiDevice.WaveState <> wsIdle do

Application.ProcessMessages;

ApdTapiDevice.InterruptWave := True;

ApdTapiDevice.StartWaveRecord;

LBSysInfo.Items.Add('dtmf:录音成功');

except

LBSysInfo.Items.Add('dtmf:录音失败');

end;

end;

end;

waPlayClose: LBSysInfo.Items.Add('wavnotify:播放关闭');

waRecordOpen: LBSysInfo.Items.Add('wavnotify:录音开始');

waDataReady:

begin

LBSysInfo.Items.Add('wavnotify:数据准备');

TimeSeparator := '-';

FileName := DateTimeToStr(Now) + '.wav';

try

ApdTapiDevice.SaveWaveFile(ExtractFilePath(ParamStr(0)) + 'record' +

FileName, True);

LBSysInfo.Items.Add('wavNotify:保存声音文件 ' + FileName);

except

LBSysInfo.Items.Add('wavnotify:保存声音文件失败');

end;

end;

waRecordClose:

begin

LBSysInfo.Items.Add('wavnotify:记录声音结束');

CurrentState := csWaiting;

ActionCancelExecute(nil);

Timer1.Enabled := True;

end;

end;

end;

整个流程就是通过一个Case语句来判断当前声音状态,如果为waPlayDone(播放完毕),同事CurrentStatus为csRecording的话,就调用StartWaveRecord方法来记录声音。而当Msg为waDataReady状态时,表明录音数据已经可以存盘了,这时根据当前时间生成一个文件名,并将数据保存为波文件。而当录音结束后,我们就需要调用ActionCancelExecute(nil)来挂断电话,并将状态设置为csWaiting来等待下次接入,注意的在代码最后,我们将一个TTimer控件激活了。这个TTimer控件的时间间隔Interval设置为8秒,同时其OnTimer事件代码如下:

procedure TFrmMain.Timer1Timer(Sender: TObject);

begin

try

  //应答电话

ActionAnswerExecute(nil);

CurrentState := csWaiting;

Timer1.Enabled := False;

except

end;

end;

这样设置的原因在于,当调用CancelCall方法来挂断电话后,TAPI设备需要8秒来恢复正常状态,如果立刻执行AutoAnswer的话,这个方法就会失效,无法正确监控电话接入,因此要用TTimer来控制恢复电话应答的时间。

异常处理

要想程序非常健壮的反复应答电话接入,我们必须对用户突然挂断电话进行处理,用户断开的事件会激发控件的OnTapiStatus事件,当用户挂断电话时,我们要做的是如果当前还在录音,就停止录音,如果是在播放歌曲,就挂断电话,然后设置TTimer生效,重新进入电话应答状态。下面就是整个处理过程的代码:

procedure TFrmMain.ApdTapiDeviceTapiStatus(CP: TObject; First,

Last: Boolean; Device, Message, Param1, Param2, Param3: Cardinal);

begin

if (Message = Line_CallState) then

begin

case Param1 of

LineCallState_Disconnected:

begin

LBSysInfo.Items.Add('status:disconnected from remote modem');

if CurrentState = csRecording then

begin

ApdTapiDevice.StopWaveRecord;

Exit;

end;

CurrentState := csDisconnected;

ActionCancelExecute(nil);

Timer1.Enabled := True;

end;

end;

end;

end;

进一步完善

当录音完毕后,我们想听一下电话留言的话,可以在窗体上放置一个打开文件对话框,用下面代码实现:

procedure TFrmMain.ActionPlayRecExecute(Sender: TObject);

var

FrmPlay: TFrmPlayRec;

begin

DlgOpenRec.InitialDir := ExtractFilePath(ParamStr(0)) + 'Record';

if DlgOpenRec.Execute then

//播放声音记录文件

ShellExecute(Application.Handle, PChar('open'), PChar(DlgOpenRec.FileName),

nil, nil, SW_SHOW);

end;

另外,如果大家自信自己的歌喉不比那些歌星差的话,完全可以录制自己的歌声,然后播放给你的女朋友或朋友听,也许效果更棒:)。

最后,我要说的就是Telephone API所能提供的功能远远不止本文中所提到的,感兴趣的朋友可以进一步查阅相关资料来研究。

最后,要说的是Turbo Power已经不再开发Async Pro了,它把所有的源码都放到了Sourceforge上共享,大家可以到SourceForge上下载。