C# 使用waveIn实现声音采集

文章目录

  • 前言
  • 一、需要的对象及方法
  • 二、整体流程
  • 三、关键实现
    • 1、使用Thread开启线程
    • 2、TaskCompletionSource实现异步
    • 3、使用异步流简化调用
  • 四、完整代码
    • 1.接口
    • 2.具体实现
  • 五、使用示例
    • 示例一
    • 示例二
  • 总结


前言

之前实现了《C++ 使用waveIn实现声音采集》,后来C#项目也有此功能的需求,直接调用C++封装的dll是可以的。但是wimm这种基于win32 api的库,完全可以直接用C#去调用,将依赖减少到最小。


一、需要的对象及方法

参考《C++ 使用waveIn实现声音采集》,此处不再赘述。


二、整体流程

参考《C++ 使用waveIn实现声音采集》,此处不再赘述。


三、关键实现

此处讲一些与C#相关的点。

1、使用Thread开启线程

笔者一开是实现是使用Task开启线程,由于Task基于线程池可以提高资源利用率,但是这也出现了一些问题。由于录制需要在子线程开启消息循环,多次重复调用录制时,有概率打开同一个线程,就有可能收到上一个录制的数据消息,造成非法内存的读取问题。目前没找到销毁线程中消息循环的方法,只有通过结束线程的方式结束消息循环。所以使用Thread开启线程,才能够解决问题。

 _thread = new Thread(() => { _CollectThread();});
 _thread.Start();      

2、TaskCompletionSource实现异步

因为C#支持async、await机制,这样就可以直接去掉开始和停止两个回调,使用异步实现开始和停止方法。

/// 
/// 开始采集,Start和Stop需要成对使用,await可变成同步式,真正开始采集才会返回。
/// 失败会抛出异常,可通过ContinueWith或await获取异常。
/// 		
public async Task<Task> Start();
/// 
/// 停止采集,直接调用是异步,可await等待真正停止
/// 此方法是有可能抛异常的,采集过程中出现的异常,会在此方法中抛出
/// 
public async Task<Task> Stop();

调用方式

await wic.Start();
//此行是采集真正开始的时机
await wic.Stop();
//此行是已经停止的时机

由于使用了Thread开启线程,所以我们需要使用其他方式生成Task,在Thread结束后触发Task完成。用过flutter的朋友应该知道这种情况使用Completer就可以,C#中对应Dart的Completer就是TaskCompletionSource。
示例代码如下

public async Task<Task> Start()
{
    TaskCompletionSource? tcsStart=new TaskCompletionSource(); ;
    _tcs = new TaskCompletionSource();
    _thread = new Thread(() => { _CollectThread(tcsStart); _tcs.SetResult();/*线程结束触发完成*/ });
    _thread.Start();     
    //等待开始完成的信号 
    await tcsStart.Task;
    return Task.CompletedTask;
}
void _CollectThread(TaskCompletionSource tcsStart){
    while(GetMessage(out msg)!=0)
    {
       //接收到Wimm开始消息,触发完成
       tcsStart.SetResult();
       //接收到Wimm结束消息退出循环结束线程
    }   
}
 public async Task<Task> Stop()
 {
     if (_thread != null)
     { 
         //发送消息结束线程
         _exitFlag = true;
         PostThreadMessage(_threadId, MM_WIM_CLOSE); 
         //等待线程结束
         await _tcs!.Task;
         _tcs = null;
         _thread = null;
     }
     return Task.CompletedTask;
 }

3、使用异步流简化调用

因为使用了异步实现,结合参考dart的stream,以及C#8.0提供的异步流,可以进一步优化调用方式。首先Start和Stop是可以省略的,直接构造即开始,dispose即结束。然后是将构造和dispose放入同一个上下文,中间通过一个循环yield return返回采集数据。最后通过异步流,实现await foreach调用。

实现示例

///创建音频采集数据流,
 public static async IAsyncEnumerable<byte[]> CreateStream()
 {
 //构造采集对象
 //while(true)
 //{
 //子线程采集
 // yield return await 采集数据
 //}
 //dispose
 }

调用方式

await foreach(var i in CreateStream())
{
 //i即是采集的数据,需要停止采集则直接break。
}

四、完整代码

1.接口

由于采用了异步流的方式设计,接口有且仅有2个,即:CreateStream方法、和AvailableDevices属性。
接口设计如下:
.net 6.0

