轉貼
利用語音Modem實現電話點播和留言功能
作者:陳省
有一段時間沒有更新網站了,最近挺忙的,所以寫書的進度慢了一些,兩周隻寫了10多頁設計模式相關的內容。希望在接下來的幾周能加快進度,趕緊弄完。另外前兩天,我被評為了Borland Delphi產品專家,加上這兩天北京的非典形勢也緩和多了,很高興。為此公開很久以前寫的一篇文章,與大家分享一下我的快樂。
偶然的起因
記ji得de還hai是shi在zai去qu年nian情qing人ren節jie的de時shi候hou,當dang時shi一yi直zhi在zai為wei給gei女nv朋peng友you送song什shen麼me禮li物wu而er發fa愁chou,覺jiao得de送song花hua實shi在zai沒mei有you什shen麼me創chuang意yi,可ke又you不bu知zhi道dao什shen麼me樣yang的de禮li物wu即ji能neng給gei她ta一yi個ge驚jing喜xi同tong事shi又you不bu昂ang貴gui。這zhe時shi,我wo的de一yi個ge好hao朋peng友you出chu了le一yi個ge主zhu意yi,說shuo不bu如ru電dian話hua點dian歌ge吧ba,還hai比bi較jiao特te別bie。可ke是shi如ru果guo是shi通tong過guo電dian台tai點dian歌ge後hou,再zai告gao訴su她ta收shou聽ting的de話hua就jiu起qi不bu到dao意yi外wai的de效xiao果guo了le。
就在沒有什麼好辦法的時候,我在Delphiluntanshangxiaguangdeshihou,yigerentichudewentituranqifalewo,wentishiguanyuruguobianchengshixianyuyinliuyanhedianhuaanjiandejilugongnengde。woturanxiangweishenmewobunengxieyigechengxulaikongzhidianhua,ranhouzaigeinvyoudayigechuanhu,rangtahuidianhua,dangdianhuajietonghou,wodechengxuxianbofangyiduanshixianluzhihaodehua,tishitatongguodianhuaanjianlaixuange,bingnengtigongliuyandegongnengne。zhuyiyiding,wojiuganmangzhayuezhefangmiandeziliaole,yikaishipengyoumengaosukeyitongguoyuyinkalaishixianzhexiegongneng,keshiyuyinkabijiaogui,erqiewomailehou,chuleyongyiciyiwaiyihouyebuhuijingchangyongdao,shizaishiyoudianlangfei,houlaiwangyoucced提到他聽人說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;
當(dang)用(yong)戶(hu)打(da)入(ru)被(bei)監(jian)控(kong)的(de)電(dian)話(hua)後(hou),會(hui)激(ji)發(fa)這(zhe)個(ge)事(shi)件(jian),程(cheng)序(xu)應(ying)該(gai)在(zai)用(yong)戶(hu)接(jie)入(ru)後(hou)播(bo)放(fang)提(ti)示(shi)語(yu)音(yin),提(ti)示(shi)用(yong)戶(hu)按(an)不(bu)同(tong)功(gong)能(neng)鍵(jian)來(lai)點(dian)歌(ge)或(huo)留(liu)言(yan)。程(cheng)序(xu)設(she)置(zhi)當(dang)前(qian)狀(zhuang)態(tai)為(wei)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,有you人ren可ke能neng要yao問wen,沒mei看kan到dao用yong來lai錄lu音yin的de代dai碼ma呀ya,這zhe部bu分fen其qi實shi是shi實shi現xian在zai另ling外wai的de事shi件jian中zhong的de,我wo們men稍shao後hou就jiu會hui講jiang到dao。再zai來lai看kan點dian歌ge部bu分fen,同tong樣yang的de根gen據ju按an鍵jian的de組zu合he在zai先xian前qian加jia載zai進jinCommandList的(de)字(zi)符(fu)串(chuan)列(lie)表(biao)中(zhong)查(zha)找(zhao)相(xiang)匹(pi)配(pei)的(de)歌(ge)曲(qu),如(ru)果(guo)有(you)相(xiang)應(ying)的(de)歌(ge)曲(qu)就(jiu)播(bo)放(fang),否(fou)則(ze)播(bo)放(fang)錯(cuo)誤(wu)提(ti)示(shi)語(yu)音(yin),提(ti)示(shi)用(yong)戶(hu)重(zhong)新(xin)輸(shu)入(ru)命(ming)令(ling),然(ran)後(hou)將(jiang)按(an)鍵(jian)清(qing)空(kong)等(deng)待(dai)重(zhong)新(xin)輸(shu)入(ru)。另(ling)外(wai)注(zhu)意(yi)在(zai)事(shi)件(jian)的(de)日(ri)誌(zhi)記(ji)錄(lu)中(zhong)我(wo)記(ji)錄(lu)了(le)ApdTapiDevice.calleridname和CallerID的de屬shu性xing,它ta們men對dui應ying的de是shi打da入ru電dian話hua的de號hao碼ma,不bu過guo這zhe項xiang功gong能neng隻zhi對dui開kai通tong了le來lai電dian顯xian示shi功gong能neng的de電dian話hua號hao碼ma才cai有you效xiao,通tong過guo對dui打da入ru電dian話hua號hao碼ma信xin息xi的de處chu理li,我wo們men可ke以yi提ti供gong一yi些xie額e外wai的de功gong能neng,不bu過guo這zhe是shi題ti外wai話hua了le。
前麵提到了在按鍵處理事件中我們並沒有進行留言的錄製功能,這主要是因為我們要保證留言提示語音不被按鍵中斷(設定Interruptwave:=false),因此把留言錄製功能放到了TApdTapiDevice的OnWaveNotify事(shi)件(jian)中(zhong)了(le),這(zhe)個(ge)事(shi)件(jian)可(ke)以(yi)提(ti)示(shi)波(bo)文(wen)件(jian)播(bo)放(fang)的(de)狀(zhuang)態(tai),比(bi)如(ru)播(bo)放(fang)結(jie)束(shu)和(he)錄(lu)音(yin)所(suo)需(xu)聲(sheng)音(yin)數(shu)據(ju)準(zhun)備(bei)狀(zhuang)態(tai)等(deng),在(zai)本(ben)程(cheng)序(xu)中(zhong)我(wo)們(men)需(xu)要(yao)在(zai)提(ti)示(shi)語(yu)音(yin)播(bo)放(fang)結(jie)束(shu)後(hou),開(kai)始(shi)記(ji)錄(lu)留(liu)言(yan),並(bing)在(zai)留(liu)言(yan)聲(sheng)音(yin)數(shu)據(ju)準(zhun)備(bei)好(hao)後(hou),將(jiang)其(qi)保(bao)存(cun)到(dao)磁(ci)盤(pan)文(wen)件(jian)中(zhong)。下(xia)麵(mian)是(shi)處(chu)理(li)過(guo)程(cheng)的(de)流(liu)程(cheng):
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狀(zhuang)態(tai)時(shi),表(biao)明(ming)錄(lu)音(yin)數(shu)據(ju)已(yi)經(jing)可(ke)以(yi)存(cun)盤(pan)了(le),這(zhe)時(shi)根(gen)據(ju)當(dang)前(qian)時(shi)間(jian)生(sheng)成(cheng)一(yi)個(ge)文(wen)件(jian)名(ming),並(bing)將(jiang)數(shu)據(ju)保(bao)存(cun)為(wei)波(bo)文(wen)件(jian)。而(er)當(dang)錄(lu)音(yin)結(jie)束(shu)後(hou),我(wo)們(men)就(jiu)需(xu)要(yao)調(tiao)用(yong)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上下載。