C# 使用waveIn实现声音采集
文章目录
- 前言
- 一、需要的对象及方法
- 二、整体流程
- 三、关键实现
- 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#中实现还是有用的,调用简单而且没有额外依赖。
相关文章:

C# 使用waveIn实现声音采集
文章目录 前言一、需要的对象及方法二、整体流程三、关键实现1、使用Thread开启线程2、TaskCompletionSource实现异步3、将指针封装为Stream 四、完整代码1.接口2.具体实现 五、使用示例方式一方式二 总结 前言 之前实现了《C 使用waveIn实现声音采集》,后来C#项目…...

长连接的原理
Apollo的长连接实现是 Spring的DeferredResult来实现的,先看怎么用 import ...RestController RequestMapping("deferredResult") public class DeferredResultController {private Map<String, Consumer<DeferredResultResponse>> taskMap new HashMa…...

软考系列(系统架构师)- 2015年系统架构师软考案例分析考点
试题一 软件架构(质量属性效用树、架构风险、依够点、权衡点) 【问题1】(12分) 在架构评估过程中,质量属性效用树(utility tree)是对系统质量属性进行识别和优先级排序的重要工具。请给出合适的…...

小程序开发——小程序的视图与渲染
1.视图与渲染过程 基本概念: 视图层由WXML页面文件和样式文件WXSS共同组成。事件是视图层和逻辑层沟通的纽带,用户操作触发事件后可通过同名的事件处理函数执行相应的逻辑,处理完成后,更新的数据又将再次渲染到页面上。 WXML页面…...

用python实现操作mongodb的插入和查找操作
用python实现操作mongodb的插入和查找操作 import pymongoclient pymongo.MongoClient("mongo://localhost:27017") db client["app"] col db["C1"]# 插入一条数据 #user { # "name": "Sam", # "age":…...

代码审计及示例
简介: 代码安全测试是从安全的角度对代码进行的安全测试评估。 结合丰富的安全知识、编程经验、测试技术,利用静态分析和人工审核的方法寻找代码在架构和编码上的安全缺陷,在代码形成软件产品前将业务软件的安全风险降到最低。 方法&#x…...

【Kotlin精简】第6章 反射
1 反射简介 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性。 1.1 Kotlin反射 我们对比Kotlin和Java的反射类图。 1.1.1 Kotlin反射常用的数据结…...

基于FPGA的电风扇控制器verilog,视频/代码
名称:基于FPGA的电风扇控制器verilog 软件:QuartusII 语言:Verilog 代码功能: 基于FPGA的电风扇控制器 运用 EDA SOPO实验开发系统设计一个基于FPGA的电风扇定时开关控制器,能实现手动和自动模式之间的切换。要求: (1)KI为电…...

【MySQL】区分:等值连接/自连接/自然连接/外连接 以及ON和Where使用
区分:等值连接/自连接/自然连接/外连接 以及ON和Where使用 一、等值连接二、自连接三、自然连接四、外连接1.左外连接2.右外连接3.全外连接 五、using 和 on六、JOIN 关联表中 ON、WHERE 后面跟条件的区别 一、等值连接 等值连接:它是基于两个表之间的相…...

Windows环境下Apache安装部署说明及常见问题解决
一、软件准备 1.1 Python的下载与安装 见博客 链接: Python下载安装 1.2 Pycharm的下载与安装 见博客 链接: pycharm安装 1.3 Mysql的下载与安装 见博客 链接: MySQL安装 1.4 Navicat的下载与安装 可参考软件安装管家。 解释说明:Pycharm是Python的集成编译环境,Nav…...

Linux-安装docker-compose
前言:本文建立在服务器中已经存在docker环境的基础上,总结了安装docker-compose过程,以及安装过程中遇到的问题和解决方案。 一、下载docker-compose 在网上找了两种,一种是github官方的,一种是国内的镜像 gitbub官…...

机器学习实验一:KNN算法,手写数字数据集(使用汉明距离)
KNN-手写数字数据集: 使用sklearn中的KNN算法工具包( KNeighborsClassifier)替换实现分类器的构建,注意使用的是汉明距离; 分段解释代码: import os import pandas as pd from Levenshtein import hamming导入所需的库,包括os用于文件操作,pandas用于数据处理,以及hamm…...

Java零基础入门-赋值运算符
前言 Java是一门广泛被应用的编程语言,它被用于开发各种类型的应用程序,从桌面应用程序到企业级后端系统。对于零基础的人来说,学习Java可能会感到有些困难。本文将帮助那些没有编程经验的人了解Java的赋值运算符。 摘要 本文将介绍Java中…...

xshell+xming显示jmeter的gui页面
1.下载和安装xming,下载地址:https://sourceforge.net/projects/xming/ 2.配置xming 记住这个端口,一会要用到 修改进入xming安装目录修改host文件 此处是远程服务器的ip 3.服务器执行vi /etc/ssh/sshd_config,修改成如图所示…...

el-tree业务
<el-form-item label"选择节点" prop"node_ids"><el-checkboxv-if"regionList.length"v-model"selectAll":disabled"selectDisabled":indeterminate"isIndeterminate":show-checkbox"!selectDisabl…...

警惕Mallox勒索病毒的最新变种malloxx,您需要知道的预防和恢复方法。
导言: 恶意软件的威胁不断进化,其中之一是.malloxx勒索病毒。这种病毒可以加密您的文件,并要求您支付赎金以解锁它们。本文91数据恢复将详细介绍.malloxx勒索病毒,包括如何恢复被加密的数据文件以及如何预防这种威胁。如果受感染…...

linux中断下文之tasklet(中断二)
在申请 GPIO 中断时使用 request_irq,但是request_irq绑定的中断服务程序指的是中断上文。在 Linux 内核中,tasklet 是一种特殊的软中断机制,被广泛用于处理中断下文相关的任务。它是一种常见且有效的方法,在多核处理系统上可以避免并发问题。…...

Mysql事务+redo日志+锁分类+隔离级别+mvcc
事务: 是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行;事务是一组不可再分割的操作集合(工作逻辑单元)&a…...

Kafka-Java四:Spring配置Kafka消费者提交Offset的策略
一、Kafka消费者提交Offset的策略 Kafka消费者提交Offset的策略有 自动提交Offset: 消费者将消息拉取下来以后未被消费者消费前,直接自动提交offset。自动提交可能丢失数据,比如消息在被消费者消费前已经提交了offset,有可能消息…...

Python 训练集、测试集以及验证集切分方法:sklearn及手动切分
目录 方法一 方法二 需求目的:针对模型训练输入,按照6:2:2的比例进行训练集、测试集和验证集的划分。当前数据量约10万条。如果针对的是记录条数达上百万的数据集,可按照98:1:1的比例进行切分。 方法一:切分训练集和测试集&…...

数据结构,及分类(存储分类、逻辑分类)介绍
一、数据结构: 数据是软件开发的核心。在软件开发过程中基本上就是对数据的新增、删除、修改、查看的操作。 如何合理存储数据,如何有效提升数据操作开发效率,都是软件开发中的重中之重。使用合理的数据结构是非常重要的。 1.1简介ÿ…...

Powershell脚本自动备份dhcp数据库
文章目录 为什么要备份DHCP数据库呢?在PowerShell中自动备份DHCP数据库1,创建备份目录2,判断备份路径是否存在3,备份DHCP数据库4,完整自动备份脚本5,安排定期备份 推荐阅读 为什么要备份DHCP数据库呢&#…...

第十六章总结:反射和注解
.1.1:访问构造方法 反射: 1.class类 2.获取构造方法 3.获取成员属性 4.获取成员方法 注解 1.内置注解 2.反射注解 3 创建Class对象的三种方式 1.使用getClass()方法 object str new object()…...

mysql 切割字符串函数
93、mysql 切割字符串函数 需求,使用in 匹配多个参数,name字段值类型:1234(小明) 结果: select * from user where SUBSTRING_INDEX(REPLACE(name, ), ), (, -1) in ( 小明,小李)使用的函数如下 1、使用SUBSTRING_INDEX函数 SU…...

汽车发动机电机右盖设计
摘要 随着我国微型电子技术和社会经济的发展,目前行业内为满足客户需求出现了大量的电器设备,而大多数的电气设备的重要组成中都有电机,并且电机端盖成为电机研发人员重点关注和研究的对象,逐渐成为电机的重要组成部分,…...

ETHERNET/IP从站转CANOPEN主站连接AB系统的配置方法
你还在为配置网关的ETHERNET/IP从站和CANOPEN主站发愁吗?今天来教你解决办法! 一,首先,配置网关的ETHERNET/IP从站,需要使用AB系统的配置方法,具体步骤如下 1,使用 AB 系统配置网关的 ETHERNET/…...

人工智能和机器学习:走向智能未来的关键
人工智能(AI)和机器学习(ML)是当今IT领域中最令人振奋的发展方向之一。从自动驾驶汽车到智能助手,AI技术的应用正在不断扩展,重新定义着我们的生活方式和商业模式。在这个文章中,我们将深入探讨…...

openGauss本地Centos7.6单机安装和简单应用
openGauss本地Centos7.6单机安装和简单应用 openGauss基础环境配置openGauss安装openGauss使用测试openGauss常用命令 openGauss基础环境配置 在VMware Workstation中安装一台 centos7.6 内存:8GB,亲测4GB安装不够 磁盘:测试50GB-100GB够用 …...

LeetCode--1 两数之和
文章目录 1 题目描述2 解题思路2.1 暴力破解2.2 使用 Map 1 题目描述 给定一个整数数组 nums 和一个整数目标值 target, 请你在该数组中找出 和为目标值 target 的那 两个 整数, 并返回它们的数组下标 你可以假设每种输入只会对应一个答案。但是, 数组中同一个元素在答案里不…...

Hafnium安全分区管理器和示例参考软件栈
安全之安全(security)博客目录导读 目录 一、安全分区管理器 1、术语 2、对旧平台的支持 二、示例参考软件栈 一、安全分区管理器 安全分区管理器的三种实现在TF-A代码库并存: 1.基于FF-A规范的S-EL2 SPMC(SPM Core),使能安全…...