/************************************************************************
* @Project:  	AC::WaveInCollector
* @Decription:  音频采集工具
* @Verision:  	v1.2.0.0
* @Author:  	Xin Nie
* @Create:  	2023/10/8 09:27:00
* @LastUpdate:  2023/10/30 10:30:00
************************************************************************
* Copyright @ 2025. All rights reserved.
************************************************************************/
namespace AC
{
    /// 
    /// 声音采集对象
    /// 
    /// 
    /// 声音采集对象
    ///这是一个功能完整声音采集对象,所有接口通过了测试。
    ///采样了纯异步实现,接口变得极为简单。只有一个接口:创建异步流(CreateStream)
    ///需要循环await调用CreateStream获取音频数据(参考dart的stream),如果获取数据频率低于采集频率(比如循环内有耗时操作),内部会缓存数据,长度为10,超过则丢弃。
    ///私有化了原来的对象的new、start、stop等方法。只提供两个方法:创建异步流CreateStream。以及获取可用设备AvailableDevices。
    ///这样的做好处是极度简化了调用,避免了手动资源释,循环退出自动就释放了资源。
    /// 
    public class WaveInCollector : IAsyncDisposable
    {
        /// 
        ///创建音频采集数据流
        ///这是一个异步流(async stream),是C#8.0的特性,与dart的stream有点类似。
        ///参考 https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/generate-consume-asynchronous-stream
        /// 用法:
        /// await foreach(var i in WaveInCollector.CreateStream())
        /// {
        ///  //i即是采集的数据,需要停止采集则直接break。
        /// }
        /// 采集过程中遇到错误会抛出异常。
        /// 
        /// 设备Id,0为默认设备,可以通过WaveInCollector.AvailableDevices获取可用设备,同一个设备不能同时打开多个
        /// 声音格式,null时默认为:2,44100,16
        /// 缓冲区长度,即每次获取数据的长度。其值大小会影响采集频率,值越小采集频率越高
        /// 采集开始的Completion对象,通过这个对象可以获得采集开始事件
        /// 异步流
        public static async IAsyncEnumerable<byte[]> CreateStream(uint deviceId = 0, SampleFormat? sf = null, uint bufferLength = 8192, TaskCompletionSource? startedTcs = null);   
        /// 
        /// 枚举可用的声音采集设备
        /// 由于api限制Name长度最大为32
        /// 
        public static IEnumerable<AudioDevice> AvailableDevices {get;};
        /// 
        /// 异步dispose继承与IAsyncDisposable
        /// 
        /// 
        public async ValueTask DisposeAsync();
   }
     /// 
     /// 声音格式
     /// 
    public class SampleFormat
    {
        /// 
        /// 声道数
        /// 
        public ushort Channels { set; get; }
        /// 
        /// 采样率
        /// 
        public uint SampleRate { set; get; }
        /// 
        /// 位深
        /// 
        public ushort BitsPerSample { set; get; }
    }
     /// 
     /// 音频设备
     /// 
    public class AudioDevice
    {
        /// 
        /// 设备Id
        /// 
        public uint Id { set; get; }
        /// 
        /// 设备名称
        /// 
        public string Name { set; get; } = "";
        /// 
        /// 声道数
        /// 
        public int Channels { set; get; }
        /// 
        /// 支持的格式
        /// 
        public IEnumerable<SampleFormat> SupportedFormats { set; get; }
    }
}

2.具体实现

vs2022 .net6.0 项目,所有win api通过dllimport引入,没有任意额外依赖。
注:winmm不能识别dshow虚拟设备,请根据需要下载资源。
https://download.csdn.net/download/u013113678/88483543


五、使用示例

采集声音并保存为wav文件,其中的WavWriter对象参考《C# 将音频PCM数据封装成wav文件》

示例一

默认设备采集

using AC;
try
{
    using (var ww = WavWriter.Create("test.wav", 2, 44100, 16))
        await foreach (var i in WaveInCollector.CreateStream())
        {
            ww.Write(i);
            if (ww.WrittenLength / (2 * 44100 * 16 / 8) > 10/*录制时长10s*/)
                break;
        }
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

示例二

获取可用设备并采集,这是个完整的示例基本用到了所有接口元素。

using AC;
try
{
    //获取可用的音频设备
    var device = WaveInCollector.AvailableDevices.First();
    //创建wav文件
    using (var ww = WavWriter.Create("test.wav", device.SupportedFormats!.First().Channels, device.SupportedFormats!.First().SampleRate, device.SupportedFormats!.First().BitsPerSample))
    {
        //由于api限制设备名称不一定全。长度最大32。
        Console.WriteLine("设备名称:" + device.Name);
        Console.WriteLine("声音格式:Chanels=" + device.SupportedFormats.First().Channels + " SampleRate=" + device.SupportedFormats.First().SampleRate + " BitsPerSample=" + device.SupportedFormats.First().BitsPerSample);
        bool exitFlag = false;
        //录制10s后退出
        _ = Task.Delay(10000).ContinueWith(o => exitFlag = true);
        //tcs对象获取开始事件
        TaskCompletionSource tcs = new TaskCompletionSource();      
        _ = tcs.Task.ContinueWith(o =>/*开始事件*/ Console.WriteLine("录制开始"));
        //开始录制
        await foreach (var i in WaveInCollector.CreateStream(device.Id, device.SupportedFormats.First(),1024, tcs))
        {
            Console.WriteLine("接收数据长度" + i.Length);
            //数据写入文件
            ww.Write(i);
            if (exitFlag) break;
        }
        Console.WriteLine("录制完成");
    }
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

效果预览
C# 使用waveIn实现声音采集_第1张图片


总结

以上就是今天要讲的内容,实现waveIn声音采集虽然核心部分和C++一样,但是对于接口的设计以及调用流程都有很大的不同,尤其是C#的异步可以简化调用,使得接口变得很简洁,而且通过disposable又可以和using配合省去Stop的调用。但唯一比较麻烦的地方就是内存的互操作,尤其是音频数据缓存的读取和写入,在非unsafe的环境下会多一次拷贝。总的来说,这个功能在C#中实现还是有用的,调用简单而且没有额外依赖。

你可能感兴趣的:(.Net,音视频,1024程序员节,音视频,windows,c#,音频)