第一次接触NAudio时,我对着官方文档折腾了半天才把环境配好。现在回想起来,其实用NuGet安装就三秒钟的事。打开Visual Studio,在解决方案资源管理器里右键项目,选择"管理NuGet程序包",搜索框输入"NAudio"直接安装最新稳定版。安装完成后,你会发现在工具箱里自动多了个NAudio.WinForms分组,里面都是可以直接拖拽使用的音频控件。
这里有个新手容易踩的坑:NAudio对.NET版本有要求。我去年在.NET Framework 4.5的项目里装最新版NAudio就报错了,后来发现要装1.10.0这个特定版本才兼容。建议用.NET Core 3.1或以上版本开发,能避免很多奇怪的兼容性问题。安装完成后,记得在代码文件顶部添加using语句:
csharp复制using NAudio.Wave;
using NAudio.FileFormats;
用WaveIn类实现麦克风录音比想象中简单。我在项目里封装了个Recorder类,核心代码就二十来行。初始化时需要设置采样率(44100Hz是CD音质标准)和声道数,然后挂载DataAvailable事件:
csharp复制public class AudioRecorder
{
private WaveInEvent waveSource;
private WaveFileWriter waveFile;
public void StartRecording(string outputPath)
{
waveSource = new WaveInEvent {
WaveFormat = new WaveFormat(44100, 1)
};
waveSource.DataAvailable += (s, e) => {
waveFile.Write(e.Buffer, 0, e.BytesRecorded);
};
waveFile = new WaveFileWriter(outputPath, waveSource.WaveFormat);
waveSource.StartRecording();
}
}
实际测试发现,笔记本内置麦克风的录音质量很一般。后来我加了个设备选择功能,通过WaveIn.GetCapabilities获取所有输入设备,让用户自己选外接麦克风:
csharp复制for (int i = 0; i < WaveIn.DeviceCount; i++)
{
var caps = WaveIn.GetCapabilities(i);
deviceComboBox.Items.Add($"{i}. {caps.ProductName}");
}
录制电脑播放的声音要用WasapiLoopbackCapture类。这个功能我调试时遇到个坑:如果系统音量开得太小,录出来的文件几乎是静音的。解决方法是在初始化时设置AudioClientShareMode为Shared:
csharp复制var loopback = new WasapiLoopbackCapture {
ShareMode = AudioClientShareMode.Shared
};
还有个实用技巧是实时显示音量。在DataAvailable事件里计算RMS值就能得到当前音量:
csharp复制float max = 0;
for (int i = 0; i < e.BytesRecorded; i += 2)
{
short sample = BitConverter.ToInt16(e.Buffer, i);
float sample32 = sample / 32768f;
max = Math.Max(max, Math.Abs(sample32));
}
volumeProgress.Value = (int)(max * 100);
做K歌功能时需要混合麦克风和人声伴奏,MixingSampleProvider是首选方案。但要注意所有输入源的采样率必须一致,否则会报错。我的做法是用SampleRateConverter统一转成44100Hz:
csharp复制var micProvider = new WaveInEvent { WaveFormat = new WaveFormat(44100, 1) };
var musicProvider = new AudioFileReader("bgm.mp3");
// 统一采样率
var convertedProvider = new SampleRateConverter(
musicProvider.ToSampleProvider(),
micProvider.WaveFormat.SampleRate);
var mixer = new MixingSampleProvider(new[] {
micProvider.ToSampleProvider(),
convertedProvider
});
用WaveOut开发播放器时,我发现直接调用Play()会有明显延迟。后来改用WaveOutEvent就好了,它内部用了事件驱动模型。完整播放控制应该包含这些功能:
csharp复制public class AudioPlayer
{
private WaveOutEvent outputDevice;
private AudioFileReader audioFile;
public void Play(string filePath)
{
Stop(); // 先停止当前播放
audioFile = new AudioFileReader(filePath);
outputDevice = new WaveOutEvent();
outputDevice.Init(audioFile);
outputDevice.Play();
}
public void Pause() => outputDevice?.Pause();
public void Resume() => outputDevice?.Play();
public void Stop()
{
outputDevice?.Stop();
audioFile?.Dispose();
}
}
进度条控制是个技术点。我用了Timer定时获取当前播放位置,换算成百分比显示。反向拖动进度条跳转时要注意线程安全:
csharp复制timer.Elapsed += (s, e) => {
this.Invoke((MethodInvoker)delegate {
if(audioFile != null) {
progressBar.Value = (int)(100 * audioFile.CurrentTime.TotalMilliseconds / audioFile.TotalTime.TotalMilliseconds);
}
});
};
用NAudio实现变调不变速效果,需要用到PitchShiftProvider。我封装了个变声器类,支持男女声切换:
csharp复制public class VoiceChanger
{
private SmbPitchShiftingSampleProvider pitchShifter;
public ISampleProvider ApplyEffect(ISampleProvider source)
{
pitchShifter = new SmbPitchShiftingSampleProvider(source);
return pitchShifter;
}
public void SetPitch(float factor) // 0.5-2.0
{
if(pitchShifter != null)
pitchShifter.PitchFactor = factor;
}
}
实测发现PitchFactor在0.8-1.2之间效果最自然。超过这个范围会有明显机械音,需要配合EQ均衡器调整:
csharp复制var equalizer = new Equalizer(audioSource, new[] {
new EqualizerBand { Frequency=100, Gain=2, Bandwidth=0.8f },
new EqualizerBand { Frequency=1000, Gain=-1, Bandwidth=0.6f }
});
用BiQuadFilter可以实现简单的降噪。这个滤波器我调参调了很久,最终效果最好的配置是:
csharp复制var lowPass = BiQuadFilter.LowPassFilter(44100, 4000, 0.5f);
var highPass = BiQuadFilter.HighPassFilter(44100, 80, 0.5f);
sampleProvider = sampleProvider
.Filter(lowPass)
.Filter(highPass);
对于录音常见的爆音问题,可以加个Limiter限制最大振幅:
csharp复制var limiter = new SimpleCompressorStream(audioStream) {
Threshold = 0.9f,
Ratio = 20,
Attack = 5,
Release = 100
};
经过三个版本的迭代,我的音频工具最终采用MVVM模式设计。核心模块包括:
视图层用Binding实现控件联动,比如这个录音按钮的状态绑定:
csharp复制recordButton.DataBindings.Add("Enabled", viewModel, nameof(viewModel.CanRecord));
音频开发中最头疼的就是各种异常。我总结了几类常见问题及解决方案:
csharp复制if(WaveOut.DeviceCount == 0)
throw new Exception("没有可用音频设备");
csharp复制try {
using(var reader = new AudioFileReader(path)) { ... }
} catch(NAudio.MmException ex) {
// 提示用户文件损坏
}
csharp复制protected override void Dispose(bool disposing)
{
waveOut?.Dispose();
audioFile?.Dispose();
base.Dispose(disposing);
}
处理大音频文件时容易内存溢出。我的解决方案是使用BufferedWaveProvider:
csharp复制var buffer = new BufferedWaveProvider(waveFormat) {
BufferDuration = TimeSpan.FromSeconds(30),
DiscardOnBufferOverflow = true
};
对于长时间录音,建议分块保存文件。我实现了每5分钟自动分割:
csharp复制DateTime lastSplit = DateTime.Now;
waveIn.DataAvailable += (s, e) => {
writer.Write(e.Buffer, 0, e.BytesRecorded);
if((DateTime.Now - lastSplit).TotalMinutes >= 5) {
writer.Close();
writer = new WaveFileWriter(GetNewFileName(), waveIn.WaveFormat);
lastSplit = DateTime.Now;
}
};
UI线程和音频线程必须分离。我常用BackgroundWorker处理耗时操作:
csharp复制var worker = new BackgroundWorker();
worker.DoWork += (s, e) => {
using(var converter = new WaveFormatConversionStream(targetFormat, sourceStream))
{
WaveFileWriter.CreateWaveFile(outputPath, converter);
}
};
worker.RunWorkerCompleted += (s, e) => {
if(e.Error != null) ShowError(e.Error);
else conversionProgress.Hide();
};
worker.RunWorkerAsync();
实时音频处理要注意缓冲区大小。经过测试,1024字节的缓冲区在延迟和CPU占用间取得较好平衡:
csharp复制waveIn.BufferMilliseconds = 50; // 约1024字节
虽然NAudio主要面向Windows,但通过.NET Standard也能实现部分跨平台功能。我在Mac上测试时发现WaveOut不可用,改用BassAudio就解决了:
csharp复制#if WINDOWS
IWavePlayer player = new WaveOutEvent();
#else
IWavePlayer player = new BassAudioOutput();
#endif
音频格式兼容性也是个坑。Windows默认不支持MP3编码,需要单独安装MediaFoundation:
csharp复制if(!MediaFoundationEncoder.IsSupported(MediaFoundationEncoder.MP3))
throw new Exception("系统缺少MP3编码器");
最后分享个实用技巧:用NAudio.Lame这个NuGet包可以完美解决MP3编码问题。它封装了LAME编码器,使用起来特别简单:
csharp复制using(var reader = new AudioFileReader("input.wav"))
using(var writer = new LameMP3FileWriter("output.mp3", reader.WaveFormat, 128))
{
reader.CopyTo(writer);
}