使用NAudio优化Wav与Raw PCM音频重采样及格式转换方案咨询
我需要在服务器程序中处理音频数据,具体需求如下:
- 接收客户端传入的带RIFF头的PCM Wave格式音频(48000Hz、16位、单/双声道),将其重采样为16000Hz、16位单声道的Raw PCM数据(无RIFF头),传输给外部服务
- 将外部服务传回的同类型Raw PCM数据,反向处理为48000Hz带RIFF头的Wave格式返回给客户端
目前我使用NAudio进行实时二进制数据处理(无需保存为文件),现有下采样转Raw PCM的代码:
var processingData = GetAudioData(); // This is the input audio data in Wav format using (Stream stream = new MemoryStream(processingData)) { using (var wavFileReader = new WaveFileReader(stream)) { var resampler = new WdlResamplingSampleProvider(wavFileReader.ToSampleProvider(), 16000); var monoSource = resampler.ToMono().ToWaveProvider16(); using (var outputStream = new MemoryStream()) { byte[] bytesOutput = new byte[monoSource.WaveFormat.AverageBytesPerSecond]; while (true) { int bytesRead = monoSource.Read(bytesOutput, 0, bytesOutput.Length); if (bytesRead == 0) break; outputStream.Write(bytesOutput, 0, bytesRead); } var outputData = outputStream.ToArray(); // This is raw PCM data } } }
我想知道有没有更高效的方式从WaveProvider获取Raw PCM,而非逐字节写入MemoryStream。曾尝试ACM重采样器(WaveStream可直接复制流),但它无法同时修改采样率和声道数,我需要此步骤尽可能快速以减少延迟。
另外,我的反向处理代码如下:
var processingData = GetRawData(); // This is the raw PCM data using (Stream stream = new MemoryStream(processingData)) { using (var rawStream = new RawSourceWaveStream(stream, new WaveFormat(16000, 16, 1))) using (var upsample = new WaveFormatConversionStream(new WaveFormat(48000, 16, 2), rawStream)) using (var outputStream = new MemoryStream()) { WaveFileWriter.WriteWavFileToStream(outputStream, upsample.ToSampleProvider().ToWaveProvider16()); var outputData = outputStream.ToArray(); } }
我使用WaveFileWriter.WriteWavFileToStream生成RIFF头并写入MemoryStream,但担心此方式的兼容性与效率。WaveFormatConversionStream基于ACM,可能在新服务器软硬件上存在问题,能否像下采样一样使用Wdl实现此步骤?
一、下采样:更高效获取Raw PCM数据
你目前的逐字节写入MemoryStream确实可以优化,其实可以直接计算输出数据的总长度,一次性分配数组,避免MemoryStream的多次扩容开销,同时减少循环中的内存操作:
var processingData = GetAudioData(); using (var stream = new MemoryStream(processingData)) using (var wavFileReader = new WaveFileReader(stream)) { var inputProvider = wavFileReader.ToSampleProvider(); var resampler = new WdlResamplingSampleProvider(inputProvider, 16000); var monoProvider = resampler.ToMono().ToWaveProvider16(); // 计算总采样数:输入总采样数 * (目标采样率/原采样率),再转字节数 long totalSamples = (long)(wavFileReader.SampleCount * (16000.0 / wavFileReader.WaveFormat.SampleRate)); int totalBytes = (int)(totalSamples * 2); // 16位单声道,每个采样2字节 byte[] outputData = new byte[totalBytes]; int bytesWritten = 0; while (bytesWritten < totalBytes) { int bytesRead = monoProvider.Read(outputData, bytesWritten, totalBytes - bytesWritten); if (bytesRead == 0) break; bytesWritten += bytesRead; } // outputData就是最终的Raw PCM数据 }
这个方法的优势在于:
- 一次性分配足够大的数组,避免MemoryStream内部的缓冲区扩容(这是原代码的主要性能开销点)
- 减少了一次从MemoryStream到byte[]的拷贝操作(原代码的
ToArray()会做一次全量拷贝) - 依然保持WDL重采样的低延迟优势,同时解决了ACM无法同时改采样率和声道的问题
如果你的音频数据是实时流式的(不是一次性获取全部),可以考虑直接在读取回调中处理输出,完全跳过内存缓存,进一步降低延迟。
二、上采样:用WDL替代ACM实现跨平台兼容
当然可以用WdlResamplingSampleProvider来实现上采样,完全避开ACM的兼容性问题,同时效率也更稳定。这里需要手动构建Wave文件头,而不是依赖WaveFileWriter.WriteWavFileToStream,这样更高效也更可控:
var processingData = GetRawData(); var inputFormat = new WaveFormat(16000, 16, 1); var targetFormat = new WaveFormat(48000, 16, 2); using (var inputStream = new MemoryStream(processingData)) using (var rawProvider = new RawSourceWaveStream(inputStream, inputFormat).ToSampleProvider()) { // 用WDL重采样到48000Hz,然后转双声道 var resampler = new WdlResamplingSampleProvider(rawProvider, 48000); var stereoProvider = resampler.ToStereo().ToWaveProvider16(); // 计算Wave文件总大小:头大小(44) + PCM数据大小 long pcmDataLength = (long)(processingData.Length * (48000.0 / 16000) * 2); // 采样率*2,声道数*2 long totalWaveSize = 44 + pcmDataLength; byte[] outputData = new byte[totalWaveSize]; // 手动写入RIFF头 using (var writer = new BinaryWriter(new MemoryStream(outputData))) { // RIFF chunk writer.Write(System.Text.Encoding.ASCII.GetBytes("RIFF")); writer.Write((int)(totalWaveSize - 8)); writer.Write(System.Text.Encoding.ASCII.GetBytes("WAVE")); // fmt chunk writer.Write(System.Text.Encoding.ASCII.GetBytes("fmt ")); writer.Write(16); // PCM格式的大小 writer.Write((short)1); // PCM编码 writer.Write((short)targetFormat.Channels); writer.Write(targetFormat.SampleRate); writer.Write(targetFormat.AverageBytesPerSecond); writer.Write((short)targetFormat.BlockAlign); writer.Write((short)targetFormat.BitsPerSample); // data chunk writer.Write(System.Text.Encoding.ASCII.GetBytes("data")); writer.Write((int)pcmDataLength); } // 写入PCM数据到数组的头之后的位置 int bytesWritten = 44; while (bytesWritten < totalWaveSize) { int bytesRead = stereoProvider.Read(outputData, bytesWritten, (int)(totalWaveSize - bytesWritten)); if (bytesRead == 0) break; bytesWritten += bytesRead; } // outputData就是带RIFF头的48000Hz双声道Wave数据 }
这里的优势:
- 完全脱离ACM依赖,在Windows、Linux等平台都能稳定运行(NAudio的WDL实现是跨平台的)
- 手动构建Wave头比
WaveFileWriter更高效,避免了额外的流操作和拷贝 - 同样用一次性数组分配,减少内存开销
如果需要兼容单声道的输出(比如原Raw PCM可能需要转单声道Wave),只需要调整targetFormat的声道数,以及计算pcmDataLength时的声道数系数即可。
额外优化建议
- 对于实时处理场景,建议复用
WdlResamplingSampleProvider实例,而不是每次创建新的,这样可以避免初始化开销 - 如果你的服务器是多线程环境,注意WDL重采样器不是线程安全的,每个线程需要独立的实例
- 可以根据音频数据的实际大小调整读取缓冲区的大小(比如用4096或8192字节的缓冲区),平衡内存占用和读取效率
内容的提问来源于stack exchange,提问作者Spincel

