C#中利用Socket实现网络语音通信[初级版本]
???? 開發(fā)平臺(tái):.NET Framework 2.0 ,VS 2005,Windows XP,DirectX SDK(June 2008)下載頁(yè)面?。
???? 開發(fā)語(yǔ)言:C#。
???? 測(cè)試環(huán)境:Windows XP 、.net framework 2.0、普通局域網(wǎng)。
???? 測(cè)試結(jié)果:在多臺(tái)安裝了windows XP系統(tǒng)且配置不同的電腦上測(cè)試,均能正常運(yùn)行。可以進(jìn)行語(yǔ)音對(duì)話,但是有明顯的雜音,沿時(shí)低。
???? 限于篇幅,在本文中會(huì)詳細(xì)介紹本人認(rèn)為比較關(guān)健的問(wèn)題,其它部分只做大概介紹,為了便于大家理解,可以先閱讀:
???? 1.DirectX編程:[初級(jí)]C# 中利用 DirectSound 錄音
???? 2.C# Socket編程筆記
???? 在本文中打算按照以下順序介紹:
?????1.項(xiàng)目結(jié)果預(yù)覽與說(shuō)明
?????2.實(shí)現(xiàn)方法概要
???? 3.語(yǔ)言采集
???? 4.語(yǔ)音傳輸
?????5.語(yǔ)音播放
?????項(xiàng)目結(jié)果預(yù)覽與說(shuō)明
????? 界面如下:
???????????????????
????? 說(shuō)明:界面很簡(jiǎn)單,只提供了一個(gè)選擇或輸入對(duì)方IP的功能,當(dāng)選擇合適局域網(wǎng)內(nèi)IP之后,單擊確定便激活了語(yǔ)音聊天的按鈕。如果你想進(jìn)行語(yǔ)音聊天就可以開始聊天了,聊天端口采用8000。本軟件只適用于局域網(wǎng)內(nèi)用戶的聊天,另外因?yàn)闆](méi)有增加用戶認(rèn)證的功能,所以只有在雙方都啟動(dòng)了這款軟件才能進(jìn)行通信。如果只想在單機(jī)上測(cè)試,那只需要選擇本機(jī)的IP便可。由于囧于技術(shù)水平,嘗試N次之后,任不知如何才能正確地實(shí)現(xiàn)語(yǔ)音效果(如回聲消除、降噪等)來(lái)保障音質(zhì),因此在單機(jī)測(cè)試會(huì)有回聲干擾,囂叫聲比較嚴(yán)重,希望高手解囊。
??????實(shí)現(xiàn)方法概要
??????要想實(shí)現(xiàn)語(yǔ)音聊天,有幾個(gè)步驟是必須的(就是我不說(shuō),相信你應(yīng)該也能想得到一些):
????? a 語(yǔ)音采集:采集的作用就是從你的麥克風(fēng)中獲取數(shù)據(jù),我采用DirectSound類來(lái)實(shí)現(xiàn)這個(gè)技術(shù)。參考:C# 中利用 DirectSound 錄音
???(b 語(yǔ)音編碼:利用語(yǔ)音編碼算法對(duì)采集到的話音進(jìn)行壓縮編碼,進(jìn)行編碼的目的是為了減少網(wǎng)絡(luò)帶寬的壓力。)
??????c 語(yǔ)音傳輸:將采集到的聲音傳輸?shù)骄W(wǎng)絡(luò)上的其它主機(jī),我采用Socket UDP方式來(lái)實(shí)現(xiàn)。參考:C# Socket編程筆記
???(d 語(yǔ)音解碼:如果所傳輸?shù)恼Z(yǔ)音進(jìn)行過(guò)壓縮編碼,則必須對(duì)語(yǔ)音進(jìn)行解碼,否則無(wú)法得到原始語(yǔ)音數(shù)據(jù)。)
??????e 語(yǔ)音播放:當(dāng)對(duì)方通過(guò)網(wǎng)絡(luò)傳輸?shù)奖緳C(jī)時(shí)(,如果需要解碼則先執(zhí)行d),進(jìn)行實(shí)時(shí)播放。
????? 上面紅色標(biāo)記的步驟,可以省略。在本軟件中,我并未采用這兩個(gè)步驟,因?yàn)楫?dāng)我采用了這兩個(gè)步驟后,發(fā)現(xiàn)語(yǔ)音時(shí)延異常的嚴(yán)重。我采用的編解碼算法是G.729,利用的是g729.dll庫(kù)文件,壓縮效果不錯(cuò),但是時(shí)延比較嚴(yán)重,可能是自己哪里沒(méi)有設(shè)置好。如果有朋友使用過(guò)該算法,且時(shí)延低的,希望不吝賜教。
??????接下來(lái),重點(diǎn)介紹語(yǔ)音采集、語(yǔ)音傳輸、語(yǔ)音播放的實(shí)現(xiàn)。
??????語(yǔ)音采集
??????由于所實(shí)現(xiàn)的方法與錄音方法一致,因此不會(huì)著墨過(guò)多,如果你不能很好的理解,請(qǐng)先參考:C# 中利用 DirectSound 錄音
????? 與錄音不同的是,錄音我們需要建立一個(gè)WAVE文件來(lái)存儲(chǔ)這些采集到的數(shù)據(jù),而在語(yǔ)音聊天中,則不需要存儲(chǔ),當(dāng)采集到一些數(shù)據(jù)后,就立刻發(fā)送出去,因此也不需要開辟很大的空間來(lái)存放PCM數(shù)據(jù)。
????? 我們先來(lái)回顧下采集的基本步驟:
????? 1. 設(shè)置PCM格式,設(shè)置相關(guān)的參數(shù),如:采樣頻率、量化位數(shù)等。
????? 2. 建立采集用的設(shè)備對(duì)象,建立采集用的緩沖區(qū)對(duì)象。
????? 3. 設(shè)置緩沖區(qū)通知,設(shè)置通知被觸發(fā)后的事件。通知是用于當(dāng)緩沖區(qū)的讀指針達(dá)到某預(yù)設(shè)位置時(shí)觸發(fā)通知事件,提醒我們可以對(duì)某部分的數(shù)據(jù)進(jìn)行傳送了。
????? 4. 開始采集聲音。
????? 5. 當(dāng)通知被觸發(fā)后,建立一個(gè)新的線程來(lái)處理數(shù)據(jù)傳送的事件。(建立一個(gè)新的線程,就是為了防止采集過(guò)程被中斷)。
????????///?<summary>
????????///?設(shè)置音頻格式,如采樣率等
????????///?</summary>
????????///?<returns>設(shè)置完成后的格式</returns>
????????private?WaveFormat?SetWaveFormat()
????????{
????????????WaveFormat?format?=?new?WaveFormat();
????????????format.FormatTag?=?WaveFormatTag.Pcm;//設(shè)置音頻類型
????????????format.SamplesPerSecond?=?11025;//采樣率(單位:赫茲)典型值:11025、22050、44100Hz
????????????format.BitsPerSample?=?16;//采樣位數(shù)
????????????format.Channels?=?1;//聲道
????????????format.BlockAlign?=?(short)(format.Channels?*?(format.BitsPerSample?/?8));//單位采樣點(diǎn)的字節(jié)數(shù)
????????????format.AverageBytesPerSecond?=?format.BlockAlign?*?format.SamplesPerSecond;
????????????return?format;
????????????//按照以上采樣規(guī)格,可知采樣1秒鐘的字節(jié)數(shù)為22050*2=44100B?約為?43K
????????}
????????///?<summary>
????????///?創(chuàng)建捕捉設(shè)備對(duì)象
????????///?</summary>
????????///?<returns>如果創(chuàng)建成功返回true</returns>
????????private?bool?CreateCaputerDevice()
????????{
????????????//首先要玫舉可用的捕捉設(shè)備
????????????CaptureDevicesCollection?capturedev?=?new?CaptureDevicesCollection();
????????????Guid?devguid;
????????????if?(capturedev.Count?>?0)
????????????{
????????????????devguid?=?capturedev[0].DriverGuid;
????????????}
????????????else
????????????{
????????????????System.Windows.Forms.MessageBox.Show("當(dāng)前沒(méi)有可用于音頻捕捉的設(shè)備",?"系統(tǒng)提示");
????????????????return?false;
????????????}
????????????//利用設(shè)備GUID來(lái)建立一個(gè)捕捉設(shè)備對(duì)象
????????????capture?=?new?Capture(devguid);
????????????return?true;
????????}
????????///?<summary>
????????///?創(chuàng)建捕捉緩沖區(qū)對(duì)象
????????///?</summary>
????????private?void?CreateCaptureBuffer()
????????{
????????????//想要?jiǎng)?chuàng)建一個(gè)捕捉緩沖區(qū)必須要兩個(gè)參數(shù):緩沖區(qū)信息(描述這個(gè)緩沖區(qū)中的格式等),緩沖設(shè)備。
????????????WaveFormat?mWavFormat?=?SetWaveFormat();
????????????CaptureBufferDescription?bufferdescription?=?new?CaptureBufferDescription();
????????????bufferdescription.Format?=?mWavFormat;//設(shè)置緩沖區(qū)要捕捉的數(shù)據(jù)格式
????????????iNotifySize?=?mWavFormat.AverageBytesPerSecond?/?iNotifyNum;//1秒的數(shù)據(jù)量/設(shè)置的通知數(shù)得到的每個(gè)通知大小小于0.2s的數(shù)據(jù)量,話音延遲小于200ms為優(yōu)質(zhì)話音
????????????iBufferSize?=?iNotifyNum?*?iNotifySize;
????????????bufferdescription.BufferBytes?=?iBufferSize;
????????????bufferdescription.ControlEffects?=?true;
????????????bufferdescription.WaveMapped?=?true;
????????????capturebuffer?=?new?CaptureBuffer(bufferdescription,?capture);//建立設(shè)備緩沖區(qū)對(duì)象
????????}
????????//設(shè)置通知
????????private?void?CreateNotification()
????????{
????????????BufferPositionNotify[]?bpn?=?new?BufferPositionNotify[iNotifyNum];//設(shè)置緩沖區(qū)通知個(gè)數(shù)
????????????//設(shè)置通知事件
????????????notifyEvent?=?new?AutoResetEvent(false);
????????????notifyThread?=?new?Thread(RecoData);//通知觸發(fā)事件
????????????notifyThread.IsBackground?=?true;
????????????notifyThread.Start();
????????????for?(int?i?=?0;?i?<?iNotifyNum;?i++)
????????????{
????????????????bpn[i].Offset?=?iNotifySize?+?i?*?iNotifySize?-?1;//設(shè)置具體每個(gè)的位置
????????????????bpn[i].EventNotifyHandle?=?notifyEvent.Handle;
????????????}
????????????myNotify?=?new?Notify(capturebuffer);
????????????myNotify.SetNotificationPositions(bpn);
????????}
????????//線程中的事件
????????private?void?RecoData()
????????{
????????????while?(true)
????????????{
????????????????//?等待緩沖區(qū)的通知消息
????????????????notifyEvent.WaitOne(Timeout.Infinite,?true);
????????????????//?錄制數(shù)據(jù)
????????????????RecordCapturedData(Client,epServer);
????????????}
????????}
????????//真正轉(zhuǎn)移數(shù)據(jù)的事件,其實(shí)就是把數(shù)據(jù)傳送到網(wǎng)絡(luò)上去。
????????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;//這個(gè)大小就是我們可以安全讀取的大小
????????????if?(locksize?==?0)
????????????{
????????????????return;
????????????}
????????????if?(locksize?<?0)
????????????{//因?yàn)槲覀兪茄h(huán)的使用緩沖區(qū),所以有一種情況下為負(fù):當(dāng)文以載讀指針回到第一個(gè)通知點(diǎn),而Ibuffeoffset還在最后一個(gè)通知處
????????????????locksize?+=?iBufferSize;
????????????}
????????????capturedata?=?(byte[])capturebuffer.Read(iBufferOffset,?typeof(byte),?LockFlag.FromWriteCursor,?locksize);
????????????//capturedata?=?g729.Encode(capturedata);//語(yǔ)音編碼
????????????try
????????????{
????????????????Client.SendTo(capturedata,?epServer);//傳送語(yǔ)音
????????????}
????????????catch
????????????{
????????????????throw?new?Exception();
????????????}
????????????iBufferOffset?+=?capturedata.Length;
????????????iBufferOffset?%=?iBufferSize;//取模是因?yàn)榫彌_區(qū)是循環(huán)的。
????????}
??????上述代碼可以很好的采集到聲音數(shù)據(jù),幾乎與原始聲音一致。如果你已經(jīng)可以實(shí)現(xiàn)錄音,那么以上對(duì)你來(lái)說(shuō)應(yīng)該并不陌生。
??????語(yǔ)音傳輸
????? 這部分并不是很難,如果你熟悉socket編程,那么就可以PASS這一部分了,與以往傳輸不同的只是現(xiàn)在傳輸?shù)氖钦Z(yǔ)音而已。如果你沒(méi)接觸過(guò)socket,那可以瞧瞧C# Socket編程筆記。
????? 感覺(jué)這部分叫“語(yǔ)音傳輸”并不是很恰當(dāng),因?yàn)槠鋵?shí)真正用于傳輸?shù)恼Z(yǔ)句只有一句。除了語(yǔ)音傳輸之外,我們還需要對(duì)網(wǎng)絡(luò)進(jìn)行監(jiān)聽,從而能捕獲對(duì)方發(fā)送給自己的語(yǔ)音信息。但是,也不知道叫什么好,就估且這么叫著吧。在這一部分,我主要講下大致流程。
????? 1. 建立socket對(duì)象,在實(shí)例化這個(gè)對(duì)象的時(shí)候有一個(gè)參數(shù)是設(shè)置使用的協(xié)議,在本軟件中,我采用的是UDP。
????? 為什么要采用UDP?建立TCP能不能傳送語(yǔ)音,答案肯定是能的。在本軟件中,我考慮的主要是語(yǔ)音延時(shí)問(wèn)題,?采用TCP在建立連接和維護(hù)連接中對(duì)時(shí)間和系統(tǒng)資源的開銷較大,因此會(huì)有明顯的時(shí)延發(fā)生,嚴(yán)重影響了實(shí)時(shí)性。另外,因?yàn)閁DP是無(wú)連接的,這使得采用UDP可以支持日后功能上的擴(kuò)展(如:組播)。
????? 2. 綁定本機(jī)的IP和端口,因?yàn)橐粋€(gè)主機(jī)可能會(huì)有不止一個(gè)IP地址,如回發(fā)地址:127.0.0.1 和局域網(wǎng)地址:192.168.#.#。為了增加可用性,我這里選擇綁定到任何本機(jī)可用的IP地址(IPAddress.Any),而端口我們約定默認(rèn)為8000。
????? 3. 啟動(dòng)監(jiān)聽線程,來(lái)監(jiān)聽網(wǎng)絡(luò)。我采用異步的方式,以便獲得更好的系統(tǒng)響應(yīng)度。
????????private?Thread?ListenThread;
????????private?byte[]?bytData;
????????///?<summary>
????????///?監(jiān)聽方法,用于監(jiān)聽遠(yuǎn)程發(fā)送到本機(jī)的信息
????????///?</summary>
????????public?void?Listen()
????????{www.elivn.com
????????????ListenThread?=?new?Thread(new?ThreadStart(DoListen));
????????????ListenThread.IsBackground?=?true;//設(shè)置為后臺(tái)線程,這樣當(dāng)主線程結(jié)束后,該線程自動(dòng)結(jié)束
????????????ListenThread.Start();
????????}
????????private?EndPoint?epRemote;
????????///?<summary>
????????///?監(jiān)聽線程
????????///?</summary>
????????private?void?DoListen()
????????{
????????????bytData?=?new?byte[intMaxDataSize];
????????????epRemote?=?(EndPoint)(new?IPEndPoint(IPAddress.Any,?0));
????????????while?(true)
????????????{
????????????????if?(LocalSocket.Poll(5000,?SelectMode.SelectRead))
????????????????{//每5ms查詢一下網(wǎng)絡(luò),如果有可讀數(shù)據(jù)就接收
????????????????????LocalSocket.BeginReceiveFrom(bytData,?0,?bytData.Length,?SocketFlags.None,?ref?epRemote,?new?AsyncCallback(ReceiveData),?null);
????????????????}
????????????}
????????}
????????///?<summary>
????????///?接收數(shù)據(jù)
????????///?</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);//調(diào)用聲音模塊中的GetVoiceData()從字節(jié)數(shù)組中獲取聲音并播放
??????????????????//GetVoiceData()會(huì)在下一部分中提到
????????????}
????????}
????? 4. 數(shù)據(jù)的發(fā)送因?yàn)橹挥幸痪湓?#xff0c;所以我直接放在上一部分的語(yǔ)音采集中了。 Client.SendTo(capturedata,?epServer);//傳送語(yǔ)音
????? 語(yǔ)音播放
??????最麻煩的就是這部分了,而且感覺(jué)現(xiàn)在的實(shí)現(xiàn)方法仍然需要改進(jìn)才好。
????? 當(dāng)聲音傳輸?shù)奖緳C(jī)后,該怎么樣才能讓這些數(shù)據(jù)經(jīng)過(guò)音響設(shè)備放出聲音來(lái)呢?因?yàn)槁曇舨シ攀菑木彌_區(qū)中獲取聲音數(shù)據(jù)的因此我們必須先將獲取到的數(shù)據(jù)寫入緩沖區(qū),然后再調(diào)用相應(yīng)的方法來(lái)播放??雌饋?lái)似乎不復(fù)雜,可是實(shí)現(xiàn)起來(lái)遠(yuǎn)沒(méi)有這么簡(jiǎn)單。
????? 我遇到的問(wèn)題:
??????大家可以看下語(yǔ)音采集部分,我是在每次通知后進(jìn)行語(yǔ)音采集然后就將采集到的語(yǔ)音發(fā)送到網(wǎng)絡(luò)上,如果運(yùn)行正常的話,這一部分?jǐn)?shù)據(jù)實(shí)際播放長(zhǎng)度遠(yuǎn)小于1秒。也就是說(shuō)對(duì)方每次接收到的語(yǔ)音長(zhǎng)度為毫秒級(jí)。而且如果網(wǎng)絡(luò)質(zhì)量可以的話,那么連續(xù)兩次接收到數(shù)據(jù)的時(shí)間間隔也是相當(dāng)小的。這樣就產(chǎn)生問(wèn)題了,如果我在接收到第一次數(shù)據(jù)后,將它寫入緩沖區(qū),然后調(diào)用相應(yīng)的播放方法,由于語(yǔ)音長(zhǎng)度實(shí)際很短,因此幾乎聽不到什么效果,而且可能發(fā)生當(dāng)?shù)谝淮尉彌_區(qū)中的數(shù)據(jù)還沒(méi)播放完,就已經(jīng)被第二次的數(shù)據(jù)覆蓋,導(dǎo)致聲音混亂。經(jīng)測(cè)試,此種方法無(wú)法達(dá)到聲音實(shí)時(shí)效果。期間我也曾修改過(guò)數(shù)據(jù)發(fā)送部分,希望當(dāng)語(yǔ)音長(zhǎng)度達(dá)到某一長(zhǎng)度時(shí)在發(fā)送,可是問(wèn)題依舊,看樣子重要的是在接收端進(jìn)行相應(yīng)處理。
??????直接緩沖播放的方法不行,那就換~~
?????上網(wǎng)搜,可惜的是這方面的資料實(shí)在有限,C#的就更少了。參考一些文獻(xiàn),大家提到利用在緩沖區(qū)設(shè)置兩個(gè)指針,一個(gè)播放指針,一個(gè)寫指針(寫指針用于表示當(dāng)前從網(wǎng)絡(luò)上接收到的數(shù)據(jù)從寫指針?biāo)甘疚恢瞄_始往下寫,播放指針則表示當(dāng)前所播放的數(shù)據(jù)末尾)。當(dāng)播放指針達(dá)到某個(gè)位置時(shí)就播放某一部分?jǐn)?shù)據(jù),而不影響將被寫入的緩沖區(qū)部分,這樣就可以很好的解決數(shù)據(jù)覆蓋的問(wèn)題。除此之外,還要將緩沖區(qū)設(shè)置為循環(huán)緩沖區(qū),也就是頭尾相接,當(dāng)?shù)竭_(dá)尾部時(shí),自己從部開始,此時(shí)將覆蓋頭部數(shù)據(jù)。
????? 看了這些,你是不是感覺(jué)很眼熟?是不是和語(yǔ)音采集很類似?是的,我們?cè)诓蹲骄彌_區(qū)中就是這樣設(shè)置的,我們利用通知來(lái)設(shè)置觸發(fā)事件。不同的是我們接收語(yǔ)音用的緩沖區(qū)并不是捕捉緩沖區(qū),MS為捕捉單獨(dú)設(shè)置了一個(gè)捕捉緩沖區(qū)。我們利用的是另一個(gè)緩沖區(qū),輔助緩沖區(qū)(SecondaryBuffer)。后來(lái)發(fā)現(xiàn)該緩沖區(qū)也有類似的通知,這意味什么?我當(dāng)時(shí)很興奮,可是~~相當(dāng)郁悶的是,我不管怎么設(shè)置通知,編譯時(shí)都會(huì)報(bào)錯(cuò),到外詢求答案,均無(wú)果。在 MS 相關(guān)網(wǎng)站上咨詢后,有一位叫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.
????? 簡(jiǎn)單意思就是:Notify出問(wèn)題已經(jīng)很長(zhǎng)時(shí)間了,而且MS可能永遠(yuǎn)都不會(huì)去修復(fù)這個(gè)問(wèn)題。而且他也為播放聲音提供了些建議,這些建議與上面所講的基本一致。
??????至于這個(gè)答案是否正確,因?yàn)闊o(wú)從考證,就不再討論了。如果哪位高手曾經(jīng)實(shí)現(xiàn)過(guò),希望賜教。
??????既然目前無(wú)法正常使用,就只能來(lái)手動(dòng)寫了。這個(gè)方法名就是:GetVoiceData()。
??????思路如下:
????? ·利用MemoryStream來(lái)代表這個(gè)接收緩沖區(qū)。
????? ·設(shè)置兩個(gè)表示指針位置的字段:
????? ?? private int intPosWrite = 0;//內(nèi)存流中寫指針位移
????? ?? private int intPosPlay = 0;//內(nèi)存流中播放指針位移
??????·當(dāng)接收到數(shù)據(jù)后,則移動(dòng)寫指針,移動(dòng)的長(zhǎng)度為接收到的數(shù)據(jù)長(zhǎng)度。
????? ·利用一個(gè)字段表示通知大小:private int intNotifySize = 5000;
??????·當(dāng)寫指針的位置達(dá)到通知大小,則執(zhí)行播放操作,然后移動(dòng)播放指針到剛才的通知的位置。
????? ·如果當(dāng)前寫指針的位移與將要寫入到緩沖區(qū)的數(shù)據(jù)大小相加后超過(guò)緩沖容量的,則進(jìn)行摩爾運(yùn)算,實(shí)現(xiàn)循環(huán)的效果。 ??????
?????? 這樣,基本上就可以實(shí)現(xiàn)語(yǔ)音聊天了??墒沁@樣的效果還只能是初步的,而且由于回聲的原因,相當(dāng)影響音質(zhì),還可能產(chǎn)生囂叫,為了解決這個(gè)問(wèn)題,我本打算采用MS提供的AEC算法,可是由于不知道如何實(shí)現(xiàn),一直無(wú)法得到效果,因此這也是比較遺憾的地方。
??????
??????可執(zhí)行文件(注:要在安裝了 .net framework 2.0 的平臺(tái)上運(yùn)行):MatureVoiceEXE.rar
????? 源文件:MatureVoice.rar
??
????????private?int?intPosWrite?=?0;//內(nèi)存流中寫指針位移
????????private?int?intPosPlay?=?0;//內(nèi)存流中播放指針位移
????????private?int?intNotifySize?=?5000;//設(shè)置通知大小
????????///?<summary>
????????///?從字節(jié)數(shù)組中獲取音頻數(shù)據(jù),并進(jìn)行播放
????????///?</summary>
????????///?<param?name="intRecv">字節(jié)數(shù)組長(zhǎng)度</param>
????????///?<param?name="bytRecv">包含音頻數(shù)據(jù)的字節(jié)數(shù)組</param>
????????public?void?GetVoiceData(int?intRecv,?byte[]?bytRecv)
????????{
????????????//intPosWrite指示最新的數(shù)據(jù)寫好后的末尾。intPosPlay指示本次播放開始的位置。
????????????if?(intPosWrite?+?intRecv?<=?memstream.Capacity)
????????????{//如果當(dāng)前寫指針?biāo)诘奈灰?#43;將要寫入到緩沖區(qū)的長(zhǎng)度小于緩沖區(qū)總大小
????????????????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)
????????????????{//先存儲(chǔ)一定量的數(shù)據(jù),當(dāng)達(dá)到一定數(shù)據(jù)量時(shí)就播放聲音。
????????????????????buffDiscript.BufferBytes?=?intPosWrite?-?intPosPlay;//緩沖區(qū)大小為播放指針到寫指針之間的距離。
????????????????????SecondaryBuffer?sec?=?new?SecondaryBuffer(buffDiscript,?PlayDev);//建立一個(gè)合適的緩沖區(qū)用于播放這段數(shù)據(jù)。
????????????????????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;//緩沖區(qū)大小為播放指針到寫指針之間的距離。
????????????????????SecondaryBuffer?sec?=?new?SecondaryBuffer(buffDiscript,?PlayDev);//建立一個(gè)合適的緩沖區(qū)用于播放這段數(shù)據(jù)。
????????????????????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
????????????{//當(dāng)數(shù)據(jù)將要大于memstream可容納的大小時(shí)
????????????????int?irest?=?memstream.Capacity?-?intPosWrite;//memstream中剩下的可容納的字節(jié)數(shù)。
????????????????memstream.Write(bytRecv,?0,?irest);//先寫完這個(gè)內(nèi)存流。
????????????????memstream.Position?=?0;//然后讓新的數(shù)據(jù)從memstream的0位置開始記錄
????????????????memstream.Write(bytRecv,?irest,?intRecv?-?irest);//覆蓋舊的數(shù)據(jù)
????????????????intPosWrite?=?intRecv?-?irest;//更新寫指針位置。寫指針指示下一個(gè)開始寫入的位置而不是上一次結(jié)束的位置,因此不用減一
????????????}
????????}
轉(zhuǎn)載于:https://www.cnblogs.com/seoxs/archive/2011/04/20/2021839.html
總結(jié)
以上是生活随笔為你收集整理的C#中利用Socket实现网络语音通信[初级版本]的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 让iPhone自动帮你关闭音乐
- 下一篇: C#里的一些加密解密标准函数示例——DE