C# encapsulates audio PCM data into wav files

Article directory

  • Preface
  • 1. How to achieve it?
    • 1. Define the header structure
    • 2. Reserve head space
    • 3.Write PCM data
    • 4. Write header information
  • 2. Complete code
  • 3. Usage examples
  • Summarize

Foreword

Previously, “C++ encapsulates audio PCM data into wav files” was implemented, and it was recently changed to a C# version. When using C# to implement the recording function, you still need to write a wav file. It is relatively simple to implement it directly in C#, which can avoid unnecessary dependencies.


1. How to implement?

First of all, we need to construct the wav header. All the audio information of the wav file is stored in the header. What we have to do is to add the wav header in front of the PCM data and record the relevant parameters of the PCM.

1. Define the header structure

Only the wav file header in PCM format is defined, including 3 parts: riff, format, and data. You need to use [StructLayout(LayoutKind.Sequential)] to describe the structure to ensure memory continuity.
WAV header

//WAV header structure-PCM format
[StructLayout(LayoutKind.Sequential)]
struct WavPCMFileHeader
{<!-- -->
    RIFF riff=new RIFF();
    Format format = new Format();
    Data data = new Data();
    public WavPCMFileHeader() {<!-- --> }
}

RTTF part

[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 part

[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 section

[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. Reserve header space

Reserve header space when creating a file

_stream = File.Open(fileName, FileMode.Create);
_stream!.Seek(Marshal.SizeOf<WavPCMFileHeader>(), SeekOrigin.Begin);

3.Write PCM data

Write data

_stream!.Write(data);

4. Write header information

When closing the file, return to the starting position and write the header information

//Write header information
_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;

2. Complete code

.net6.0
WavWriter.cs

using System.Runtime.InteropServices;
/****************************************************** *************************
* @Project: AC::WavWriter
* @Decription: wav file writing tool
* @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 writing tool, currently only supports pcm format
    /// </summary>
    public class WavWriter:IDisposable
    {<!-- -->
        ushort _channels;
        uint _sampleRate;
        ushort _bitsPerSample;
        FileStream? _stream;
        /// <summary>
        /// Create object
        /// </summary>
        /// <param name="fileName">File name</param>
        /// <param name="channels">Number of channels</param>
        /// <param name="sampleRate">Sampling rate, unit hz</param>
        /// <param name="bitsPerSample">Bit depth</param>
        public static WavWriter Create(string fileName, ushort channels, uint sampleRate, ushort bitsPerSample)
        {<!-- -->
          return new WavWriter(fileName, channels, sampleRate, bitsPerSample);
        }

        /// <summary>
        /// Construction method
        /// </summary>
        /// <param name="fileName">File name</param>
        /// <param name="channels">Number of channels</param>
        /// <param name="sampleRate">Sampling rate, unit hz</param>
        /// <param name="bitsPerSample">Bit depth</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>
        ///Write PCM data
        /// </summary>
        /// <param name="data">PCM data</param>
        public void Write(byte[] data)
        {<!-- -->
            _stream!.Write(data);
        }
        /// <summary>
        ///Write PCM data
        /// </summary>
        /// <param name="stream">PCM data</param>
        public void Write(Stream stream)
        {<!-- -->
            stream.CopyTo(_stream!);
        }
        /// <summary>
        /// Close the file
        /// </summary>
        public void Close()
        {<!-- -->
            //Write header information
            _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 header structure-PCM format
    [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;
        }
    };
}

3. Usage examples

using AC;
try
{<!-- -->
    using (var ww = WavWriter.Create("test.wav", 2, 44100, 16))
    {<!-- -->
        byte[]data;
        //Get PCM data
\t\t//slightly
//Get PCM data-end
//Write PCM data
ww.Write(data);
    }
}
catch (Exception e)
{<!-- -->
    Console.WriteLine(e.Message);
}

Summary

The above is what I will talk about today. It is relatively simple to encapsulate PCM into wav. As long as you understand the wav header structure, then customize its header structure, and then conduct certain tests, you can achieve it. Such a function.