简介:面向WPF初、中级开发者的录音与音频播放实战资源,基于.NET Framework 4.5与Visual Studio 2017,集中解决WPF应用中音频采集、播放控制、UI联动和本地音频信息读取等常见需求,适用于聊天软件、教育工具等场景。压缩包共175个文件,包含45个C#源码、13个DLL动态库、9个BAML界面编译文件、5个WAV测试音频以及xaml、config、exe等类型,整体约3.18MB,目前已有926人学习下载。资源内容围绕NAudio库与MediaPlayer类展开,既演示了通过WasapiLoopbackCapture捕获声卡数据并写入文件的完整录音流程,也给出了音频文件加载、播放、暂停、继续、停止及资源释放的调用示例。同时结合WPF按钮事件绑定说明如何触发录音与播放,并利用AudioFileReader获取音频时长等元信息,按需查阅这些代码与配套工程文件,可少走弯路,快速搭建具备基础音频处理能力的WPF应用。
1. wpf 录音和播放音频:先解决“采集端和播放端各管一段”的问题
wpf 录音和播放音频,最容易被导航带偏的地方,是把“录音”和“播放”当成同一个问题去查资料。SoundPlayer 点几下就能播 WAV,但录音涉及设备句柄、缓冲区、线程回调和文件头写入,两件事放在同一个窗口时,方案往往一拍脑袋就定了。我建议动手前先想清楚三件事:录多长时间、存成什么格式、播放是否需要音浪反馈。这三个答案直接决定你是用 NAudio 包办采集与播放,还是 SoundPlayer、MediaPlayer 各管一段。这篇按采集、播放、UI 集成、排错、验证的顺序展开,适合正在 WPF 里做语音备忘录、通话质检或本地音频采集的 .NET 开发者。
2. 录音链路:为什么绕不开 NAudio,从设备枚举到 WAV 落盘的完整代码
2.1 WPF 没有“开箱即用”的录音 API,选型其实只有三条路
WPF 本身没有暴露麦克风采集接口。常见选择有三条路:一是用 NAudio 这类托管封装库,底层映射到 Windows 的 waveIn / WASAPI;二是通过 WinRT 的 MediaCapture,但这套 API 主要面向 UWP / .NET MAUI 场景,传统 WPF 桌面项目里要处理权限模型和异步生命周期的成本很高;三是自己 P/Invoke waveInXxx 系列函数,完整走一遍设备句柄、缓冲区队列和回调,工作量约等于重造半个 NAudio。
我一般直接选 NAudio。理由不是它的 API 最简单,而是它的 WaveInEvent 把设备枚举、缓冲区管理和回调线程都整理好了,出错时你能把精力放在“音频数据怎么处理”上,而不是“Windows 底层为什么又给我一个空指针”。如果你评估过 .NET MAUI,会发现那边的录音方案基本走 MediaCapture,代码和 NAudio 不通用,所以别抱着“跨平台”的心态在 WPF 里选型,先把当前项目的桌面场景做扎实。
2.2 最小可用录音机:枚举设备、打开流、把数据写进 WAV
先从枚举设备开始。多麦克风环境里,DeviceNumber 写死 0 经常会录到立体声混音甚至空设备,所以第一步是把设备列表打出来确认下标。
for (int i = 0; i < WaveInEvent.DeviceCount; i++) { var caps = WaveInEvent.GetCapabilities(i); Console.WriteLine($"{i}: {caps.ProductName}"); }这段代码返回的是当前 Windows 已识别的录音设备,ProductName 通常是“Microphone Array”“Realtek High Definition Audio”之类。拿到下标后,把它作为 DeviceNumber 传给录音器,不要用控件的默认 0。设备插拔后下标可能变化,所以正式项目里最好在启动时扫描一次并存起来。
接下来是最小录音实现。重点是把 WaveInEvent 的生命周期挂到类字段上,而不是放进一个局部方法里。
// 录音核心:WaveInEvent 负责采集,WaveFileWriter 负责把 PCM 写进 WAV private WaveInEvent _waveIn; private WaveFileWriter _writer; private string _savePath; void StartRecording(string filePath) { _savePath = filePath; _waveIn = new WaveInEvent { DeviceNumber = 0, // 用刚才枚举出来的下标 BufferMilliseconds = 50, // 每 50ms 回调一次,越小越跟手,越小越容易丢 WaveFormat = new WaveFormat(44100, 16, 1) // CD 音质采样率,16bit,单声道 }; _waveIn.DataAvailable += OnDataAvailable; _waveIn.RecordingStopped += OnRecordingStopped; _writer = new WaveFileWriter(filePath, _waveIn.WaveFormat); _waveIn.StartRecording(); } private void OnDataAvailable(object sender, WaveInEventArgs e) { // e.Buffer 是字节数组,e.BytesRecorded 是本次实际收到的字节数 _writer?.Write(e.Buffer, 0, e.BytesRecorded); } private void OnRecordingStopped(object sender, StoppedEventArgs e) { _writer?.Dispose(); _writer = null; _waveIn?.Dispose(); _waveIn = null; } void StopRecording() { _waveIn?.StopRecording(); }这段代码逻辑很简单,但值得拆开说明。WaveInEvent 每间隔 BufferMilliseconds 产生一次 DataAvailable,这里的 Buffer 不是“一块固定 2048 字节”的包,而是每次采集到的 PCM 字节,直接交给 WaveFileWriter 写入即可。采样率决定了数据回调的节奏:44100Hz、16bit、单声道时,每秒大约 88200 字节,那么 50ms 就是约 4410 字节。
关于采样率的选择,语音备注用 44100 或 48000 都行,但注意要和后续播放设备、转写服务的要求对齐。如果只是普通对话记录,22050Hz 也能听清,文件体积直接减半。录音格式如果要降噪处理,后期一般再统一转,不建议在采集端就牺牲太多信息量。
2.3 停止录音与释放设备:顺序写反就是设备占用
WaveInEvent 的 StopRecording 不是立即停止,它只是发一个停止请求,随后在后台线程触发 RecordingStopped 事件,事件触发时缓冲区里的最后一段数据可能还没写入文件。所以停止流程要做成异步的:StopRecording 之后不要立刻把 _writer 置空,而是在 OnRecordingStopped 里统一收尾。
常见错误是在 StopRecording 方法里直接写 _writer.Dispose(),结果 WAV 文件头长度字段没回写,播放器能打开文件但进度条不走,或者时长读出 0。WaveFileWriter 在 Dispose 时会回写 RIFF 头,必须等所有 DataAvailable 都结束后再释放。我的习惯是保持上面代码的结构:Stop 只负责请求停止,Dispose 统一在 RecordingStopped 里处理。
另外注意,StopRecording 后到 RecordingStopped 触发之间,DataAvailable 可能还会来一两次。这时 _waveIn 已经不再采集,但事件队列里还残留数据,所以写入前要判空。如果你在 Stop 之后立刻尝试修改 UI 上的状态,也会因为事件还在后台线程而出现意外。
提示:WaveInEvent.Repeated 参数通常不用改。它是兼容老式 waveIn 驱动的外部循环标志,桌面环境下默认 false 就是对的。别看到文档里出现“repeated”就去调,调错会导致录音数据重复写入。
3. 播放音频的三种姿势:SoundPlayer、MediaPlayer、NAudio 怎么选
3.1 SoundPlayer:零依赖但只认 WAV,适合提示音
System.Media.SoundPlayer 是 WPF 里最轻量的播放方案,不需要引入任何 NuGet 包,只支持 WAV 格式。它的定位是操作提示音、消息提醒这一类短音频,而不是长录音回放。
using System.Media; var player = new SoundPlayer("notice.wav"); player.Load(); // 同步加载,文件大时会卡 UI player.Play(); // 异步播放,不阻塞界面线程 // player.PlaySync(); // 阻塞到播完,适合做“确认音” // player.PlayLooping(); // 循环播放,慎用,容易忘记 stop这段代码里 Load 和 Play 的区别要讲清:Load 是同步加载,会把整个 WAV 读入内存;Play 启动后台播放,立即返回。如果拿 SoundPlayer 去播一段几分钟的录音,内存占用和加载卡顿都受不了。还要注意它没有内置音量控制接口,想调音量只能操作系统混音器,这对一个演示项目来说很不友好。所以我的边界判断是:文件小于 1MB,纯提示音,选 SoundPlayer;其余情况换方案。
3.2 MediaPlayer:让系统解码 MP3/AAC,别自己碰压缩格式
WPF 自带的 System.Windows.Media.MediaPlayer 是另一个“零额外依赖”方案,它底层走 Windows Media Foundation,可以解码 MP3、AAC、WMA 等压缩格式,不用自己处理压缩音频解码。播放前直接 Open 一个 Uri,比 SoundPlayer 能力范围宽很多。
var media = new System.Windows.Media.MediaPlayer(); media.Open(new Uri(@"D:\audio\sample.mp3", UriKind.Absolute)); media.Volume = 0.8; // 0 到 1,直接可调 media.MediaEnded += (s, e) => media.Close(); media.Play();MediaPlayer 的 Open 是异步准备,调用后不能立刻 Play,要先等 MediaOpened 或者直接让状态机自动进入 Buffering。它还有个特点:Play 之后声音从系统默认输出设备走,和 WPF 窗口本身无关。你没法拿到每一帧的音频数据去做音浪显示,也没有逐帧 seek 回读的能力,这些限制决定了它适合“把一条录音放给用户听”,不适合做音频分析或播放进度裁剪。
3.3 NAudio 播放:当你要控制进度、音量和数据,用它就对了
录音已经引用了 NAudio,播放端如果也用它,整个项目只需要维护一套音频抽象。NAudio 的播放思路是把音频文件解码成 PCM,然后送给 WaveOutEvent 这个输出设备。
using NAudio.Wave; var reader = new AudioFileReader(@"D:\audio\sample.mp3"); var output = new WaveOutEvent { DesiredLatency = 200, // 输出缓冲 200ms,音画同步要求高时调小 NumberOfBuffers = 3 // 缓冲块数,太小容易破音 }; output.Init(reader); output.Play(); // 停止和释放时,顺序不能反 output.Stop(); output.Dispose(); reader.Dispose();这里 AudioFileReader 会自动识别文件类型并解码成 PCM,WaveOutEvent 则负责把 PCM 送到声卡。注意 Init 之后不要立刻 Dispose reader,播放引擎还在异步读取它的数据。DesiredLatency 是输出延迟指标:设 50ms 能大幅降低“按键后出声”的延迟,但系统负载高时更容易出现爆音;设 200ms 更稳妥。做语音对讲类的项目我会从 100ms 开始试,做提示音则无所谓。
三种方案的取舍,我用一张表记录在项目文档里:
| 方案 | 支持格式 | 延迟控制 | 数据可读性 | 适合场景 |
|---|---|---|---|---|
| SoundPlayer | 仅 WAV | 基本不可控 | 无 | 短提示音、零依赖 |
| MediaPlayer | MP3/AAC/WMA 等 | 系统级 | 无 | 录音回放、背景音乐 |
| NAudio | 视 Reader 而定,基本全覆盖 | 可调缓冲 | 可逐帧处理 | 需要音浪、进度分析、低延迟 |
如果只是做录音回放,MediaPlayer 足够;但你已经为录音引入了 NAudio,播放端再用它,至少省掉“两种播放状态各自维护”的心智负担。我个人的分工是:系统提示音走 SoundPlayer,录音回放走 MediaPlayer,需要实时音浪或裁剪的音频走 NAudio。
4. 把录音塞进 WPF UI:时长显示、音浪与 MVVM 的正确姿势
4.1 DataAvailable 跑在后台线程:更新界面必须过 Dispatcher
WaveInEvent 的 DataAvailable 事件是在后台线程触发的,这一点新手翻车率极高。直接在事件里写 _textBlock.Text = ...,看起来偶尔能跑,但一旦数据量大、界面刷新频繁,就会随机抛“调用线程无法访问此对象”。
private void OnDataAvailable(object sender, WaveInEventArgs e) { // DataAvailable 在后台线程触发,不能直接碰控件 float rms = ComputeRms(e.Buffer, e.BytesRecorded); Application.Current.Dispatcher.BeginInvoke(new Action(() => { _volumeBar.Value = rms * 100; })); }这段代码把音频分析放在后台线程,只把“结果值”通过 Dispatcher 回 UI 线程。注意 BeginInvoke 是异步投递,高频调用时 UI 队列里会积累大量委托,导致界面卡顿。我在实际项目里会加一个节流:记录上一次刷新的时间,如果距上次超过 50ms 才再次投递。音浪条的刷新率 20 次/秒人眼已经觉得很流畅,没必要跟着每次采集回调走。
如果你的项目用 MVVM,我建议把 AudioRecorder 封装成一个服务,暴露 SampleLevel 和 StateChanged 事件,ViewModel 订阅后转换成普通属性。不要在事件里直接拿 View 的引用,否则后续换界面样式时,音频逻辑会和控件绑死。
4.2 时长显示别靠“数缓冲块”,用 DispatcherTimer 对齐录音状态
很多初学者用统计 DataAvailable 次数来估算录音时长,这是踩坑起点。DataAvailable 的触发频率受 BufferMilliseconds 影响,还受系统调度波动,累计次数乘上 50ms 得到的时长经常比真实时间慢或快。正确做法是单独用 Stopwatch 计时,再配合 DispatcherTimer 刷新文本。
private Stopwatch _stopwatch; private DispatcherTimer _timer; void StartTimer() { _stopwatch = Stopwatch.StartNew(); _timer = new DispatcherTimer(DispatcherPriority.Render) { Interval = TimeSpan.FromMilliseconds(200) }; _timer.Tick += (s, e) => { if (_recording) { _txtDuration.Text = _stopwatch.Elapsed.ToString(@"hh\:mm\:ss"); } }; _timer.Start(); }DispatcherTimer 虽然跑在 UI 线程,但它的 Tick 不会在文本框频繁重绘时产生剧烈竞争,因为 200ms 刷新一次足够满足显示需求。重点是把 _recording 这个标志位放在哪里:在录音服务里要有一个 IsRecording 属性,UI 的 Tick 里判断它,而不是用“Timer 是否启动”来推断录音状态,否则 Stop 之后还有最后一次 Tick 会把多余时间显示出来。
4.3 录音对象该放哪:字段、单例还是 ViewModel
录音对象不能放在函数局部变量里,这是所有问题的根源。WaveInEvent 是托管对象,局部函数执行完毕、没有引用后,GC 随时可能回收它,表现就是“录 3 秒后静音”或“完全没有回调”。我的方案是:把录音器提升到窗体类字段,或者封装成单例服务。前者适合单窗口工具,后者适合跨页面共享采集能力的项目。
如果做 MVVM,录音服务最好实现 IDisposable 并注册到容器里。但 Attention:WPF 的 Window 关闭不保证立即 Dispose,要在 Closing 事件里主动调用。否则会出现“程序关了,麦克风灯还亮着”的诡异现象。这个问题的排查成本不低,一开始就按生命周期管理来做能省很多事。
界面库对录音没有帮助。HandyControl、MaterialDesign 这类 UI 库能给你漂亮的按钮和进度条,但音频设备采集仍然要自己接,别指望靠控件库省掉音频层的代码。
5. 录音和播放的常见问题排查:5 个高频坑与修复顺序
5.1 录出来是 0 字节或文件头损坏
现象:录音结束后文件存在,但打开时长是 0,甚至播放器直接报文件损坏。 原因:WaveFileWriter 还没写完 RIFF 头就被中途释放,或者在 DataAvailable 还在写入时就把 _writer 置空并关闭了。WaveFileWriter 的头部长度字段是在 Dispose 时统一回写的,提前关闭等于写了一个“永远不知道长度”的残缺文件。 解决:把 writer.Dispose() 统一放在 RecordingStopped 事件里执行,不要在 Stop 按钮的点击方法里急着释放。StopRecording 只调用 Stop,收尾全部交给事件。
5.2 录音只有前几秒,后面全是静音
现象:前 3 秒有声音,后面波形平直,停止时还能正常生成文件。 原因:录音对象被 GC 回收。这是最隐蔽的一类问题,因为界面线程没有崩溃,设备可能彻底关闭或被系统静默挂起。 解决:把 WaveInEvent 实例挂在类字段或服务容器里,保证整个录音期间都存在强引用。排查时可以临时在 DataAvailable 里写一个日志,看回调是否在某条时间点后突然消失。
5.3 二次录音报“设备被占用”
现象:第一次录完一切正常,第二次点击录音瞬间抛异常或者没有任何回调。 原因:第一次停止后没有 Dispose,设备句柄还占着。WaveInEvent.StopRecording() 只停止采集,不释放设备。Windows 对独占采集设备的占用很敏感,不释放的下场就是其他进程也没法用。 解决:在 RecordingStopped 里执行 _waveIn?.Dispose() 并把引用置空。再录之前重新 new WaveInEvent,不要反复 stop / start 同一个实例。
5.4 播放时爆音、卡顿,尤其是边录边放
现象:用 NAudio 播放录音时出现周期性“咔哒”声,或者播放进度忽快忽慢。 原因:WaveOutEvent 的 DesiredLatency 设置过小,系统在 CPU 繁忙时没有及时填满输出缓冲,导致声卡缓冲下溢。录音和播放同时进行时,磁盘写入和采集回调会抢资源,问题更容易爆发。 解决:把 DesiredLatency 调回 200ms,NumberOfBuffers 给 3 或 4。同时确认录音采样率和播放端一致,48kHz 素材用 44.1kHz 设备播放不仅变调,还叠加爆音。
5.5 录到的声音里混进了电脑播放的声音
现象:戴着耳机说话,录音里却包含系统提示音或室友正在播放的视频声音。原因:音频设备选了“立体声混音”或“Wave Out Mix”,这类设备走的是声卡内部回环,专门采集扬声器输出。笔记本电脑尤其容易默认指向混音设备。 解决:回到 2.2 小节的设备枚举代码,把列表打出来,确认选中的是“Microphone Array”“External Mic”这类输入设备,而不是带“Mix”“Loopback”“Stereo Mix”字样的项。部分设备驱动会把混音设备放在列表最前面,肉眼确认比盲选靠谱。
6. 验证与进阶:用回放、静音检测和波形盯住采集链路
6.1 用回放比对法验证“录进去的内容和听感一致”
录音完成后,立刻用 MediaPlayer 做一次回放,人工确认高频噪声、音量起伏是否符合预期。这一步能快速暴露采样率错配、声道反转这类自动测试发现不了的问题。比对时注意:用耳机,不要用外放,否则麦克风会把回放声音再录回去,形成二次混合,你会误判成回声问题。
我还会额外做一个回环测试:把播放设备和录音设备的采样率都固定成 44100Hz,录一段 1kHz 正弦波,再播放出来,听音调是否准确。如果播放比原来低,说明录音链路的采样率被悄悄换了,多半是某个设备驱动强制用了 48kHz。
6.2 用 RMS 判断“录了但全是静音”的伪成功
文件能生成、时长也对,但内容全是静音,是另一个高频假象。手动听一个 10 分钟文件代价太大,我会直接在 DataAvailable 里算 RMS,再决定要不要在录音结束后提示用户。
// 计算 16bit 单声道 PCM 的 RMS 电平,返回 0 到 1 float ComputeRms(byte[] buffer, int bytesRecorded) { int sampleCount = bytesRecorded / 2; double sum = 0; for (int i = 0; i < bytesRecorded; i += 2) { short sample = BitConverter.ToInt16(buffer, i); sum += sample * (double)sample; } double rms = Math.Sqrt(sum / sampleCount); return (float)(rms / short.MaxValue); }这段代码每 16bit 取一个样本,累加平方后开根号再归一化。RMS 低于 0.01 基本就是静音,0.1 到 0.3 属于正常说话音量。如果单句 RMS 长期低于阈值,说明麦克风增益不足或设备选错。把 ComputeRms 和音浪条串联,录完还能顺手生成一张音量统计,判断整段录音是否存在大量低电平区域。
6.3 下一步玩法:把 WAV 直接喂给本地转写服务
录音文件已经是标准 WAV,后续可以交给本地部署的语音转写服务做会议纪要,或通过离线 ASR 转成文字后做全文检索。这一步不需要动录音代码,只要保证采集格式和转写服务要求的采样率一致,通常设置成 16000Hz 或 44100Hz 单声道。我现在的习惯是,接到 WPF 音频需求先问三件事:录多久、存成什么格式、播放要不要跟手,然后再写第一行代码。音频链路不复杂,但线程模型和资源释放的坑很密集,先定好生命周期,能省掉你后面一整天的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取