C# 使用waveIn实现声音采集

news2025/1/14 18:21:37

文章目录

  • 前言
  • 一、需要的对象及方法
  • 二、整体流程
  • 三、关键实现
    • 1、使用Thread开启线程
    • 2、TaskCompletionSource实现异步
    • 3、将指针封装为Stream
  • 四、完整代码
    • 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机制,这样就可以直接去掉开始和停止两个回调,使用异步实现开始和停止方法。

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

通过Wimm采集的音频数据是指针的形式,如果需要转为byte[]这需要使用Marshall进行数据拷贝,为了避免拷贝,数据形式不能是byte[]数组。直接提供指针又不方便使用,笔者采用了Stream的方式提供数据,而且文件流直接支持Stream写入。C#本身有个UmanagedMemoryStream可以支持读取指针的数据,但是需要unsafe上下文,这显然是没必要的(有unsafe上下文,直接通过地址读取数据即可,或者将此功能放dll单独设置unsafe对外提供Stream也不便于管理)。最好的方式还是自己实现一个Stream用于读取指针数据。
完整代码如下:

using System.Runtime.InteropServices;
namespace AC
{
    /// <summary>
    /// 用于读取指针数据的流,内部不会管理指针
    /// 由于.net库提供的UnmanagedMemoryStream需要unsafe上下文,所以直接自己封装一个类似功能的stream避开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>
        /// 构造方法
        /// </summary>
        /// <param name="ptr">数据地址</param>
        /// <param name="len">数据长度</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;
        }
    }
}

四、完整代码

将采集功能封装成一个通用工具,方便在任意地方使用。

1.接口

接口设计如下:

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>
    /// 声音采集对象
    /// </summary>
    /// <summary>
    /// 声音采集对象
    ///这是一个功能完整声音采集对象,所有接口通过了测试。
    ///非线程安全,所有方法需确保在单线程中调用,即比如:Start和Stop不能在两个线程中同时调用。
    /// </summary>
    public class WaveInCollector : IAsyncDisposable
    {
        /// <summary>
        /// 数据到达事件参数
        /// </summary>
        public class DataArrivedEventArgs : EventArgs
        {
            /// <summary>
            /// 声音格式
            /// </summary>
            public SampleFormat Format { set; get; }
            /// <summary>
            /// 声音数据流,为了减少数据拷贝次数,将非托管内存封装成流的形式提供,只读,生命周期为回调方法内。
            /// </summary>
            public Stream Stream { set; get; }
        }
        /// <summary>
        /// 采集数据到达事件
        /// </summary>
        public event EventHandler<DataArrivedEventArgs>? DataArrived;
        /// <summary>
        /// 采集速率单位:次/秒
        /// 此属性会影响每次输出数据的大小
        /// 开始采集前设置有效
        /// </summary>
        public int Frequency { set; get; } = 50;
        /// <summary>
        /// 声音格式
        /// </summary>
        public SampleFormat Format { private set; get; }
        /// <summary>
        /// 当前设备
        /// </summary>
        public AudioDevice Device { private set; get; }
        /// <summary>
        /// 枚举可用的声音采集设备
        /// </summary>
        public static IEnumerable<AudioDevice> AvailableDevices { get; }
        /// <summary>
        /// 构造方法
        /// </summary>
        /// <param name="device">音频设备,不能为空</param>
        /// <param name="SampleFormat">声音格式</param>
        public WaveInCollector(AudioDevice device, SampleFormat sf);
        /// <summary>
        /// 构造方法
        /// 如果系统没有任何设备则会抛出异常
        /// </summary>
        /// <param name="deviceId">声音设备Id,0为默认设备</param>
        /// <param name="SampleFormat">声音格式</param>
        public WaveInCollector(uint deviceId, SampleFormat sf) : this(GetWaveInDeviceById(deviceId)!, sf) { }
        /// <summary>
        /// 开始采集,Start和Stop需要成对使用,await可变成同步式,真正开始采集才会返回。
        /// 失败会抛出异常,可通过ContinueWith或await获取异常。
        /// </summary>		
        public async Task<Task> Start();
        /// <summary>
        /// 停止采集,直接调用是异步,可await等待真正停止
        /// 此方法是有可能抛异常的,采集过程中出现的异常,会在此方法中抛出
        /// </summary>
        public async Task<Task> Stop();
    }
     /// <summary>
     /// 声音格式
     /// </summary>
    public class SampleFormat
    {
        /// <summary>
        /// 声道数
        /// </summary>
        public ushort Channels { set; get; }
        /// <summary>
        /// 采样率
        /// </summary>
        public uint SampleRate { set; get; }
        /// <summary>
        /// 位深
        /// </summary>
        public ushort BitsPerSample { set; get; }
    }
     /// <summary>
     /// 音频设备
     /// </summary>
    public class AudioDevice
    {
        /// <summary>
        /// 设备Id
        /// </summary>
        public uint Id { set; get; }
        /// <summary>
        /// 设备名称
        /// </summary>
        public string Name { set; get; } = "";
        /// <summary>
        /// 声道数
        /// </summary>
        public int Channels { set; get; }
        /// <summary>
        /// 支持的格式
        /// </summary>
        public IEnumerable<SampleFormat> SupportedFormats { set; get; }
    }
}

