全國最多中醫師線上諮詢網站-台灣中醫網
發文 回覆 瀏覽次數:3738
推到 Plurk!
推到 Facebook!

delphi 利用語音Modem實現電話點播和留言功能

 
conundrum
尊榮會員


發表:893
回覆:1272
積分:643
註冊:2004-01-06

發送簡訊給我
#1 引用回覆 回覆 發表時間:2004-08-15 13:23:53 IP:61.221.xxx.xxx 未訂閱
http://www.cn-cio.org/show.php?article_id=2334&col=art delphi 利用語音Modem實現電話點播和留言功能 提供者:jzj 發表日期:2003-12-06 ________________________________________ 作者:陳省 發表日期:2003年9月24日 有一段時間沒有更新網站了,最近挺忙的,所以寫書的進度慢了一些,兩周只寫了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上下載。
p561
一般會員


發表:0
回覆:1
積分:0
註冊:2005-11-02

發送簡訊給我
#2 引用回覆 回覆 發表時間:2005-11-10 20:26:22 IP:222.77.xxx.xxx 未訂閱
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;     我需要设定Message := Line_CallState) then //编译时,到这里老出错误 Param1:=LineCallState_Disconnected 两个变量吗?
系統時間:2024-04-23 14:20:56
聯絡我們 | Delphi K.Top討論版
本站聲明
1. 本論壇為無營利行為之開放平台,所有文章都是由網友自行張貼,如牽涉到法律糾紛一切與本站無關。
2. 假如網友發表之內容涉及侵權,而損及您的利益,請立即通知版主刪除。
3. 請勿批評中華民國元首及政府或批評各政黨,是藍是綠本站無權干涉,但這裡不是政治性論壇!