十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

WPF录音与播放实战:用NAudio轻松实现音频采集与播放

WPF录音与播放实战:用NAudio轻松实现音频采集与播放 简介面向需要在Windows桌面应用中集成录音、播放以及简单音频分析功能的开发者这份示例工程完整展示了基于.NET Framework 4.5和Visual Studio 2017的WPF实现方案。工程借助NAudio库中的WasapiLoopbackCapture进行声卡数据捕获再通过MediaPlayer完成音频的加载、播放、暂停、继续与停止并将这些操作封装为界面按钮事件让读者看清从底层采集到UI交互的完整链路。资源共175个文件主要包含45个C#源码、XAML界面布局、编译生成的程序集与界面资源以及用于测试的wav音频和必要配置文件压缩包整体仅3.18MB结构完整、目录清晰适合直接打开学习或作为项目模板快速改造。目前已有926人学习下载。通过该工程读者可以掌握录音数据写入、播放器控制、本地音频文件时长与比特率读取等关键代码写法同时了解NAudio与WPF整合时的常见处理方式节省从零搭建和反复调试的时间对开发聊天软件、教育工具、语音笔记等场景有直接参考价值。 做WPF上位机或者工具类应用的时候录音和播放音频是特别容易撞上的需求。现场采集一段语音、做语音对讲前的试听、给测试程序加个声音提示甚至是把麦克风输入存成样本数据核心要解决的问题就两个怎么录怎么放。WPF本身并没有提供一个开箱即用的录音控件但把NAudio这个库引进来之后录音、播放、暂停、进度控制都能在C#里直接搞定十几分钟就能跑通一条完整流程。这篇总结适合已经会写基本WPF页面、但没碰过音频采集的开发者也适合想快速给现有项目加语音模块的同行参考。1. 方案选型先弄明白WPF里能做音频的几套路子1.1 录音不是WPF的强项选对库是关键很多人一开始会去找WPF自带的MediaElement或者MediaPlayer但这两个类主要负责播放录音能力很弱。WPF的定位是界面框架底层没有直接暴露麦克风采集API硬要用也不是不行得绕到WinRT的Windows.Media.Capture但那种做法既要处理异步API还要面对桌面应用和WinRT组件之间的类型转换问题写起来并不顺手。实际项目里我更推荐直接用NAudio。这是一个开源跨平台的音频处理库封装了Windows底层waveIn、waveOut、DirectSound、WASAPI等一系列音频API核心功能覆盖录音、播放、格式转换、音频处理、混音等。对WPF开发者来说NAudio最大的好处是API设计非常直白录音就是拉数据播放就是喂数据中间没有太多概念门槛。而且它支持.NET Framework和.NET 6/8老项目新项目都能用。市面上还有个常见选择是Windows.Media.Playback和MediaPlayer但这个组合主要是面向UWP以及WinUI的在纯WPF里使用多多少少会遇到兼容性摩擦。如果是做轻量级工具不想引入第三方依赖也可以只用系统自带的SoundPlayer不过SoundPlayer只支持WAV格式播放MP3之类的压缩格式会很尴尬。所以在项目里我一般优先用NAudio它既能做录音又能做播放一套API走完避免同时维护两套音频处理逻辑。1.2 环境准备新建项目并引入NAudio先说环境Visual Studio 2022创建WPF应用程序目标框架选.NET 8或.NET Framework 4.7.2都可以。NAudio 2.2.1针对这两种框架都有兼容包直接NuGet安装就行。在包管理器控制台里执行Install-Package NAudio或者在“管理NuGet程序包”界面搜索NAudio选择最新稳定版安装。装完之后你会看到依赖里多了NAudio.Core、NAudio.WinForms等子包没关系那是NAudio模块化之后的结果我们直接用命名空间NAudio.Wave和NAudio.Wave.SampleProviders就够了。这里提醒一句如果项目最终要部署到32位系统或者要兼容某些老声卡驱动建议把目标平台显式设置成x86或x64不要让“Any CPU”乱选。因为NAudio底层会加载本机音频模块位数不一致时偶尔会出现莫名其妙的初始化失败。我在一个工控机上就踩过这个坑IntPtr指针在64位进程里访问32位驱动返回的句柄直接抛异常后来统一改成x64才稳定。2. 录音把麦克风声音落到WAV文件2.1 录音链路上的两个核心类NAudio里做录音最核心的配合是WaveInEvent和WaveFileWriter。WaveInEvent负责从声卡采集PCM音频数据它按照设定的格式持续产生数据块每次采集完一批数据就会触发DataAvailable事件。这个事件发生在后台线程所以绝对不能在事件里直接操作WPF控件。WaveFileWriter则负责把收到的原始音频数据按WAV容器格式写入文件WAV文件本质上就是在PCM数据前面加一个44字节的头这个库已经帮我封装好了。整个录音过程可以理解成一根水管声卡裸数据往WaveInEvent里流DataAvailable事件是水龙头WaveFileWriter是接水的桶。只要开着水龙头数据就不断装进桶里关掉水龙头调用StopRecording桶里就是完整的录音文件。用这两个类组合还有一个隐藏好处录制过程中如果程序崩溃已经写进文件里的数据仍然是一个合法WAV的骨架只是没有文件头可以用其他工具修复不至于全部丢失。2.2 一个能直接用的录音封装我先贴一段实际项目里常用的录音类放到WPF的普通类库里就能跑。using NAudio.Wave; public class AudioRecorder : IDisposable { private WaveInEvent _capture; private WaveFileWriter _writer; public AudioRecorder(string filePath) { _capture new WaveInEvent { WaveFormat new WaveFormat(44100, 16, 1), BufferMilliseconds 50 }; _writer new WaveFileWriter(filePath, _capture.WaveFormat); _capture.DataAvailable OnDataAvailable; _capture.RecordingStopped OnRecordingStopped; } private void OnDataAvailable(object sender, WaveInEventArgs e) { // e.Buffer里就是麦克风采集到的原始PCM数据 _writer.Write(e.Buffer, 0, e.BytesRecorded); } private void OnRecordingStopped(object sender, StoppedEventArgs e) { _writer.Dispose(); _writer null; _capture.Dispose(); _capture null; } public void Start() { _capture.StartRecording(); } public void Stop() { if (_capture ! null _capture.CaptureState CaptureState.Capturing) { _capture.StopRecording(); } } public void Dispose() { Stop(); _capture?.Dispose(); _writer?.Dispose(); } }这里有个细节值得注意WaveFormat(44100, 16, 1)的三个参数分别是采样率、位深、声道数在绝大多数Windows声卡上都能正常工作。BufferMilliseconds 50表示每个音频缓冲区的大小是50毫秒数值越小录音延迟越低但CPU占用会升高系统负载高时还容易爆音。50毫秒对语音录音来说是兼顾延迟和稳定性的起步值。RecordingStopped事件里释放资源是比较稳妥的做法因为StopRecording()是异步的真正停止并完成所有事件回调需要一点时间。如果在调用StopRecording后立刻释放_writer有概率出现数据还没写完就被释放的“半截文件”。2.3 采样参数选择和文件大小估算录音参数不是随便拍的要结合应用场景来定。语音通话、语音指令采样率8000Hz或16000Hz就够位深16bit单声道。普通语音记录、会议录音44100Hz16bit单声道或双声道音质接近CD文件体积中等。需要做后期处理、频谱分析的音频建议44100Hz或48000Hz16bit以上双声道。文件大小可以直接算公式很简单文件大小(字节) 采样率 × (位深 / 8) × 声道数 × 录音秒数比如用44100Hz、16bit、单声道录一分钟44100 × 2 × 1 × 60 5292000字节 ≈ 5.29MB如果换成双声道则翻倍到10.6MB。这也是为什么短语音用WAV问题不大但长时间录音最好另存成MP3或AACNAudio里可以配合MediaFoundationEncoder或者引入LAME编码器做MP3压缩。核心流程还是先采集成PCM再转码不复杂但需要单独写一段转换逻辑。2.4 录制时的界面状态更新因为DataAvailable事件在后台线程触发若要更新UI上的录音时长、电平指示等状态必须借助Dispatcher跳回UI线程。直接写控件会抛“调用线程无法访问此对象”的异常。我习惯做一个定时刷新不要在每次DataAvailable都调用Dispatcher。50毫秒一个缓冲相当于每秒20次跨线程切换太浪费。可以用System.Windows.Threading.DispatcherTimer每500毫秒拉一次录音时长更新界面。private void OnTimerTick(object? sender, EventArgs e) { if (_recorder ! null _captureState) { RecordingTimeText.Text DateTime.Now.Subtract(_startTime).ToString(hh\:mm\:ss); } }这样做的好处是界面刷新频率稳定CPU占用很低同时也能避免频繁Dispatcher调用导致的卡顿。3. 播放把录好的音频再放出来3.1 用WaveOutEvent播放WAV/MP3录音存成WAV之后播放就简单多了。我推荐用WaveOutEvent配合AudioFileReader。AudioFileReader是NAudio里一个很实用的读取器既能读WAV也能读MP3还会自动完成格式转换。WaveOutEvent负责把解码后的PCM数据送到声卡输出。它们两个配合基本覆盖了日常所有本地音频播放需求。using NAudio.Wave; public class AudioPlayer : IDisposable { private AudioFileReader _reader; private WaveOutEvent _output; public void Play(string filePath) { Stop(); _reader new AudioFileReader(filePath); _output new WaveOutEvent { DesiredLatency 200 }; _output.Init(_reader); _output.Play(); } public void Stop() { _output?.Stop(); _output?.Dispose(); _reader?.Dispose(); _output null; _reader null; } public void Dispose() { Stop(); } }这里的关键是_output和_reader必须保存为类字段不能声明成局部变量。因为WaveOutEvent底层依赖声卡的播放线程一旦方法结束后对象被垃圾回收音频马上会静默甚至程序直接崩溃。我见过不少新手写完后点击播放没声音十有八九就是因为这个生命周期问题。3.2 暂停、继续和播放进度WaveOutEvent提供了Pause()和Play()可以实现暂停和继续。暂停时播放位置会保留在AudioFileReader.CurrentTime里继续时从当前位置继续。进度条显示可以这样做public TimeSpan CurrentTime _reader?.CurrentTime ?? TimeSpan.Zero; public TimeSpan TotalTime _reader?.TotalTime ?? TimeSpan.Zero; public float Progress (float)(_reader.CurrentTime.TotalSeconds / _reader.TotalTime.TotalSeconds);界面上的ProgressBar用一个DispatcherTimer定期读取这两个属性即可。停止播放时把进度归零。有几个播放状态需要单独标记_output.PlaybackState会返回Stopped、Playing、Paused可以用它判断当前是否在播放中避免按钮状态混乱。另外WaveOutEvent.PlaybackStopped事件在停止、播放结束、设备拔出等情况下都会触发不要在这个事件里做太重的清理容易和主动调用Stop产生重复释放。3.3 把录音和播放接到按钮上简单写个界面交互几个按钮就够了。XAML里放三个按钮开始录音、停止录音、播放录音。Button Content开始录音 ClickOnStartRecording / Button Content停止录音 ClickOnStopRecording / Button Content播放录音 ClickOnPlayRecording / TextBlock x:NameStateText /后端逻辑private AudioRecorder _recorder; private AudioPlayer _player; private readonly string _tempFile record.wav; private void OnStartRecording(object sender, RoutedEventArgs e) { _recorder new AudioRecorder(_tempFile); _recorder.Start(); StateText.Text 录音中; } private void OnStopRecording(object sender, RoutedEventArgs e) { _recorder.Stop(); StateText.Text 录音完成; } private void OnPlayRecording(object sender, RoutedEventArgs e) { _player new AudioPlayer(); _player.Play(_tempFile); }这套交互虽然简单但已经能覆盖最基础的“录完放一下听效果”场景。实际产品里建议把录音状态和播放状态拆成枚举配合MVVM的绑定来驱动按钮可用性避免出现录着音又去点播放这种并发操作。4. 常见问题与排查记录4.1 录音相关的高频问题症状是点击开始录音后界面不报错但文件里一点声音都没有或者播放出来是刺耳的杂音。先检查设备选择。默认WaveInEvent用的是系统默认麦克风。如果电脑同时插了USB麦克风、耳机麦克风和内置麦克风默认设备很可能不是你想要的。可以用WaveInEvent.DeviceCount枚举所有录音设备把设备名列到下拉框里for (int i 0; i WaveInEvent.DeviceCount; i) { var caps WaveInEvent.GetCapabilities(i); Debug.WriteLine(${i}: {caps.ProductName}); }然后创建WaveInEvent时指定DeviceNumber属性。还有一种是系统设置里麦克风隐私权限被关了。Windows 10/11默认对桌面应用访问麦克风有隐私开关位置在“设置-隐私-麦克风”。如果程序跑起来后系统提示麦克风被禁用去这里把权限打开。4.2 播放相关的高频问题播放最常见的是点击播放后无声但录音文件用其他播放器打开又是正常的。先判断_reader是否还在代码里是不是把读取器声明成了局部变量导致播放前就被回收了。然后看声卡输出设备有没有选对多声卡环境下WaveOutEvent默认输出到系统默认播放设备如果默认设备是个虚拟声卡真实音箱不出声也正常。还有一种情况是录出来的WAV采样率太高老声卡不支持直接播放。比如用48000Hz录音有些旧声卡只支持44100Hz播放时就会有“变调”或者没声音。解决方法是播放前让AudioFileReader自动转换采样率或者录音时直接用44100Hz兼容性最好。4.3 录音播放同时用时的资源冲突WPF应用里有时候需要边录音边播放提示音比如对讲系统的“滴”声。这时要特别注意声卡的独占模式。WaveInEvent和WaveOutEvent默认走共享模式多数声卡支持录音和播放同时进行。但如果程序里有人设置了底层的独占WASAPI或者其他软件如某些语音聊天工具抢占了设备你这边再录音或播放就会失败报DeviceInUse类型的异常。遇到这种问题排查顺序是先确认没有其他软件占用麦克风再检查代码里是否同时创建了多个WaveInEvent实例如果有先停掉旧的再启动新的最后考虑用WasapiCapture替代WaveInEvent获得更好的兼容性。WasapiCapture是WASAPI回调延迟更低但代码逻辑略有差异。5. 升级一点把录音播放做成MVVM服务5.1 定义服务接口方便后续替换项目如果用了MVVM框架比如Prism或者CommunityToolkit.Mvvm建议把录音播放封装成服务接口而不是在ViewModel里直接new一个具体类。这样以后换底层实现、加音频处理逻辑都方便。public interface IAudioRecordingService { void StartRecording(string filePath); void StopRecording(); event EventHandlerTimeSpan? RecordingTimeChanged; } public interface IAudioPlaybackService { void Play(string filePath); void Pause(); void Stop(); event EventHandlerbool? PlayStateChanged; }这样ViewModel只依赖两个接口具体是NAudio实现还是系统API实现都跟界面层无关。5.2 在ViewModel里调用录音播放ViewModel里使用服务后按钮命令变得非常干净。public class VoiceViewModel { private readonly IAudioRecordingService _recordingService; private readonly IAudioPlaybackService _playbackService; public VoiceViewModel(IAudioRecordingService recordingService, IAudioPlaybackService playbackService) { _recordingService recordingService; _playbackService playbackService; } public ICommand StartRecordCommand new RelayCommand(() _recordingService.StartRecording(voice.wav)); public ICommand StopRecordCommand new RelayCommand(_recordingService.StopRecording); public ICommand PlayCommand new RelayCommand(() _playbackService.Play(voice.wav)); }录音服务在实现时把DataAvailable里产生的事件时间抛出来ViewModel订阅后更新界面。这样主界面和音频逻辑彻底解耦后续要做音频波形可视化、音量仪表直接在服务里加事件就行不会污染视图层。实际项目里我建议把_tempFile路径放到配置里并且启动时检测磁盘空间因为长时间录音会在几分钟内产生几十甚至上百MB的WAV文件如果没做控制用户容易录到一半把C盘塞满。这个坑在我第一次做语音采集功能时遇到过后来一直记着录音不是“按下就完事”内存、磁盘、线程、设备状态都要盯着。NAudio这套方案在Windows平台上已经足够成熟录音、播放、暂停、进度控制都能覆盖。如果你后面要做更复杂的实时音频处理比如降噪、回声消除、频谱显示也可以在这条技术路线上继续延伸。希望这次分享能帮正在做WPF录音播放功能的朋友少走几步弯路。本文还有配套的精品资源点击获取
返回列表