一偶然的起因 记得还是在去年情人节的时候当时一直在为给女朋友送什么礼物而发愁觉得送花实在没有什么创意可又不知道什么样的礼物即能给她一个惊喜同事又不昂贵这时我的一个好朋友出了一个主意说不如电话点歌吧还比较特别可是如果是通过电台点歌后再告诉她收听的话就起不到意外的效果了 就在没有什么好办法的时候我在Delphi论坛上瞎逛的时候一个人提出的问题突然启发了我问题是关于如果编程实现语音留言和电话按键的记录功能的我突然想为什么我不能写一个程序来控制电话然后再给女友打一个传呼让她回电话当电话接通后我的程序先播放一段事先录制好的话提示她通过电话按键来选歌并能提供留言的功能呢主意一定我就赶忙查阅这方面的资料了一开始朋友们告诉可以通过语音卡来实现这些功能可是语音卡比较贵而且我买了后除了用一次以外以后也不会经常用到实在是有点浪费后来网友cced提到他听人说TurboPower公司出的Async Professional控件提供了一组基于Telephone Api的控件可以通过语音Modem来实现类似的功能这个看来成本就低多了我的Modem正好是语音Modem于是我就下载了Async Professional(官方网wwwturbopowercom)试验了一下果然不同反响便宜且简单 二开始设计 下面我们就来看看如何利用这组控件实现语音功能对于我们程序的应用来说只需要使用两个TAPI控件TApdComPort和TApdTapiDevice即可其中TApdComPort控件是一个串口通讯控件因为Modem是同串口相连接的因此需要串口通讯控件来进行控制而TapdTapiDevice则是提供语音功能的核心控件 首先新建一个程序项目在窗体上放置一个TApdComport控件设置其属性为AutoOpen:=False;TapiMode=tmOn;这里TapiMode 设定为tmOn 表明TApdComPort 将由同其关联的TApdTapiDevice控件来控制而将AutoOpen设定为False 是因为串口的打开和关闭现在可以完全由TAPI来控制了 然后在窗体上放置一个TApdTapiDevice控件设定其Comport属性为前面的TApdComPort控件设定AnswerOnRing属性为表明第一次振铃后就开始由程序控制电话的应答设定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秒的沉默时间就挂断 procedure TFrmMainFormCreate(Sender: TObject); var TeleIni: TIniFile; begin CurrentState := csIdle; ApdTapiDeviceTrimSeconds := ; //录音时有秒静音就挂断 CommandList := TStringListCreate; TeleIni := TIniFileCreate(ExtractFilePath(ParamStr()) + Teleini); TeleIniReadSectionvalues(Commands CommandList); TeleIniFree; WindowState := wsMaximized; end; 然后是将定义在TeleIni文件中的将要播放的声音列表文件目录加载到CommandList中 TeleIni的示例如下 [Commands] #=wav #=wav #=wav #=E:\Program Files\APRO\Examples\Beepwav 其中#表示当用户按下和#号按键后程序会播放其对应的wav文件接下来就是我们要提供两个命令一个是监控电话一个是挂断电话先在窗体上添加一个TlistBox起名为LBSysInfo然后添加两个菜单项并同两个Action连接编写Action的OnExecute事件处理函数 //监控电话 procedure TFrmMainActionAnswerExecute(Sender: TObject); begin try ApdTapiDeviceEnableVoice := True; except ApplicationMessageBox(当前设备不支持语音扩展 错误 MB_OK); end; if ApdTapiDeviceEnableVoice then begin ApdTapiDeviceAutoAnswer; LBSysInfoItemsAdd(answer:接听对方电话); CurrentState := csWaiting; end end; 因为不是所有的Modem都支持语音功能因此在监控电话接入前应该先判断设置ApdTapiDeviceEnableVoice := True;如果出现异常表明Modem不支持语音功能如果支持的话就调用AutoAnswer方法等待接入同时设置状态为csWaiting并在列表框中写入日志 //挂断电话 procedure TFrmMainActionCancelExecute(Sender: TObject); begin ApdTapiDeviceCancelCall; LBSysInfoItemsAdd(cancel:挂断对方电话); CurrentState := csIdle; end; 挂断电话就简单多了只要简单的调用TApdTapiDevice控件的CancelCall方法就可以了还需要设置当前状态为csIdle 如果系统中存在多个TAPI设备的时候我们还可以选择使用哪一个来接听电话下面是选择设备的方法 //选择设备 procedure TFrmMainActionSelDevExecute(Sender: TObject); begin ApdTapiDeviceSelectDevice; ApdTapiDeviceEnableVoice := True; end; 事件驱动 Telephone API是基于事件驱动的因此核心功能需要在事件处理函数中实现先来看程序的TApdTapiDevice的OnConnect事件处理函数代码 procedure TFrmMainApdTapiDeviceTapiConnect(Sender: TObject); begin CurrentState := csConnected; LBSysInfoItemsAdd(Connect:连接成功); ApdTapiDevicePlayWaveFile(Greetingwav);//播放功能提示语音 LBSysInfoItemsAdd(connect:播放greetingwav); end; 当用户打入被监控的电话后会激发这个事件程序应该在用户接入后播放提示语音提示用户按不同功能键来点歌或留言程序设置当前状态为csConnected然后调用ApdTapiDevice的PlayWaveFile方法播放提示语音波文件 要注意的是不同Modem支持播放的波文件的格式是不同的但它们都支持PCM 位单声道的波文件但这种类型波文件的音质非常差用来播放歌曲效果实在糟糕不过大多数语音Modem都支持音质更好的波文件格式但通常都是 PCM格式的比如我的Lucent Voice Modem就支持PCM 位单声道的波文件的播放歌曲转化为波文件非常简单我用Winamp将mp文件通过Winamp本身的Disk Writer Plugin插件直接将mp转化成位的波文件(通常为M大小)然后再用一个叫goldwave的软件(我忘了从什么地方下载的了)将其转化为位的单声道波文件(通常M大小)至于提示语音我则是使用windows自带的录音机程序通过麦克风录制的 当用户听完提示语音后他们会按键来点歌或留言而用户的按键会激发TApdTapiDevice的OnDTMF事件我们就可以在这个事件中对按键进行处理下面就是处理过程代码 procedure TFrmMainApdTapiDeviceTapiDTMF(CP: TObject; Digit: Char; ErrorCode: Integer); begin if (Digit = ) or (Digit = ) then Exit; LBSysInfoItemsAdd(dtmf:按键= + Digit); CurrentCommand := CurrentCommand + Digit; {简单状态机} if Digit = # then begin if CurrentCommand = *# then begin CurrentCommand := ; ApdTapiDeviceMaxMessageLength := ; //最长记录时间秒 ApdTapiDeviceInterruptWave := False; //按键不能中断提示语音的播放 ApdTapiDevicePlayWaveFile(recordhintwav);//播放录音提示语音 CurrentState := csRecording; Exit; end; if CommandListvalues[CurrentCommand] <> then begin ApdTapiDevicePlayWaveFile(CommandListvalues[CurrentCommand]); LBSysInfoItemsAdd(Format(%s %s 正在播放 %s [ApdTapiDevicecalleridname apdtapidevicecallerid CommandListvalues[CurrentCommand]])); end else begin //播放错误提示语音并要求用户重新输入命令 ApdTapiDevicePlayWaveFile(errornowav); LBSysInfoItemsAdd(Format(%s %s 输入了错误的号码 [ApdTapiDevicecalleridname apdtapidevicecallerid])); end; //重置命令为空 CurrentCommand := ; end; end; 程序对按键进行判断(按键对应于digit参数)如果输入的为*#键就进入录音功能在录音前先播放提示语音可以告诉用户留言长度为秒然后设置当前状态为csRecording有人可能要问没看到用来录音的代码呀这部分其实是实现在另外的事件中的我们稍后就会讲到再来看点歌部分同样的根据按键的组合在先前加载进CommandList的字符串列表中查找相匹配的歌曲如果有相应的歌曲就播放否则播放错误提示语音提示用户重新输入命令然后将按键清空等待重新输入另外注意在事件的日志记录中我记录了ApdTapiDevicecalleridname和CallerID的属性它们对应的是打入电话的号码不过这项功能只对开通了来电显示功能的电话号码才有效通过对打入电话号码信息的处理我们可以提供一些额外的功能不过这是题外话了 前面提到了在按键处理事件中我们并没有进行留言的录制功能这主要是因为我们要保证留言提示语音不被按键中断(设定Interruptwave:=false)因此把留言录制功能放到了TApdTapiDevice的OnWaveNotify事件中了这个事件可以提示波文件播放的状态比如播放结束和录音所需声音数据准备状态等在本程序中我们需要在提示语音播放结束后开始记录留言并在留言声音数据准备好后将其保存到磁盘文件中下面是处理过程的流程 procedure TFrmMainApdTapiDeviceTapiWaveNotify(CP: TObject; Msg: TWaveMessage); var TimeStr: string; FileName: string; begin //决不能在case外做耗时的操作 case Msg of waPlayOpen: LBSysInfoItemsAdd(wavnotify:播放开始); waPlayDone: begin LBSysInfoItemsAdd(wavnotify:播放结束); if CurrentState = csRecording then begin try //等待波设备状态为wsIdle再开始录音 while ApdTapiDeviceWaveState <> wsIdle do ApplicationProcessMessages; ApdTapiDeviceInterruptWave := True; ApdTapiDeviceStartWaveRecord; LBSysInfoItemsAdd(dtmf:录音成功); except LBSysInfoItemsAdd(dtmf:录音失败); end; end; end; waPlayClose: LBSysInfoItemsAdd(wavnotify:播放关闭); waRecordOpen: LBSysInfoItemsAdd(wavnotify:录音开始); waDataReady: begin LBSysInfoItemsAdd(wavnotify:数据准备); TimeSeparator := ; FileName := DateTimeToStr(Now) + wav; try ApdTapiDeviceSaveWaveFile(ExtractFilePath(ParamStr()) + record\ + FileName True); LBSysInfoItemsAdd(wavNotify:保存声音文件 + FileName); except LBSysInfoItemsAdd(wavnotify:保存声音文件失败); end; end; waRecordClose: begin LBSysInfoItemsAdd(wavnotify:记录声音结束); CurrentState := csWaiting; ActionCancelExecute(nil); TimerEnabled := True; end; end; end; 整个流程就是通过一个Case语句来判断当前声音状态如果为waPlayDone(播放完毕)同时CurrentStatus为csRecording的话就调用StartWaveRecord方法来记录声音而当Msg为waDataReady状态时表明录音数据已经可以存盘了这时根据当前时间生成一个文件名并将数据保存为波文件而当录音结束后我们就需要调用ActionCancelExecute(nil)来挂断电话并将状态设置为csWaiting来等待下次接入注意在代码最后我们将一个TTimer控件激活了这个TTimer控件的时间间隔Interval设置为秒同时其OnTimer事件代码如下 procedure TFrmMainTimerTimer(Sender: TObject); begin try //应答电话 ActionAnswerExecute(nil); CurrentState := csWaiting; TimerEnabled := False; except end; end; 这样设置的原因在于当调用CancelCall方法来挂断电话后TAPI设备需要秒来恢复正常状态如果立刻执行AutoAnswer的话这个方法就会失效无法正确监控电话接入因此要用TTimer来控制恢复电话应答的时间 四异常处理 要想程序非常健壮的反复应答电话接入我们必须对用户突然挂断电话进行处理用户断开的事件会激发控件的OnTapiStatus事件当用户挂断电话时我们要做的是如果当前还在录音就停止录音如果是在播放歌曲就挂断电话然后设置TTimer生效重新进入电话应答状态下面就是整个处理过程的代码 procedure TFrmMainApdTapiDeviceTapiStatus(CP: TObject; First Last: Boolean; Device Message Param Param Param: Cardinal); begin if (Message = Line_CallState) then begin case Param of LineCallState_Disconnected: begin LBSysInfoItemsAdd(status:disconnected from remote modem); if CurrentState = csRecording then begin ApdTapiDeviceStopWaveRecord; Exit; end; CurrentState := csDisconnected; ActionCancelExecute(nil); TimerEnabled := True; end; end; end; end; 五进一步完善 当录音完毕后我们想听一下电话留言的话可以在窗体上放置一个打开文件对话框用下面代码实现 procedure TFrmMainActionPlayRecExecute(Sender: TObject); var FrmPlay: TFrmPlayRec; begin DlgOpenRecInitialDir := ExtractFilePath(ParamStr()) + Record\; if DlgOpenRecExecute then //播放声音记录文件 ShellExecute(ApplicationHandle PChar(open) PChar(DlgOpenRecFileName) nil nil SW_SHOW); end; 另外如果大家自信自己的歌喉不比那些歌星差的话完全可以录制自己的歌声然后播放给你的女朋友或朋友听也许效果更棒) 最后我要说的就是Telephone API所能提供的功能远远不止本文中所提到的感兴趣的朋友可以进一步查阅相关资料来研究还有Turbo Power已经不再开发Async Pro了它把所有的源码都放到了Sourceforge上共享大家可以到SourceForge上下载 |