首页  编辑  

利用语音Modem实现电话点播和留言功能

Tags: /超级猛料/Network.网络通讯/Modem/   Date Created:

利用语音Modem实现电话点播和留言功能

有一段时间没有更新网站了,最近挺忙的,所以写书的进度慢了一些,两周只写了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 Files\APRO\Examples\Beep.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上下载。

tpapro_4_06.zip (5.6MB)