简介:NAudio是C#生态中应用广泛的开源音频处理库,这份源码包面向需要在.NET项目中实现录音、播放、格式转换与混音等功能的开发者,尤其适合关注高采样率录音与实时音频输入输出的场景。它支持WaveIn、MME等捕获接口以及WaveOut、ASIO等输出设备,还能进行常见格式转换与流式处理,应对大型音频文件。资源共873个文件,压缩包约2.99MB,以699个cs代码文件为主,另含XAML界面、resx资源、WAV样例音频、配置与Markdown文档。cs为项目核心逻辑,XAML与resx负责界面和资源,WAV可用于测试音频功能,结构清晰。目前已有417人学习下载。包内完整项目工程可帮助理解WaveIn、WaveOut、ASIO、MixingSampleProvider等核心类的工作原理,并可直接参考或嵌入自己的应用中,适合具备C#基础、希望深入音频处理或快速搭建录音方案的开发者。
1. 为什么 .NET 录音场景绕不开 NAudio
在 C# 上位机里把麦克风录音和背景音乐混在一起存成文件,顺手还要看到实时的音频电平和设备热插拔变化,这种需求在工控和桌面工具里很典型。Windows 原生提供的声音 API 不止一套,老的 MME、后来的 WASAPI、以及为了低延迟而存在的 ASIO,每一层都要自己处理设备枚举、缓冲区、回调线程和格式协商,直接 P/Invoke 写一遍少说几百行,还容易换一台机器就翻车。NAudio 把这些底层接口封装成能直接 new 的类,录音、回放、格式转换、混音都有现成入口,源码也公开,遇到设备兼容性问题还能往下追到 P/Invoke 层。这篇文章不会去罗列 API 手册,而是围绕录音这条主线,把采样率、位深、缓冲区这几个最容易被忽略的参数讲清楚,顺便也给已经在用 NAudio 的人一些排错方向。
2. 录音管线选型:WaveInEvent、WaveIn 与 WasapiCapture 的实际差异
第一次接触 NAudio 的人,容易把 WaveInEvent、WaveIn、WasapiCapture 当成同一个东西的不同写法。实际上它们对应的是不同系统音频栈。WaveIn 和 WaveInEvent 走的是 MME,兼容性最广,但延迟和采样率上限受驱动影响非常大;WasapiCapture 走 WASAPI 共享模式,Win10 之后的稳定性明显更好,也更适合处理 96k 这种高采样率场景。
2.1 三种捕获接口的适用边界
我在实际项目里一般按这个表来选型:
| 接口 | 系统音频栈 | 延迟 | 采样率上限 | 适合场景 |
|---|---|---|---|---|
| WaveInEvent | MME | 30-80ms | 多数 USB 声卡只暴露到 48k | 录音、语音通讯、工控上位机 |
| WaveIn | MME | 手动控制缓冲区 | 同上 | 对回调时机有特殊要求的老项目 |
| WasapiCapture | WASAPI 共享模式 | 10-30ms | 取决于设备 MixFormat,可到 96k/192k | 高采样率录音、低延迟监听 |
WaveInEvent 是线程池回调,开发者不需要在窗口消息里折腾波形设备,这是它适合快速落地的原因。WaveIn 要自己考虑回调中的内存拷贝和线程同步,除非维护老代码,否则现在新项目没必要再从它开始。WasapiCapture 虽然延迟更好,但要注意它工作在共享模式,独占模式的低延迟效果并不是由这个类提供的,定位不要搞混。
2.2 WaveInEvent 最小可运行录音代码
先给一套能直接抄走的 WaveInEvent 录音实现,包含开始录制、数据写入、停止释放三个关键点:
private WaveInEvent _capture; private WaveFileWriter _writer; public void Start(string filePath) { if (WaveInEvent.DeviceCount == 0) throw new InvalidOperationException("没有找到可用录音设备"); _capture = new WaveInEvent { DeviceNumber = 0, // 默认麦克风 WaveFormat = new WaveFormat(44100, 16, 1), // 44.1kHz, 16bit, 单声道 BufferMilliseconds = 25, // 每个缓冲区的毫秒数 NumberOfBuffers = 3 // 缓冲区数量 }; _capture.DataAvailable += OnDataAvailable; _capture.RecordingStopped += OnRecordingStopped; _writer = new WaveFileWriter(filePath, _capture.WaveFormat); _capture.StartRecording(); } private void OnDataAvailable(object sender, WaveInEventArgs e) { _writer.Write(e.Buffer, 0, e.BytesRecorded); } private void OnRecordingStopped(object sender, StoppedEventArgs e) { _capture.Dispose(); _writer.Dispose(); _writer = null; _capture = null; }这段代码的核心是把WaveInEvent的DataAvailable回调数据直接交给WaveFileWriter。WaveFileWriter在构造函数里会先写 WAV 头,停止时通过Dispose把头部的大小字段补正确,所以不要让 writer 提前被垃圾回收。
BufferMilliseconds决定回调频率,25ms 对大多数场景足够。如果把它压到 10ms 以下,MME 驱动反而可能因为缓冲不足出现丢数据,表现为录出来的时长明显短于实际录音时长。NumberOfBuffers表示系统在后台准备几块缓冲来防止驱动抖动,默认是 3,一般不用改。
2.3 高采样率录音别直接塞 WaveFormat
highers1r 这个标签,我理解就是 higher sample rate,也就是 96kHz 甚至 192kHz 这类高采样率。WaveInEvent 在部分 USB 麦克风上能协商到 48kHz,再往上往往报错或者录出静音文件。WASAPI 共享模式下,设备驱动会提供一个 MixFormat,很多设备的 MixFormat 本身就是 48k 的整数倍,所以走 WasapiCapture 更靠谱。
常见做法是继承WasapiCapture,重写GetCaptureWaveFormat来指定目标格式:
public class HighSampleRateCapture : WasapiCapture { private readonly int _sampleRate; public HighSampleRateCapture(MMDevice device, int sampleRate) : base(device) { _sampleRate = sampleRate; } protected override WaveFormat GetCaptureWaveFormat() { return new WaveFormat(_sampleRate, 24, 2); // 96kHz, 24bit, 双声道 } }启动时从MMDeviceEnumerator拿到录音设备:
var enumerator = new MMDeviceEnumerator(); var devices = enumerator .EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active) .ToArray(); using var capture = new HighSampleRateCapture(devices[0], 96000); using var writer = new WaveFileWriter("high.wav", new WaveFormat(96000, 24, 2)); capture.DataAvailable += (s, e) => writer.Write(e.Buffer, 0, e.BytesRecorded); capture.RecordingStopped += (s, e) => writer.Dispose(); capture.StartRecording();注意:不是所有设备都支持 96k,
StartRecording可能抛出AudioClientException,错误码0x88890008就是AUDCLNT_E_UNSUPPORTED_FORMAT。上线前最好把 96k、48k、44.1k 做成降级链,依次尝试初始化,而不是让用户手动猜。
3. 回放与格式转换:WaveOutEvent、ASIO 与 MediaFoundation 的采样率边界
录音之后最常见的需求就是回放和格式转换。NAudio 里播放这一层有 WaveOutEvent、WasapiOut、AsioOut 三个主要入口,功能上都能把 IWaveProvider 或者 ISampleProvider 送到声卡,但它们的延迟特性和驱动依赖差别很大。
3.1 播放器选型
| 播放器 | 底层 | 延迟 | 使用注意 |
|---|---|---|---|
| WaveOutEvent | MME | 中等 | 兼容性最好,适合通用播放器 |
| WasapiOut | WASAPI | 较低,可独占模式 | 独占模式会占用设备,其他程序无声 |
| AsioOut | ASIO | 极低 | 必须安装对应声卡的 ASIO 驱动 |
普通上位机里我一般用 WaveOutEvent,因为它不挑设备。代码很短:
using var reader = new AudioFileReader("input.mp3"); using var player = new WaveOutEvent(); player.DesiredLatency = 200; player.NumberOfBuffers = 2; player.Init(reader); player.Play(); while (player.PlaybackState == PlaybackState.Playing) { Thread.Sleep(100); }AudioFileReader是 NAudio 里的一个复合类,内部走 MediaFoundation,能直接读 WAV、MP3、AAC。DesiredLatency的单位是毫秒,200ms 对语音播放足够,也不会让声卡缓冲溢出。如果做实时监听,再考虑 WasapiOut 或 AsioOut。
3.2 MP3 转 WAV 的两种姿势
格式转换最典型的场景是 MP3 转 WAV。第一种是只做解码,保持源采样率不变:
using var reader = new MediaFoundationReader("input.mp3"); using var pcm = WaveFormatConversionStream.CreatePcmStream(reader); WaveFileWriter.CreateWaveFile("output.wav", pcm);CreatePcmStream只把压缩编码转成 PCM,不会做重采样。如果 MP3 是 44.1kHz,输出 WAV 也是 44.1kHz,位深变成 16bit。
第二种是需要改变采样率或者位深,比如把 44.1k 的 MP3 转换成 48k 的 WAV:
using var reader = new MediaFoundationReader("input.mp3"); using var resampler = new MediaFoundationResampler(reader, new WaveFormat(48000, 16, 2)) { ResamplerQuality = 60 }; WaveFileWriter.CreateWaveFile("output_48k.wav", resampler);MediaFoundationResampler直接调用 Windows Media Foundation 的重采样器,能同时处理采样率、位深和通道数三个维度的转换。ResamplerQuality取值范围是 1 到 60,越高越吃 CPU。离线转换踩不到实时限制,我会直接给到 60,如果是流处理就给 30 左右。
3.3 转码前一定要确认的坑
WaveFormatConversionStream 和 MediaFoundationResampler 最大的区别是:前者只改编码,不改采样率;后者可以顺手把 96k 降到 48k,甚至在 5.1 声道和立体声之间做 channel mapping。转码后如果拿AudioFileReader再打开一次,能看到WaveFormat.SampleRate,这一步建议每次转完都自查一下。
提示:在 Windows Server 上跑 NAudio,MediaFoundation 需要安装“桌面体验”或“Media Foundation”功能,否则
MediaFoundationReader可能抛 COM 异常,这不是代码问题。
4. 混音与音量平衡:用 ISampleProvider 把两条音频流合成一路
混音是 NAudio 里最体现设计思路的部分。很多人一开始会去找 WaveStream 合并,但实际项目里更常见的是先统一成 ISampleProvider,再交给 MixingSampleProvider。原因是 float 样本天然能表达负数,混音后不会因为 byte 溢出产生异常爆音。
4.1 混音前先弄清楚 WaveProvider 和 SampleProvider
WaveProvider 返回 byte[],SampleProvider 返回 float[]。byte 数组要做 16bit 采样点切分和符号扩展,float 数组则是标准化的 -1.0 到 1.0,处理起来直观得多。NAudio 给 WaveInEvent 这类捕获源提供了WaveInProvider包装,再调用ToSampleProvider()就能得到 ISampleProvider;AudioFileReader本身实现了 ISampleProvider,可以直接参与混音。
4.2 离线混音:麦克风文件 + 背景音乐
这个流程适合先把麦克风录成 WAV,再和背景音乐文件离线合并。代码很清晰:
using var mic = new AudioFileReader("mic.wav"); using var bgm = new AudioFileReader("bgm.mp3"); var micSample = mic.ToSampleProvider(); // 背景音乐采样率先对齐到麦克风文件 var bgmResampled = new WdlResamplingSampleProvider(bgm, mic.WaveFormat.SampleRate); // 如果背景音乐是单声道,转成双声道再混音 ISampleProvider bgmSample = bgmResampled.WaveFormat.Channels == 1 ? new MonoToStereoSampleProvider(bgmResampled) : bgmResampled; var mixer = new MixingSampleProvider(new ISampleProvider[] { micSample, bgmSample }); // 输出格式用 IEEE float,采样率和通道数跟随麦克风文件 var outFormat = WaveFormat.CreateIeeeFloatWaveFormat( mic.WaveFormat.SampleRate, mic.WaveFormat.Channels); using var writer = new WaveFileWriter("mixed.wav", outFormat); // 缓冲区固定为 10ms,避免一次循环里堆积太多样本 var buffer = new float[outFormat.AverageBytesPerSecond / 100]; int n; while ((n = mixer.Read(buffer, 0, buffer.Length)) > 0) { writer.WriteSamples(buffer, 0, n); }WdlResamplingSampleProvider是 NAudio 内置的采样率转换器,质量比 MediaFoundationResampler 略低,但不需要额外依赖,适合文件间对齐。缓冲区大小用AverageBytesPerSecond / 100是取 10ms 的样本量,这样在 44.1kHz 和 96kHz 下都能保持自然的计算节奏。
4.3 给每路信号加音量控制
混音前包一层VolumeSampleProvider是最简单的音量平衡做法:
var micVolume = new VolumeSampleProvider(micSample) { Volume = 1.0f }; var bgmVolume = new VolumeSampleProvider(bgmSample) { Volume = 0.6f }; var mixer = new MixingSampleProvider(new ISampleProvider[] { micVolume, bgmVolume });Volume的范围是 0.0 到 1.0,可以超过 1.0,但两个超过 1.0 的信号叠加后,MixingSampleProvider不会做 normalize,结果就是削波。所以背景音乐一般给到 0.5-0.7,有语音活动的时候再压低到 0.3,比后期想去削波要省事得多。
5. 上线前的三个验证动作:削波检测、文件时长和设备回退
代码写出来只是第一步,真正花时间的是验证不同设备上的表现。我每次集成完 NAudio,都会做下面三个动作,能省掉很多现场问题。
5.1 削波检测
混音文件生成后,用同一个MixingSampleProvider再读一遍,统计峰值:
float peak = 0f; var sampleBuffer = new float[4096]; int read; while ((read = mixer.Read(sampleBuffer, 0, sampleBuffer.Length)) > 0) { for (int i = 0; i < read; i++) { float v = Math.Abs(sampleBuffer[i]); if (v > peak) peak = v; } } if (peak >= 0.98f) { // 接近削波,需要压低背景音乐或麦克风增益 }这个峰值检测不是专业响度分析,但能快速发现混音后是否接近 0dBFS。
5.2 文件时长核对
WAV 时长比预期短,通常不是编码问题,而是录音回调丢数据。用AudioFileReader打开成品文件:
using var check = new AudioFileReader("mixed.wav"); Console.WriteLine($"实际时长: {check.TotalTime.TotalSeconds:F3}s");如果录音 10 秒但文件只有 8 秒,优先检查BufferMilliseconds是不是太小,其次换WasapiCapture再试。
5.3 设备采样率回退
最后是高采样率设备上的降级逻辑:
int[] rates = { 96000, 48000, 44100 }; HighSampleRateCapture? active = null; foreach (int rate in rates) { try { active = new HighSampleRateCapture(devices[0], rate); active.StartRecording(); break; } catch { active?.Dispose(); active = null; } } if (active == null) { // 所有采样率都失败,提示用户检查麦克风驱动 }把 96k 作为首选,失败后自动落到 48k,再不行落到 44.1k。这套降级链能覆盖大部分 USB 声卡和板载声卡,现场调试时也能从最终使用的采样率快速定位是哪一层驱动不兼容。
本文还有配套的精品资源,点击获取