C#利用waveIn實現聲音采集
前言
之前實現了《C++ 使用waveIn實現聲音采集》,后來C#項目也有此功能的需求,直接調用C++封裝的dll是可以的。但是wimm這種基于win32 api的庫,完全可以直接用C#去調用,將依賴減少到最小。
一、需要的對象及方法
參考《C++ 使用waveIn實現聲音采集》,此處不再贅述。
二、整體流程
參考《C++ 使用waveIn實現聲音采集》,此處不再贅述。
三、關鍵實現
此處講一些與C#相關的點。
1、使用Thread開啟線程
筆者一開是實現是使用Task開啟線程,由于Task基于線程池可以提高資源利用率,但是這也出現了一些問題。由于錄制需要在子線程開啟消息循環(huán),多次重復調用錄制時,有概率打開同一個線程,就有可能收到上一個錄制的數據消息,造成非法內存的讀取問題。目前沒找到銷毀線程中消息循環(huán)的方法,只有通過結束線程的方式結束消息循環(huán)。所以使用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結束后觸發(fā)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();/*線程結束觸發(fā)完成*/ });
_thread.Start();
//等待開始完成的信號
await tcsStart.Task;
return Task.CompletedTask;
}
void _CollectThread(TaskCompletionSource tcsStart){
while(GetMessage(out msg)!=0)
{
//接收到Wimm開始消息,觸發(fā)完成
tcsStart.SetResult();
//接收到Wimm結束消息退出循環(huán)結束線程
}
}
public async Task<Task> Stop()
{
if (_thread != null)
{
//發(fā)送消息結束線程
_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>
/// 構造方法
/// 如果系統(tǒng)沒有任何設備則會拋出異常
/// </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();
//創(chuàng)建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
{
??????? //創(chuàng)建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的環(huán)境下會多一次拷貝。總的來說,這個功能在C#中實現還是有用的,調用簡單而且沒有額外依賴。
以上就是C#利用waveIn實現聲音采集的詳細內容,更多關于C# waveIn聲音采集的資料請關注腳本之家其它相關文章!
相關文章
C# winformTextBox 鍵盤監(jiān)聽方式
這篇文章主要介紹了C# winformTextBox 鍵盤監(jiān)聽方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-04-04

