C#中利用Socket实现网络语音通信[初级版本]
???? 開發平臺:.NET Framework 2.0 ,VS 2005,Windows XP,DirectX SDK(June 2008)下載頁面?。
???? 開發語言:C#。
???? 測試環境:Windows XP 、.net framework 2.0、普通局域網。
???? 測試結果:在多臺安裝了windows XP系統且配置不同的電腦上測試,均能正常運行。可以進行語音對話,但是有明顯的雜音,沿時低。
???? 限于篇幅,在本文中會詳細介紹本人認為比較關健的問題,其它部分只做大概介紹,為了便于大家理解,可以先閱讀:
???? 1.DirectX編程:[初級]C# 中利用 DirectSound 錄音
???? 2.C# Socket編程筆記
???? 在本文中打算按照以下順序介紹:
?????1.項目結果預覽與說明
?????2.實現方法概要
???? 3.語言采集
???? 4.語音傳輸
?????5.語音播放
?????項目結果預覽與說明
????? 界面如下:
???????????????????
????? 說明:界面很簡單,只提供了一個選擇或輸入對方IP的功能,當選擇合適局域網內IP之后,單擊確定便激活了語音聊天的按鈕。如果你想進行語音聊天就可以開始聊天了,聊天端口采用8000。本軟件只適用于局域網內用戶的聊天,另外因為沒有增加用戶認證的功能,所以只有在雙方都啟動了這款軟件才能進行通信。如果只想在單機上測試,那只需要選擇本機的IP便可。由于囧于技術水平,嘗試N次之后,任不知如何才能正確地實現語音效果(如回聲消除、降噪等)來保障音質,因此在單機測試會有回聲干擾,囂叫聲比較嚴重,希望高手解囊。
??????實現方法概要
??????要想實現語音聊天,有幾個步驟是必須的(就是我不說,相信你應該也能想得到一些):
????? a 語音采集:采集的作用就是從你的麥克風中獲取數據,我采用DirectSound類來實現這個技術。參考:C# 中利用 DirectSound 錄音
???(b 語音編碼:利用語音編碼算法對采集到的話音進行壓縮編碼,進行編碼的目的是為了減少網絡帶寬的壓力。)
??????c 語音傳輸:將采集到的聲音傳輸到網絡上的其它主機,我采用Socket UDP方式來實現。參考:C# Socket編程筆記
???(d 語音解碼:如果所傳輸的語音進行過壓縮編碼,則必須對語音進行解碼,否則無法得到原始語音數據。)
??????e 語音播放:當對方通過網絡傳輸到本機時(,如果需要解碼則先執行d),進行實時播放。
????? 上面紅色標記的步驟,可以省略。在本軟件中,我并未采用這兩個步驟,因為當我采用了這兩個步驟后,發現語音時延異常的嚴重。我采用的編解碼算法是G.729,利用的是g729.dll庫文件,壓縮效果不錯,但是時延比較嚴重,可能是自己哪里沒有設置好。如果有朋友使用過該算法,且時延低的,希望不吝賜教。
??????接下來,重點介紹語音采集、語音傳輸、語音播放的實現。
??????語音采集
??????由于所實現的方法與錄音方法一致,因此不會著墨過多,如果你不能很好的理解,請先參考:C# 中利用 DirectSound 錄音
????? 與錄音不同的是,錄音我們需要建立一個WAVE文件來存儲這些采集到的數據,而在語音聊天中,則不需要存儲,當采集到一些數據后,就立刻發送出去,因此也不需要開辟很大的空間來存放PCM數據。
????? 我們先來回顧下采集的基本步驟:
????? 1. 設置PCM格式,設置相關的參數,如:采樣頻率、量化位數等。
????? 2. 建立采集用的設備對象,建立采集用的緩沖區對象。
????? 3. 設置緩沖區通知,設置通知被觸發后的事件。通知是用于當緩沖區的讀指針達到某預設位置時觸發通知事件,提醒我們可以對某部分的數據進行傳送了。
????? 4. 開始采集聲音。
????? 5. 當通知被觸發后,建立一個新的線程來處理數據傳送的事件。(建立一個新的線程,就是為了防止采集過程被中斷)。
????????///?<summary>
????????///?設置音頻格式,如采樣率等
????????///?</summary>
????????///?<returns>設置完成后的格式</returns>
????????private?WaveFormat?SetWaveFormat()
????????{
????????????WaveFormat?format?=?new?WaveFormat();
????????????format.FormatTag?=?WaveFormatTag.Pcm;//設置音頻類型
????????????format.SamplesPerSecond?=?11025;//采樣率(單位:赫茲)典型值:11025、22050、44100Hz
????????????format.BitsPerSample?=?16;//采樣位數
????????????format.Channels?=?1;//聲道
????????????format.BlockAlign?=?(short)(format.Channels?*?(format.BitsPerSample?/?8));//單位采樣點的字節數
????????????format.AverageBytesPerSecond?=?format.BlockAlign?*?format.SamplesPerSecond;
????????????return?format;
????????????//按照以上采樣規格,可知采樣1秒鐘的字節數為22050*2=44100B?約為?43K
????????}
????????///?<summary>
????????///?創建捕捉設備對象
????????///?</summary>
????????///?<returns>如果創建成功返回true</returns>
????????private?bool?CreateCaputerDevice()
????????{
????????????//首先要玫舉可用的捕捉設備
????????????CaptureDevicesCollection?capturedev?=?new?CaptureDevicesCollection();
????????????Guid?devguid;
????????????if?(capturedev.Count?>?0)
????????????{
????????????????devguid?=?capturedev[0].DriverGuid;
????????????}
????????????else
????????????{
????????????????System.Windows.Forms.MessageBox.Show("當前沒有可用于音頻捕捉的設備",?"系統提示");
????????????????return?false;
????????????}
????????????//利用設備GUID來建立一個捕捉設備對象
????????????capture?=?new?Capture(devguid);
????????????return?true;
????????}
????????///?<summary>
????????///?創建捕捉緩沖區對象
????????///?</summary>
????????private?void?CreateCaptureBuffer()
????????{
????????????//想要創建一個捕捉緩沖區必須要兩個參數:緩沖區信息(描述這個緩沖區中的格式等),緩沖設備。
????????????WaveFormat?mWavFormat?=?SetWaveFormat();
????????????CaptureBufferDescription?bufferdescription?=?new?CaptureBufferDescription();
????????????bufferdescription.Format?=?mWavFormat;//設置緩沖區要捕捉的數據格式
????????????iNotifySize?=?mWavFormat.AverageBytesPerSecond?/?iNotifyNum;//1秒的數據量/設置的通知數得到的每個通知大小小于0.2s的數據量,話音延遲小于200ms為優質話音
????????????iBufferSize?=?iNotifyNum?*?iNotifySize;
????????????bufferdescription.BufferBytes?=?iBufferSize;
????????????bufferdescription.ControlEffects?=?true;
????????????bufferdescription.WaveMapped?=?true;
????????????capturebuffer?=?new?CaptureBuffer(bufferdescription,?capture);//建立設備緩沖區對象
????????}
????????//設置通知
????????private?void?CreateNotification()
????????{
????????????BufferPositionNotify[]?bpn?=?new?BufferPositionNotify[iNotifyNum];//設置緩沖區通知個數
????????????//設置通知事件
????????????notifyEvent?=?new?AutoResetEvent(false);
????????????notifyThread?=?new?Thread(RecoData);//通知觸發事件
????????????notifyThread.IsBackground?=?true;
????????????notifyThread.Start();
????????????for?(int?i?=?0;?i?<?iNotifyNum;?i++)
????????????{
????????????????bpn[i].Offset?=?iNotifySize?+?i?*?iNotifySize?-?1;//設置具體每個的位置
????????????????bpn[i].EventNotifyHandle?=?notifyEvent.Handle;
????????????}
????????????myNotify?=?new?Notify(capturebuffer);
????????????myNotify.SetNotificationPositions(bpn);
????????}
????????//線程中的事件
????????private?void?RecoData()
????????{
????????????while?(true)
????????????{
????????????????//?等待緩沖區的通知消息
????????????????notifyEvent.WaitOne(Timeout.Infinite,?true);
????????????????//?錄制數據
????????????????RecordCapturedData(Client,epServer);
????????????}
????????}
????????//真正轉移數據的事件,其實就是把數據傳送到網絡上去。
????????private?void?RecordCapturedData(Socket?Client,EndPoint?epServer?)
????????{
????????????byte[]?capturedata?=?null;
????????????int?readpos?=?0,?capturepos?=?0,?locksize?=?0;
????????????capturebuffer.GetCurrentPosition(out?capturepos,?out?readpos);
????????????locksize?=?readpos?-?iBufferOffset;//這個大小就是我們可以安全讀取的大小
????????????if?(locksize?==?0)
????????????{
????????????????return;
????????????}
????????????if?(locksize?<?0)
????????????{//因為我們是循環的使用緩沖區,所以有一種情況下為負:當文以載讀指針回到第一個通知點,而Ibuffeoffset還在最后一個通知處
????????????????locksize?+=?iBufferSize;
????????????}
????????????capturedata?=?(byte[])capturebuffer.Read(iBufferOffset,?typeof(byte),?LockFlag.FromWriteCursor,?locksize);
????????????//capturedata?=?g729.Encode(capturedata);//語音編碼
????????????try
????????????{
????????????????Client.SendTo(capturedata,?epServer);//傳送語音
????????????}
????????????catch
????????????{
????????????????throw?new?Exception();
????????????}
????????????iBufferOffset?+=?capturedata.Length;
????????????iBufferOffset?%=?iBufferSize;//取模是因為緩沖區是循環的。
????????}
??????上述代碼可以很好的采集到聲音數據,幾乎與原始聲音一致。如果你已經可以實現錄音,那么以上對你來說應該并不陌生。
??????語音傳輸
????? 這部分并不是很難,如果你熟悉socket編程,那么就可以PASS這一部分了,與以往傳輸不同的只是現在傳輸的是語音而已。如果你沒接觸過socket,那可以瞧瞧C# Socket編程筆記。
????? 感覺這部分叫“語音傳輸”并不是很恰當,因為其實真正用于傳輸的語句只有一句。除了語音傳輸之外,我們還需要對網絡進行監聽,從而能捕獲對方發送給自己的語音信息。但是,也不知道叫什么好,就估且這么叫著吧。在這一部分,我主要講下大致流程。
????? 1. 建立socket對象,在實例化這個對象的時候有一個參數是設置使用的協議,在本軟件中,我采用的是UDP。
????? 為什么要采用UDP?建立TCP能不能傳送語音,答案肯定是能的。在本軟件中,我考慮的主要是語音延時問題,?采用TCP在建立連接和維護連接中對時間和系統資源的開銷較大,因此會有明顯的時延發生,嚴重影響了實時性。另外,因為UDP是無連接的,這使得采用UDP可以支持日后功能上的擴展(如:組播)。
????? 2. 綁定本機的IP和端口,因為一個主機可能會有不止一個IP地址,如回發地址:127.0.0.1 和局域網地址:192.168.#.#。為了增加可用性,我這里選擇綁定到任何本機可用的IP地址(IPAddress.Any),而端口我們約定默認為8000。
????? 3. 啟動監聽線程,來監聽網絡。我采用異步的方式,以便獲得更好的系統響應度。
????????private?Thread?ListenThread;
????????private?byte[]?bytData;
????????///?<summary>
????????///?監聽方法,用于監聽遠程發送到本機的信息
????????///?</summary>
????????public?void?Listen()
????????{www.elivn.com
????????????ListenThread?=?new?Thread(new?ThreadStart(DoListen));
????????????ListenThread.IsBackground?=?true;//設置為后臺線程,這樣當主線程結束后,該線程自動結束
????????????ListenThread.Start();
????????}
????????private?EndPoint?epRemote;
????????///?<summary>
????????///?監聽線程
????????///?</summary>
????????private?void?DoListen()
????????{
????????????bytData?=?new?byte[intMaxDataSize];
????????????epRemote?=?(EndPoint)(new?IPEndPoint(IPAddress.Any,?0));
????????????while?(true)
????????????{
????????????????if?(LocalSocket.Poll(5000,?SelectMode.SelectRead))
????????????????{//每5ms查詢一下網絡,如果有可讀數據就接收
????????????????????LocalSocket.BeginReceiveFrom(bytData,?0,?bytData.Length,?SocketFlags.None,?ref?epRemote,?new?AsyncCallback(ReceiveData),?null);
????????????????}
????????????}
????????}
????????///?<summary>
????????///?接收數據
????????///?</summary>
????????///?<param?name="iar"></param>
????????private?void?ReceiveData(IAsyncResult?iar)
????????{
????????????int?intRecv?=?0;
????????????try
????????????{
????????????????intRecv?=?LocalSocket.EndReceiveFrom(iar,?ref?epRemote);
????????????}
????????????catch
????????????{
????????????????throw?new?Exception();
????????????}
????????????if?(intRecv?>?0)
????????????{
????????????????byte[]?bytReceivedData?=?new?byte[intRecv];
????????????????Buffer.BlockCopy(bytData,?0,?bytReceivedData,?0,?intRecv);
????????????????voicecapture1.GetVoiceData(intRecv,?bytReceivedData);//調用聲音模塊中的GetVoiceData()從字節數組中獲取聲音并播放
??????????????????//GetVoiceData()會在下一部分中提到
????????????}
????????}
????? 4. 數據的發送因為只有一句話,所以我直接放在上一部分的語音采集中了。 Client.SendTo(capturedata,?epServer);//傳送語音
????? 語音播放
??????最麻煩的就是這部分了,而且感覺現在的實現方法仍然需要改進才好。
????? 當聲音傳輸到本機后,該怎么樣才能讓這些數據經過音響設備放出聲音來呢?因為聲音播放是從緩沖區中獲取聲音數據的因此我們必須先將獲取到的數據寫入緩沖區,然后再調用相應的方法來播放。看起來似乎不復雜,可是實現起來遠沒有這么簡單。
????? 我遇到的問題:
??????大家可以看下語音采集部分,我是在每次通知后進行語音采集然后就將采集到的語音發送到網絡上,如果運行正常的話,這一部分數據實際播放長度遠小于1秒。也就是說對方每次接收到的語音長度為毫秒級。而且如果網絡質量可以的話,那么連續兩次接收到數據的時間間隔也是相當小的。這樣就產生問題了,如果我在接收到第一次數據后,將它寫入緩沖區,然后調用相應的播放方法,由于語音長度實際很短,因此幾乎聽不到什么效果,而且可能發生當第一次緩沖區中的數據還沒播放完,就已經被第二次的數據覆蓋,導致聲音混亂。經測試,此種方法無法達到聲音實時效果。期間我也曾修改過數據發送部分,希望當語音長度達到某一長度時在發送,可是問題依舊,看樣子重要的是在接收端進行相應處理。
??????直接緩沖播放的方法不行,那就換~~
?????上網搜,可惜的是這方面的資料實在有限,C#的就更少了。參考一些文獻,大家提到利用在緩沖區設置兩個指針,一個播放指針,一個寫指針(寫指針用于表示當前從網絡上接收到的數據從寫指針所指示位置開始往下寫,播放指針則表示當前所播放的數據末尾)。當播放指針達到某個位置時就播放某一部分數據,而不影響將被寫入的緩沖區部分,這樣就可以很好的解決數據覆蓋的問題。除此之外,還要將緩沖區設置為循環緩沖區,也就是頭尾相接,當到達尾部時,自己從部開始,此時將覆蓋頭部數據。
????? 看了這些,你是不是感覺很眼熟?是不是和語音采集很類似?是的,我們在捕捉緩沖區中就是這樣設置的,我們利用通知來設置觸發事件。不同的是我們接收語音用的緩沖區并不是捕捉緩沖區,MS為捕捉單獨設置了一個捕捉緩沖區。我們利用的是另一個緩沖區,輔助緩沖區(SecondaryBuffer)。后來發現該緩沖區也有類似的通知,這意味什么?我當時很興奮,可是~~相當郁悶的是,我不管怎么設置通知,編譯時都會報錯,到外詢求答案,均無果。在 MS 相關網站上咨詢后,有一位叫jwatte的答案,讓我又高興又失望:
????? 原話如下:
????? Notify is broken in DirectSound, has been for a long time, and probably will never be fixed.
????? The only way to know when you need to play the next piece of data is to check the play pointer each time through your main loop, and then lock the buffer and fill in whatever part has been played out.
????? Also, DirectSound is now in "maintenance" mode, and won't be further developed by Microsoft. Instead, for game applications, they recommend you use XAudio2 to play sound.
????? 簡單意思就是:Notify出問題已經很長時間了,而且MS可能永遠都不會去修復這個問題。而且他也為播放聲音提供了些建議,這些建議與上面所講的基本一致。
??????至于這個答案是否正確,因為無從考證,就不再討論了。如果哪位高手曾經實現過,希望賜教。
??????既然目前無法正常使用,就只能來手動寫了。這個方法名就是:GetVoiceData()。
??????思路如下:
????? ·利用MemoryStream來代表這個接收緩沖區。
????? ·設置兩個表示指針位置的字段:
????? ?? private int intPosWrite = 0;//內存流中寫指針位移
????? ?? private int intPosPlay = 0;//內存流中播放指針位移
??????·當接收到數據后,則移動寫指針,移動的長度為接收到的數據長度。
????? ·利用一個字段表示通知大小:private int intNotifySize = 5000;
??????·當寫指針的位置達到通知大小,則執行播放操作,然后移動播放指針到剛才的通知的位置。
????? ·如果當前寫指針的位移與將要寫入到緩沖區的數據大小相加后超過緩沖容量的,則進行摩爾運算,實現循環的效果。 ??????
?????? 這樣,基本上就可以實現語音聊天了。可是這樣的效果還只能是初步的,而且由于回聲的原因,相當影響音質,還可能產生囂叫,為了解決這個問題,我本打算采用MS提供的AEC算法,可是由于不知道如何實現,一直無法得到效果,因此這也是比較遺憾的地方。
??????
??????可執行文件(注:要在安裝了 .net framework 2.0 的平臺上運行):MatureVoiceEXE.rar
????? 源文件:MatureVoice.rar
??
????????private?int?intPosWrite?=?0;//內存流中寫指針位移
????????private?int?intPosPlay?=?0;//內存流中播放指針位移
????????private?int?intNotifySize?=?5000;//設置通知大小
????????///?<summary>
????????///?從字節數組中獲取音頻數據,并進行播放
????????///?</summary>
????????///?<param?name="intRecv">字節數組長度</param>
????????///?<param?name="bytRecv">包含音頻數據的字節數組</param>
????????public?void?GetVoiceData(int?intRecv,?byte[]?bytRecv)
????????{
????????????//intPosWrite指示最新的數據寫好后的末尾。intPosPlay指示本次播放開始的位置。
????????????if?(intPosWrite?+?intRecv?<=?memstream.Capacity)
????????????{//如果當前寫指針所在的位移+將要寫入到緩沖區的長度小于緩沖區總大小
????????????????if?((intPosWrite?-?intPosPlay?>=?0?&&?intPosWrite?-?intPosPlay?<?intNotifySize)?||?(intPosWrite?-?intPosPlay?<?0?&&?intPosWrite?-?intPosPlay?+?memstream.Capacity?<?intNotifySize))
????????????????{
????????????????????memstream.Write(bytRecv,?0,?intRecv);
????????????????????intPosWrite?+=?intRecv;
????????????????}
????????????????else?if?(intPosWrite?-?intPosPlay?>=?0)
????????????????{//先存儲一定量的數據,當達到一定數據量時就播放聲音。
????????????????????buffDiscript.BufferBytes?=?intPosWrite?-?intPosPlay;//緩沖區大小為播放指針到寫指針之間的距離。
????????????????????SecondaryBuffer?sec?=?new?SecondaryBuffer(buffDiscript,?PlayDev);//建立一個合適的緩沖區用于播放這段數據。
????????????????????memstream.Position?=?intPosPlay;//先將memstream的指針定位到這一次播放開始的位置
????????????????????sec.Write(0,?memstream,?intPosWrite?-?intPosPlay,?LockFlag.FromWriteCursor);
????????????????????sec.Play(0,?BufferPlayFlags.Default);
????????????????????memstream.Position?=?intPosWrite;//寫完后重新將memstream的指針定位到將要寫下去的位置。
????????????????????intPosPlay?=?intPosWrite;
????????????????}
????????????????else?if?(intPosWrite?-?intPosPlay?<?0)
????????????????{
????????????????????buffDiscript.BufferBytes?=?intPosWrite?-?intPosPlay?+?memstream.Capacity;//緩沖區大小為播放指針到寫指針之間的距離。
????????????????????SecondaryBuffer?sec?=?new?SecondaryBuffer(buffDiscript,?PlayDev);//建立一個合適的緩沖區用于播放這段數據。
????????????????????memstream.Position?=?intPosPlay;
????????????????????sec.Write(0,?memstream,?memstream.Capacity?-?intPosPlay,?LockFlag.FromWriteCursor);
????????????????????memstream.Position?=?0;
????????????????????sec.Write(memstream.Capacity?-?intPosPlay,?memstream,?intPosWrite,?LockFlag.FromWriteCursor);
????????????????????sec.Play(0,?BufferPlayFlags.Default);
????????????????????memstream.Position?=?intPosWrite;
????????????????????intPosPlay?=?intPosWrite;
????????????????}
????????????}
????????????else
????????????{//當數據將要大于memstream可容納的大小時
????????????????int?irest?=?memstream.Capacity?-?intPosWrite;//memstream中剩下的可容納的字節數。
????????????????memstream.Write(bytRecv,?0,?irest);//先寫完這個內存流。
????????????????memstream.Position?=?0;//然后讓新的數據從memstream的0位置開始記錄
????????????????memstream.Write(bytRecv,?irest,?intRecv?-?irest);//覆蓋舊的數據
????????????????intPosWrite?=?intRecv?-?irest;//更新寫指針位置。寫指針指示下一個開始寫入的位置而不是上一次結束的位置,因此不用減一
????????????}
????????}
轉載于:https://www.cnblogs.com/seoxs/archive/2011/04/20/2021839.html
總結
以上是生活随笔為你收集整理的C#中利用Socket实现网络语音通信[初级版本]的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 让iPhone自动帮你关闭音乐
- 下一篇: 太湖周围有哪些城市