C#利用waveIn實(shí)現(xiàn)聲音采集
前言
之前實(shí)現(xiàn)了《C++ 使用waveIn實(shí)現(xiàn)聲音采集》,后來(lái)C#項(xiàng)目也有此功能的需求,直接調(diào)用C++封裝的dll是可以的。但是wimm這種基于win32 api的庫(kù),完全可以直接用C#去調(diào)用,將依賴減少到最小。
一、需要的對(duì)象及方法
參考《C++ 使用waveIn實(shí)現(xiàn)聲音采集》,此處不再贅述。
二、整體流程
參考《C++ 使用waveIn實(shí)現(xiàn)聲音采集》,此處不再贅述。
三、關(guān)鍵實(shí)現(xiàn)
此處講一些與C#相關(guān)的點(diǎn)。
1、使用Thread開(kāi)啟線程
筆者一開(kāi)是實(shí)現(xiàn)是使用Task開(kāi)啟線程,由于Task基于線程池可以提高資源利用率,但是這也出現(xiàn)了一些問(wèn)題。由于錄制需要在子線程開(kāi)啟消息循環(huán),多次重復(fù)調(diào)用錄制時(shí),有概率打開(kāi)同一個(gè)線程,就有可能收到上一個(gè)錄制的數(shù)據(jù)消息,造成非法內(nèi)存的讀取問(wèn)題。目前沒(méi)找到銷毀線程中消息循環(huán)的方法,只有通過(guò)結(jié)束線程的方式結(jié)束消息循環(huán)。所以使用Thread開(kāi)啟線程,才能夠解決問(wèn)題。
_thread = new Thread(() => { _CollectThread();}); _thread.Start();
2、TaskCompletionSource實(shí)現(xiàn)異步
因?yàn)镃#支持async、await機(jī)制,這樣就可以直接去掉開(kāi)始和停止兩個(gè)回調(diào),使用異步實(shí)現(xiàn)開(kāi)始和停止方法。
/// <summary> /// 開(kāi)始采集,Start和Stop需要成對(duì)使用,await可變成同步式,真正開(kāi)始采集才會(huì)返回。 /// 失敗會(huì)拋出異常,可通過(guò)ContinueWith或await獲取異常。 /// </summary> public async Task<Task> Start(); /// <summary> /// 停止采集,直接調(diào)用是異步,可await等待真正停止 /// 此方法是有可能拋異常的,采集過(guò)程中出現(xiàn)的異常,會(huì)在此方法中拋出 /// </summary> public async Task<Task> Stop();
調(diào)用方式
await wic.Start(); //此行是采集真正開(kāi)始的時(shí)機(jī) await wic.Stop(); //此行是已經(jīng)停止的時(shí)機(jī)
由于使用了Thread開(kāi)啟線程,所以我們需要使用其他方式生成Task,在Thread結(jié)束后觸發(fā)Task完成。用過(guò)flutter的朋友應(yīng)該知道這種情況使用Completer就可以,C#中對(duì)應(yīng)Dart的Completer就是TaskCompletionSource。
示例代碼如下
public async Task<Task> Start() { TaskCompletionSource? tcsStart=new TaskCompletionSource(); ; _tcs = new TaskCompletionSource(); _thread = new Thread(() => { _CollectThread(tcsStart); _tcs.SetResult();/*線程結(jié)束觸發(fā)完成*/ }); _thread.Start(); //等待開(kāi)始完成的信號(hào) await tcsStart.Task; return Task.CompletedTask; } void _CollectThread(TaskCompletionSource tcsStart){ while(GetMessage(out msg)!=0) { //接收到Wimm開(kāi)始消息,觸發(fā)完成 tcsStart.SetResult(); //接收到Wimm結(jié)束消息退出循環(huán)結(jié)束線程 } } public async Task<Task> Stop() { if (_thread != null) { //發(fā)送消息結(jié)束線程 _exitFlag = true; PostThreadMessage(_threadId, MM_WIM_CLOSE); //等待線程結(jié)束 await _tcs!.Task; _tcs = null; _thread = null; } return Task.CompletedTask; }
3、將指針?lè)庋b為Stream
通過(guò)Wimm采集的音頻數(shù)據(jù)是指針的形式,如果需要轉(zhuǎn)為byte[]這需要使用Marshall進(jìn)行數(shù)據(jù)拷貝,為了避免拷貝,數(shù)據(jù)形式不能是byte[]數(shù)組。直接提供指針又不方便使用,筆者采用了Stream的方式提供數(shù)據(jù),而且文件流直接支持Stream寫(xiě)入。C#本身有個(gè)UmanagedMemoryStream可以支持讀取指針的數(shù)據(jù),但是需要unsafe上下文,這顯然是沒(méi)必要的(有unsafe上下文,直接通過(guò)地址讀取數(shù)據(jù)即可,或者將此功能放dll單獨(dú)設(shè)置unsafe對(duì)外提供Stream也不便于管理)。最好的方式還是自己實(shí)現(xiàn)一個(gè)Stream用于讀取指針數(shù)據(jù)。
完整代碼如下:
using System.Runtime.InteropServices; namespace AC { /// <summary> /// 用于讀取指針數(shù)據(jù)的流,內(nèi)部不會(huì)管理指針 /// 由于.net庫(kù)提供的UnmanagedMemoryStream需要unsafe上下文,所以直接自己封裝一個(gè)類似功能的stream避開(kāi)unsafe的使用。 /// </summary> class UMemoryStream : Stream { public override bool CanRead => _access == FileAccess.Read || _access == FileAccess.ReadWrite; public override bool CanSeek => true; public override bool CanWrite => _access == FileAccess.Write || _access == FileAccess.ReadWrite; public override long Length => _len; public override long Position { get; set; } = 0; FileAccess _access; nint _ptr; nint _len; /// <summary> /// 構(gòu)造方法 /// </summary> /// <param name="ptr">數(shù)據(jù)地址</param> /// <param name="len">數(shù)據(jù)長(zhǎng)度</param> /// <param name="access"></param> public UMemoryStream(nint ptr, int len, FileAccess access) { _ptr = ptr; _len = len; _access = access; } public override void Flush() { throw new NotSupportedException(); } public override int Read(byte[] buffer, int offset, int count) { if (_ptr == 0) throw new ObjectDisposedException(ToString()); if (!CanRead) throw new NotSupportedException(); var leftCount = _len - Position; if (count > leftCount) { count = (int)leftCount; } if (count > 0) { Marshal.Copy(_ptr + (nint)Position, buffer, offset, count); Position += count; } return count; } public override long Seek(long offset, SeekOrigin origin) { switch (origin) { case SeekOrigin.Begin: Position = offset; break; case SeekOrigin.Current: Position += offset; break; case SeekOrigin.End: Position = _len - offset; break; } return Position; } public override void SetLength(long value) { throw new NotSupportedException(); } public override void Write(byte[] buffer, int offset, int count) { if (_ptr == 0) throw new ObjectDisposedException(ToString()); if (!CanWrite) throw new NotSupportedException(); var leftCount = _len - Position; if (count > leftCount) { count = (int)leftCount; } if (count > 0) { Marshal.Copy(buffer, offset, _ptr + (nint)Position, count); Position += count; } else { throw new ArgumentOutOfRangeException(); } } public override void Close() { _ptr = 0; } } }
四、完整代碼
將采集功能封裝成一個(gè)通用工具,方便在任意地方使用。
1.接口
接口設(shè)計(jì)如下:
using System.Runtime.InteropServices; using static AC.Winmm; using static AC.User32; using static AC.Kernel32; ???????/************************************************************************ * @Project: AC::WaveInCollector * @Decription: 音頻采集工具 * @Verision: v1.0.0.0 * @Author: Xin Nie * @Create: 2023/10/8 09:27:00 * @LastUpdate: 2023/10/24 11:34:00 ************************************************************************ * Copyright @ 2025. All rights reserved. ************************************************************************/ namespace AC { /// <summary> /// 聲音采集對(duì)象 /// </summary> /// <summary> /// 聲音采集對(duì)象 ///這是一個(gè)功能完整聲音采集對(duì)象,所有接口通過(guò)了測(cè)試。 ///非線程安全,所有方法需確保在單線程中調(diào)用,即比如:Start和Stop不能在兩個(gè)線程中同時(shí)調(diào)用。 /// </summary> public class WaveInCollector : IAsyncDisposable { /// <summary> /// 數(shù)據(jù)到達(dá)事件參數(shù) /// </summary> public class DataArrivedEventArgs : EventArgs { /// <summary> /// 聲音格式 /// </summary> public SampleFormat Format { set; get; } /// <summary> /// 聲音數(shù)據(jù)流,為了減少數(shù)據(jù)拷貝次數(shù),將非托管內(nèi)存封裝成流的形式提供,只讀,生命周期為回調(diào)方法內(nèi)。 /// </summary> public Stream Stream { set; get; } } /// <summary> /// 采集數(shù)據(jù)到達(dá)事件 /// </summary> public event EventHandler<DataArrivedEventArgs>? DataArrived; /// <summary> /// 采集速率單位:次/秒 /// 此屬性會(huì)影響每次輸出數(shù)據(jù)的大小 /// 開(kāi)始采集前設(shè)置有效 /// </summary> public int Frequency { set; get; } = 50; /// <summary> /// 聲音格式 /// </summary> public SampleFormat Format { private set; get; } /// <summary> /// 當(dāng)前設(shè)備 /// </summary> public AudioDevice Device { private set; get; } /// <summary> /// 枚舉可用的聲音采集設(shè)備 /// </summary> public static IEnumerable<AudioDevice> AvailableDevices { get; } /// <summary> /// 構(gòu)造方法 /// </summary> /// <param name="device">音頻設(shè)備,不能為空</param> /// <param name="SampleFormat">聲音格式</param> public WaveInCollector(AudioDevice device, SampleFormat sf); /// <summary> /// 構(gòu)造方法 /// 如果系統(tǒng)沒(méi)有任何設(shè)備則會(huì)拋出異常 /// </summary> /// <param name="deviceId">聲音設(shè)備Id,0為默認(rèn)設(shè)備</param> /// <param name="SampleFormat">聲音格式</param> public WaveInCollector(uint deviceId, SampleFormat sf) : this(GetWaveInDeviceById(deviceId)!, sf) { } /// <summary> /// 開(kāi)始采集,Start和Stop需要成對(duì)使用,await可變成同步式,真正開(kāi)始采集才會(huì)返回。 /// 失敗會(huì)拋出異常,可通過(guò)ContinueWith或await獲取異常。 /// </summary> public async Task<Task> Start(); /// <summary> /// 停止采集,直接調(diào)用是異步,可await等待真正停止 /// 此方法是有可能拋異常的,采集過(guò)程中出現(xiàn)的異常,會(huì)在此方法中拋出 /// </summary> public async Task<Task> Stop(); } /// <summary> /// 聲音格式 /// </summary> public class SampleFormat { /// <summary> /// 聲道數(shù) /// </summary> public ushort Channels { set; get; } /// <summary> /// 采樣率 /// </summary> public uint SampleRate { set; get; } /// <summary> /// 位深 /// </summary> public ushort BitsPerSample { set; get; } } /// <summary> /// 音頻設(shè)備 /// </summary> public class AudioDevice { /// <summary> /// 設(shè)備Id /// </summary> public uint Id { set; get; } /// <summary> /// 設(shè)備名稱 /// </summary> public string Name { set; get; } = ""; /// <summary> /// 聲道數(shù) /// </summary> public int Channels { set; get; } /// <summary> /// 支持的格式 /// </summary> public IEnumerable<SampleFormat> SupportedFormats { set; get; } } }
2.具體實(shí)現(xiàn)
vs2022 .net6.0 項(xiàng)目,所有win api通過(guò)dllimport引入,沒(méi)有任意額外依賴。
注:winmm不能識(shí)別dshow虛擬設(shè)備,請(qǐng)根據(jù)需要下載資源。
五、使用示例
采集聲音并保存為wav文件,其中的WavWriter對(duì)象參考《C# 將音頻PCM數(shù)據(jù)封裝成wav文件》
方式一
獲取可用設(shè)備并采集
// See https://aka.ms/new-console-template for more information using AC; try { //獲取可用的音頻設(shè)備 var device = WaveInCollector.AvailableDevices.First(); //創(chuàng)建wav文件 using (var ww = WavWriter.Create("test.wav", device.SupportedFormats!.First().Channels, device.SupportedFormats!.First().SampleRate, device.SupportedFormats!.First().BitsPerSample)) { //初始化錄制對(duì)象 await using (var wic = new WaveInCollector(device.Id, device.SupportedFormats!.First())) { //由于api限制設(shè)備名稱不一定全。長(zhǎng)度最大32。 Console.WriteLine("設(shè)備名稱:" + wic.Device.Name); Console.WriteLine("聲音格式:Chanels=" + wic.Format.Channels + " SampleRate=" + wic.Format.SampleRate + " BitsPerSample=" + wic.Format.BitsPerSample ); Console.WriteLine("開(kāi)始錄制"); //注冊(cè)錄制事件 wic.DataArrived += (s, e) => { Console.WriteLine("接收數(shù)據(jù)長(zhǎng)度" + e.Stream.Length); //寫(xiě)入文件 ww.Write(e.Stream); }; //開(kāi)始錄制 await wic.Start(); //錄制10s結(jié)束 await Task.Delay(10000); Console.WriteLine("錄制完成"); } } } catch (Exception e) { Console.WriteLine(e.Message); }
方式二
指定設(shè)備下標(biāo)和聲音格式
// See https://aka.ms/new-console-template for more information using AC; try { ??????? //創(chuàng)建wav文件 using (var ww = WavWriter.Create("test.wav", 2, 44100, 16)) { //初始化錄制對(duì)象 await using (var wic = new WaveInCollector(0, new SampleFormat() { Channels = 2, SampleRate = 44100, BitsPerSample = 16 })) { //由于api限制設(shè)備名稱不一定全。長(zhǎng)度最大32。 Console.WriteLine("設(shè)備名稱:" + wic.Device.Name); Console.WriteLine("聲音格式:Chanels=" + wic.Format.Channels + " SampleRate=" + wic.Format.SampleRate + " BitsPerSample=" + wic.Format.BitsPerSample ); Console.WriteLine("開(kāi)始錄制"); //注冊(cè)錄制事件 wic.DataArrived += (s, e) => { Console.WriteLine("接收數(shù)據(jù)長(zhǎng)度" + e.Stream.Length); //寫(xiě)入文件 ww.Write(e.Stream); }; //開(kāi)始錄制 await wic.Start(); //錄制10s結(jié)束 await Task.Delay(10000); Console.WriteLine("錄制完成"); } } } catch (Exception e) { Console.WriteLine(e.Message); }
效果預(yù)覽
總結(jié)
實(shí)現(xiàn)waveIn聲音采集雖然核心部分和C++一樣,但是對(duì)于接口的設(shè)計(jì)以及調(diào)用流程都有很大的不同,尤其是C#的異步可以簡(jiǎn)化調(diào)用,使得接口變得很簡(jiǎn)潔,而且通過(guò)disposable又可以和using配合省去Stop的調(diào)用。但唯一比較麻煩的地方就是內(nèi)存的互操作,尤其是音頻數(shù)據(jù)緩存的讀取和寫(xiě)入,在非unsafe的環(huán)境下會(huì)多一次拷貝。總的來(lái)說(shuō),這個(gè)功能在C#中實(shí)現(xiàn)還是有用的,調(diào)用簡(jiǎn)單而且沒(méi)有額外依賴。
以上就是C#利用waveIn實(shí)現(xiàn)聲音采集的詳細(xì)內(nèi)容,更多關(guān)于C# waveIn聲音采集的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#的WebBrowser的操作與注意事項(xiàng)介紹
C#的WebBrowser的操作與注意事項(xiàng)介紹,需要的朋友可以參考一下2013-03-03C#開(kāi)發(fā)中經(jīng)常用的加密解密方法示例
這篇文章主要給大家介紹了關(guān)于C#開(kāi)發(fā)中經(jīng)常用的加密解密方法的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用C#具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07C#實(shí)現(xiàn)ArrayList動(dòng)態(tài)數(shù)組的示例
ArrayList是一個(gè)動(dòng)態(tài)數(shù)組,可以用來(lái)存儲(chǔ)任意類型的元素,本文就來(lái)介紹一下C#實(shí)現(xiàn)ArrayList動(dòng)態(tài)數(shù)組的示例,具有一定的參考價(jià)值,感興趣的可以了解一下2023-12-12C# winformTextBox 鍵盤(pán)監(jiān)聽(tīng)方式
這篇文章主要介紹了C# winformTextBox 鍵盤(pán)監(jiān)聽(tīng)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-04-04C#控件picturebox實(shí)現(xiàn)圖像拖拽和縮放
這篇文章主要為大家詳細(xì)介紹了C#控件picturebox實(shí)現(xiàn)圖像拖拽和縮放,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-09-09