C#音訊採集 (筆記)
阿新 • • 發佈:2019-01-02
using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Threading; using Microsoft.DirectX; using Microsoft.DirectX.DirectSound; using System.Net.Sockets; using System.Net; namespace MatureVoice { class VoiceCapture { private MemoryStream memstream;//記憶體流 private SecondaryBuffer secBuffer;//輔助緩衝區 private int iNotifySize = 0;//通知大小 private int iBufferSize = 0;//捕捉緩衝區大小 private CaptureBuffer capturebuffer;//捕捉緩衝區物件 private AutoResetEvent notifyEvent;//通知事件 private Thread notifyThread;//通知執行緒 private int iNotifyNum=0;//通知個數 private Notify myNotify;//通知物件 private Capture capture;//捕捉裝置物件 private Device PlayDev;//播放裝置物件 private BufferDescription buffDiscript; private Socket Client; private EndPoint epServer; private int iBufferOffset=0;//捕捉緩衝區位移 private IntPtr intptr;//視窗控制代碼 public IntPtr Intptr { set { intptr = value; } } public int NotifySize { set { iNotifySize = value; } } public int NotifyNum { set { iNotifyNum = value; } } public Socket LocalSocket { set { Client = value; } } public EndPoint RemoteEndPoint { set { epServer = value; } } /// <summary> /// 初始化相關操作 /// </summary> public void InitVoice() {//初始化聲音相關設定:(1)捕捉緩衝區(2)播放緩衝區 if (!CreateCaputerDevice()) { throw new Exception(); }//建立裝置物件 CreateCaptureBuffer();//建立緩衝區物件 CreateNotification();//設定通知及事件 //======(2)============== if (!CreatePlayDevice()) { throw new Exception(); } CreateSecondaryBuffer(); } /// <summary> /// 啟動聲音採集 /// </summary> public void StartVoiceCapture() { capturebuffer.Start(true);//true表示設定緩衝區為迴圈方式,開始捕捉 } /// <summary> /// 建立用於播放的音訊裝置物件 /// </summary> /// <returns>建立成功返回true</returns> private bool CreatePlayDevice() { DevicesCollection dc = new DevicesCollection(); Guid g; if (dc.Count > 0) { g = dc[0].DriverGuid; } else { return false; } PlayDev = new Device(g); PlayDev.SetCooperativeLevel(intptr, CooperativeLevel.Normal); return true; } /// <summary> /// 建立輔助緩衝區 /// </summary> private void CreateSecondaryBuffer() { buffDiscript = new BufferDescription(); WaveFormat mWavFormat = SetWaveFormat(); buffDiscript.Format = mWavFormat; iNotifySize = mWavFormat.AverageBytesPerSecond / iNotifyNum;//設定通知大小 iBufferSize = iNotifyNum * iNotifySize; buffDiscript.BufferBytes = iBufferSize; buffDiscript.ControlPan = true; buffDiscript.ControlFrequency = true; buffDiscript.ControlVolume = true; buffDiscript.GlobalFocus = true; secBuffer = new SecondaryBuffer(buffDiscript, PlayDev); byte[] bytMemory = new byte[100000]; memstream = new MemoryStream(bytMemory, 0, 100000, true, true); //g729 = new G729(); //g729.InitalizeEncode(); //g729.InitalizeDecode(); } /// <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;//取模是因為緩衝區是迴圈的。 } 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;//更新寫指標位置。寫指標指示下一個開始寫入的位置而不是上一次結束的位置,因此不用減一 } } /// <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> public void Stop() { capturebuffer.Stop(); if (notifyEvent != null) { notifyEvent.Set(); } if (notifyThread != null && notifyThread.IsAlive == true) { notifyThread.Abort(); } } }