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的比例进行切分。 方法一:切分训练集和测试集&…...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...
Caliper 配置文件解析:config.yaml
Caliper 是一个区块链性能基准测试工具,用于评估不同区块链平台的性能。下面我将详细解释你提供的 fisco-bcos.json 文件结构,并说明它与 config.yaml 文件的关系。 fisco-bcos.json 文件解析 这个文件是针对 FISCO-BCOS 区块链网络的 Caliper 配置文件,主要包含以下几个部…...
ABAP设计模式之---“简单设计原则(Simple Design)”
“Simple Design”(简单设计)是软件开发中的一个重要理念,倡导以最简单的方式实现软件功能,以确保代码清晰易懂、易维护,并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计,遵循“让事情保…...
Kafka入门-生产者
生产者 生产者发送流程: 延迟时间为0ms时,也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于:异步发送不需要等待结果,同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...
从 GreenPlum 到镜舟数据库:杭银消费金融湖仓一体转型实践
作者:吴岐诗,杭银消费金融大数据应用开发工程师 本文整理自杭银消费金融大数据应用开发工程师在StarRocks Summit Asia 2024的分享 引言:融合数据湖与数仓的创新之路 在数字金融时代,数据已成为金融机构的核心竞争力。杭银消费金…...
Python 训练营打卡 Day 47
注意力热力图可视化 在day 46代码的基础上,对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...
comfyui 工作流中 图生视频 如何增加视频的长度到5秒
comfyUI 工作流怎么可以生成更长的视频。除了硬件显存要求之外还有别的方法吗? 在ComfyUI中实现图生视频并延长到5秒,需要结合多个扩展和技巧。以下是完整解决方案: 核心工作流配置(24fps下5秒120帧) #mermaid-svg-yP…...
CentOS 7.9安装Nginx1.24.0时报 checking for LuaJIT 2.x ... not found
Nginx1.24编译时,报LuaJIT2.x错误, configuring additional modules adding module in /www/server/nginx/src/ngx_devel_kit ngx_devel_kit was configured adding module in /www/server/nginx/src/lua_nginx_module checking for LuaJIT 2.x ... not…...
Spring Boot 与 Kafka 的深度集成实践(二)
3. 生产者实现 3.1 生产者配置 在 Spring Boot 项目中,配置 Kafka 生产者主要是配置生产者工厂(ProducerFactory)和 KafkaTemplate 。生产者工厂负责创建 Kafka 生产者实例,而 KafkaTemplate 则是用于发送消息的核心组件&#x…...
compose 组件 ---无ui组件
在 Jetpack Compose 中,确实存在不直接参与 UI 渲染的组件,它们主要用于逻辑处理、状态管理或副作用控制。这些组件虽然没有视觉界面,但在架构中扮演重要角色。以下是常见的非 UI 组件及其用途: 1. 无 UI 的 Compose 组件分类 (…...