2.具体实现

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


五、使用示例

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

方式一

获取可用设备并采集

// See https://aka.ms/new-console-template for more information
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))
    {
        //初始化录制对象
        await using (var wic = new WaveInCollector(device.Id, device.SupportedFormats!.First()))
        {
            //由于api限制设备名称不一定全。长度最大32。
            Console.WriteLine("设备名称:" + wic.Device.Name);
            Console.WriteLine("声音格式:Chanels=" + wic.Format.Channels +
                " SampleRate=" + wic.Format.SampleRate +
                " BitsPerSample=" + wic.Format.BitsPerSample
                );
            Console.WriteLine("开始录制");
            //注册录制事件
            wic.DataArrived += (s, e) =>
            {
                Console.WriteLine("接收数据长度" + e.Stream.Length);
                //写入文件
                ww.Write(e.Stream);
            };
            //开始录制
            await wic.Start();
            //录制10s结束
            await Task.Delay(10000);
            Console.WriteLine("开始完成");
        }
    }
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

方式二

指定设备下标和声音格式

// See https://aka.ms/new-console-template for more information
using AC;
try
{

    //创建wav文件
    using (var ww = WavWriter.Create("test.wav", 2, 44100, 16))
    {
        //初始化录制对象
        await using (var wic = new WaveInCollector(0, new SampleFormat() { Channels = 2, SampleRate = 44100, BitsPerSample = 16 }))
        {
            //由于api限制设备名称不一定全。长度最大32。
            Console.WriteLine("设备名称:" + wic.Device.Name);
            Console.WriteLine("声音格式:Chanels=" + wic.Format.Channels +
                " SampleRate=" + wic.Format.SampleRate +
                " BitsPerSample=" + wic.Format.BitsPerSample
                );
            Console.WriteLine("开始录制");
            //注册录制事件
            wic.DataArrived += (s, e) =>
            {
                Console.WriteLine("接收数据长度" + e.Stream.Length);
                //写入文件
                ww.Write(e.Stream);
            };
            //开始录制
            await wic.Start();
            //录制10s结束
            await Task.Delay(10000);
            Console.WriteLine("开始完成");
        }
    }
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

效果预览
在这里插入图片描述


总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1127567.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ubuntu 命令行设置静态IP地址方法

一、先ifconfig查看电脑的网卡信息 找到有线网络或WiFi网络的网卡名称&#xff0c;我这里是eno1 二、输入route -n命令&#xff0c;打印路由表&#xff0c;这里主要是为了查看网关地址 我这里网关地址是192.168.10.1 三、更改配置文件 输入 vim /etc/network/interfaces&am…

企业一般纳税人查询API:简化税务信息获取的利器

前言 随着数字化时代的到来&#xff0c;企业纳税和财务管理领域也经历了革命性的变化。税务管理不再是繁琐的手动工作&#xff0c;而是通过技术工具实现高效和精确。其中&#xff0c;企业一般纳税人查询API成为了企业税务信息获取的强大利器。这一工具不仅简化了税务信息的访问…

微信公众号怎么添加送餐外卖系统

在当今快节奏的生活中&#xff0c;外卖已经成为了人们解决日常饮食需求的重要方式。微信公众号作为一个拥有广泛用户群体的平台&#xff0c;加入送餐外卖系统可以为公众号持有者带来更多的商业机会和用户便利。本文将介绍如何在微信公众号中添加送餐外卖系统&#xff0c;提升公…

系统设计 - 我们如何通俗的理解那些技术的运行原理 - 第七部分:Git、云服务、生产力工具

本心、输入输出、结果 文章目录 系统设计 - 我们如何通俗的理解那些技术的运行原理 - 第七部分&#xff1a;Git、云服务、生产力工具前言Git &#xff1a;Git 命令的工作原理Git 如何工作Git merge vs. Git rebaseGit mergeGit rebaseGit rebaes 的黄金法则 云服务 : 不同云服务…

【C++】网络在线五子棋

项目介绍 本项目主要实现⼀个网页版的五⼦棋对战游戏&#xff0c;其主要⽀持以下核心功能&#xff1a; • 用户管理&#xff1a;实现用户注册&#xff0c;用户登录、获取用户信息、用户天梯分数记录、用户比赛场次记录等 • 匹配对战&#xff1a;实现两个玩家在网页端根据天梯分…

C++前缀和算法的应用:石头游戏 VIII 原理源码测试用例

本文涉及的基础知识点 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 题目 Alice 和 Bob 玩一个游戏&#xff0c;两人轮流操作&#xff0c; Alice 先手 。 总共有 n 个石子排成一行。轮到某个玩家的回合时&#xff0c;如果石子的数目 大…

ACM练习C++知识点笔记

1、字符和数字的转换 #include<iostream> using namespace std; int main(){int n 8 - 48;cout<<n<<endl;return 0; } 数字转字符串 #include <string> #include <sstream> #include <iostream> using namespace std; int main() {doubl…

基于Django开发的推荐系统与数据分析系统

基于Django开发的推荐系统与数据分析系统 一、简介 已开发的的推荐系统&#xff1a;图书管理系统、电影推荐系统、在线选修课程推荐系统、健身推荐系统、资讯推荐系统&#xff1b; 已开发的数据分析系统&#xff1a;大众点评店铺数据分析系统。 推荐系统的目的是信息过载所…

rockchip 3588 HDMI avmute

概述 HDMI (High-Definition Multimedia Interface) 是一种数字接口标准&#xff0c;用于传输高清视频和多通道音频信号。AVMUTE 是 HDMI 规范中的一个术语&#xff0c;表示"Audio-Video Mute"&#xff08;音视频静音&#xff09;。AVMUTE 通常与 HDMI 设备的音频和…

提高Java程序性能!了解可达性分析算法、强软弱虚引用和三色标记GC的过程,避免不可达对象阻碍程序性能!

文章目录 &#x1f34a; 可达性分析算法&#x1f34a; 强软弱虚引用&#x1f389; 强引用&#x1f389; 软引用&#x1f389; 弱引用&#x1f389; 虚引用 &#x1f34a; 不可达对象GC的过程&#x1f389; GC中不可达对象的回收过程&#x1f4dd; 1. 标记阶段&#x1f4dd; 2. …

远程IO模块物联网应用提高工业自动化生产效率

远程IO模块是一款常用于工业自动化领域的通讯设备&#xff0c;它可以实现远程监测&#xff0c;帮助企业更加有效地掌控生产状态&#xff0c;提高生产效率。远程IO模块的作用是将分散的输入输出信号集中管理&#xff0c;实现实时数据采集、传输与控制。 远程IO模块通过安装在设…

Windows / Ubuntu 连wifi,网线连接旭日X3派以共享网络

首先&#xff0c;PC电脑连好wifi Windows 找到【控制面板->网络和Internet->网络和共享中心->查看网络状态和任务->更改适配器设置】 找到WLAN&#xff0c;右键【属性->共享】勾上允许&#xff0c;然后【确定】。 Ubuntu 打开设置&#xff0c;找到有线设置…

2024SCI经验心得分享---如何在零基础、导师基本放养的情况下---发表自己的第一篇SCI(三区)经验分享篇

本期的经验分享&#xff0c;采访到了我的一位非常非常非常优秀的师妹&#xff0c;师妹于今年6月份投稿&#xff0c;10月份录用&#xff0c;历时四个月录用了自己的第一篇SCI&#xff08;三区&#xff09;的文章图像处理类的&#xff0c;同时师妹也取得了很多其他优秀的荣誉。 众…

Python数据结构(栈)

Python数据结构&#xff08;栈&#xff09; 栈(stack)&#xff0c;有些地方称为堆栈&#xff0c;是一种容器&#xff0c;可存入数据元素、访问元素、删除元素&#xff0c;它的特点在于只能允许在容器的一端(称为栈顶端指标&#xff0c;英语: top)进行加入数据(英语: push)和输…

手撕Vue-实现计算属性

前言 经过上一篇的学习, 完成了将数据代理到了 Nue 的实例上方&#xff0c;这个我们已经撕完了。接下来要实现的是计算属性&#xff0c;计算属性的实现原理是通过 Object.defineProperty() 来实现的&#xff0c;我们先来看看计算属性的使用。 看之前先来改造一下我们的代码基础…

DevExpress WinForms地图组件 - 轻松集成地图功能到应用程序

DevExpress WinForms地图控件允许您在WinForms应用程序中合并地图服务&#xff0c;您可以选择现有的地图资源&#xff0c;如如Bing或OpenStreetMap&#xff0c;或者在公司网络中创建自己的地图数据服务器。DevExpress WinForms地图控件完全支持矢量和笛卡尔坐标地图。 DevExpr…

Plex踩坑——移动缓存目录

plex在生成缩略图、刮削视频的时候会生成大量的缓存文件&#xff0c;占用磁盘空间。 plex默认缓存存储位置为C:\Users\xxx\AppData\Local\Plex Media Server&#xff0c;并且这个路径在plex设置中无法更改。 可以通过修改注册表的方式修改该路径。 首先推出plex账号&#xff…

学习视觉CV Transformer (1)--Transformer介绍

先放Transformer的经典文章 Attention Is All You Need 论文代码&#xff1a;https://paperswithcode.com/paper/attention-is-all-you-need Transformer结构是google在17年的Attention Is All You Need论文中提出&#xff0c;首先主要是在自然语言处理NLP方面应用&#xff0c…

万物“邮”爱,百余志愿者参与邮票艺术共创助力生物多样性

邮票作为生动形象的文化载体&#xff0c;传承着中华文化的历史和文明。自邮票问世以来&#xff0c;就以其精美的设计和图案&#xff0c;成为人们珍藏文化遗产和学习历史文化的窗口。野生动物保护一直是人们所关注的热门话题&#xff0c;相关部门也为宣传、拯救、保护珍贵濒危野…

[量化投资-学习笔记001]Python+TDengine从零开始搭建量化分析平台-数据存储

目录 0. 简介1. 获取交易数据2. 数据库搭建2.1. 数据库安装2.2. 创建数据库2.3. 创建超级表2.4. 创建子表 3.数据导入4. Grafana 安装4.1. 安装Grafana4.2. 安装TDengine插件 附件数据导入脚本历史交易数据-1分钟K线 0. 简介 Python&#xff1a;最常用的量化分析语言&#xff0…