前言
之前实现了《C++ 将音频PCM数据封装成wav文件》,最近将其改成了C#版本。使用C#实现录音功能时还是需要写wav文件的,直接用C#实现也是比较简单的,这样可以免去不必要的依赖。
一、如何实现?
首先需要构造wav头部,wav文件音频信息全部保存在头部,我们要做的就是在PCM数据的前面加入wav头,并且记录PCM的相关参数。
1.定义头结构
只定义PCM格式的wav文件头,包含3部分riff、format、data。需要使用[StructLayout(LayoutKind.Sequential)]描述结构体,确保内存连续。
WAV头部
//WAV头部结构-PCM格式
[StructLayout(LayoutKind.Sequential)]
struct WavPCMFileHeader
{
RIFF riff=new RIFF();
Format format = new Format();
Data data = new Data();
public WavPCMFileHeader() { }
}
RTTF部分
[StructLayout(LayoutKind.Sequential)]
struct RIFF
{
byte r = (byte)'R';
byte i = (byte)'I';
byte f = (byte)'F';
byte t = (byte)'F';
public uint fileLength = 0;
byte w = (byte)'W';
byte a = (byte)'A';
byte v = (byte)'V';
byte e = (byte)'E';
public RIFF() { }
}
Format部分
[StructLayout(LayoutKind.Sequential)]
struct Format
{
byte f = (byte)'f';
byte m = (byte)'m';
byte t = (byte)'t';
byte s = (byte)' ';
public uint blockSize = 16;
public ushort formatTag=0;
public ushort channels = 0;
public uint samplesPerSec = 0;
public uint avgBytesPerSec = 0;
public ushort blockAlign = 0;
public ushort bitsPerSample = 0;
public Format() { }
}
Data部分
[StructLayout(LayoutKind.Sequential)]
struct Data
{
byte d = (byte)'d';
byte a = (byte)'a';
byte t = (byte)'t';
byte a2 = (byte)'a';
public uint dataLength=0;
public Data() { }
}
2.预留头部空间
创建文件时预留头部空间
_stream = File.Open(fileName, FileMode.Create);
_stream!.Seek(Marshal.SizeOf<WavPCMFileHeader>(), SeekOrigin.Begin);
3.写入PCM数据
写入数据
_stream!.Write(data);
4.写入头部信息
关闭文件时,回到起始位置写入头部信息
//写入头部信息
_stream!.Seek(0, SeekOrigin.Begin);
WavPCMFileHeader h = new WavPCMFileHeader(_channels, _sampleRate, _bitsPerSample, (uint)(_stream.Length - Marshal.SizeOf<WavPCMFileHeader>()));
_stream!.Write(StructToBytes(h));
_stream!.Close();
_stream = null;
二、完整代码
.net6.0
WavWriter.cs
using System.Runtime.InteropServices;
/************************************************************************
* @Project: AC::WavWriter
* @Decription: wav文件写入工具
* @Verision: v1.0.0.0
* @Author: Xin Nie
* @Create: 2023/10/8 09:27:00
* @LastUpdate: 2023/10/8 18:28:00
************************************************************************
* Copyright @ 2025. All rights reserved.
************************************************************************/
namespace AC
{
/// <summary>
/// wav写入工具,目前只支持pcm格式
/// </summary>
public class WavWriter:IDisposable
{
ushort _channels;
uint _sampleRate;
ushort _bitsPerSample;
FileStream? _stream;
/// <summary>
/// 创建对象
/// </summary>
/// <param name="fileName">文件名</param>
/// <param name="channels">声道数</param>
/// <param name="sampleRate">采样率,单位hz</param>
/// <param name="bitsPerSample">位深</param>
public static WavWriter Create(string fileName, ushort channels, uint sampleRate, ushort bitsPerSample)
{
return new WavWriter(fileName, channels, sampleRate, bitsPerSample);
}
/// <summary>
/// 构造方法
/// </summary>
/// <param name="fileName">文件名</param>
/// <param name="channels">声道数</param>
/// <param name="sampleRate">采样率,单位hz</param>
/// <param name="bitsPerSample">位深</param>
WavWriter(string fileName, ushort channels, uint sampleRate, ushort bitsPerSample)
{
_stream = File.Open(fileName, FileMode.Create);
_channels = channels;
_sampleRate = sampleRate;
_bitsPerSample = bitsPerSample;
_stream!.Seek(Marshal.SizeOf<WavPCMFileHeader>(), SeekOrigin.Begin);
}
/// <summary>
/// 写入PCM数据
/// </summary>
/// <param name="data">PCM数据</param>
public void Write(byte[] data)
{
_stream!.Write(data);
}
/// <summary>
/// 写入PCM数据
/// </summary>
/// <param name="stream">PCM数据</param>
public void Write(Stream stream)
{
stream.CopyTo(_stream!);
}
/// <summary>
/// 关闭文件
/// </summary>
public void Close()
{
//写入头部信息
_stream!.Seek(0, SeekOrigin.Begin);
WavPCMFileHeader h = new WavPCMFileHeader(_channels, _sampleRate, _bitsPerSample, (uint)(_stream.Length - Marshal.SizeOf<WavPCMFileHeader>()));
_stream!.Write(StructToBytes(h));
_stream!.Close();
_stream = null;
}
public void Dispose()
{
Close();
}
static byte[] StructToBytes<T>(T obj)
{
int size = Marshal.SizeOf(typeof(T));
IntPtr bufferPtr = Marshal.AllocHGlobal(size);
try
{
Marshal.StructureToPtr(obj!, bufferPtr, false);
byte[] bytes = new byte[size];
Marshal.Copy(bufferPtr, bytes, 0, size);
return bytes;
}
catch (Exception ex)
{
throw new Exception("Error in StructToBytes ! " + ex.Message);
}
finally
{
Marshal.FreeHGlobal(bufferPtr);
}
}
}
//WAV头部结构-PCM格式
[StructLayout(LayoutKind.Sequential)]
struct WavPCMFileHeader
{
[StructLayout(LayoutKind.Sequential)]
struct RIFF
{
byte r = (byte)'R';
byte i = (byte)'I';
byte f = (byte)'F';
byte t = (byte)'F';
public uint fileLength = 0;
byte w = (byte)'W';
byte a = (byte)'A';
byte v = (byte)'V';
byte e = (byte)'E';
public RIFF() { }
}
[StructLayout(LayoutKind.Sequential)]
struct Format
{
byte f = (byte)'f';
byte m = (byte)'m';
byte t = (byte)'t';
byte s = (byte)' ';
public uint blockSize = 16;
public ushort formatTag=0;
public ushort channels = 0;
public uint samplesPerSec = 0;
public uint avgBytesPerSec = 0;
public ushort blockAlign = 0;
public ushort bitsPerSample = 0;
public Format() { }
}
[StructLayout(LayoutKind.Sequential)]
struct Data
{
byte d = (byte)'d';
byte a = (byte)'a';
byte t = (byte)'t';
byte a2 = (byte)'a';
public uint dataLength=0;
public Data() { }
}
RIFF riff=new RIFF();
Format format = new Format();
Data data = new Data();
public WavPCMFileHeader() { }
public WavPCMFileHeader(ushort nCh, uint nSampleRate, ushort bitsPerSample, uint dataSize)
{
riff.fileLength = (uint)(36 + dataSize);
format.formatTag = 1;
format.channels = nCh;
format.samplesPerSec = nSampleRate;
format.avgBytesPerSec = nSampleRate * nCh * bitsPerSample / 8;
format.blockAlign = (ushort)(nCh * bitsPerSample / 8);
format.bitsPerSample = bitsPerSample;
data.dataLength = dataSize;
}
};
}
三、使用示例
using AC;
try
{
using (var ww = WavWriter.Create("test.wav", 2, 44100, 16))
{
byte[]data;
//获取PCM数据
//略
//获取PCM数据-end
//写入PCM数据
ww.Write(data);
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
总结
以上就是今天要讲的内容,PCM封装成wav还是相对较简单的,只要了解wav头结构,然后自定义其头结构,然后再进行一定的测试,就可以实现这样一个功能。