SoFunction
Updated on 2025-03-07

c# Play PCM sound through WinAPI

On Windows platforms, there are usually two APIs used to play PCM sounds.

  • waveOut and waveIn: Traditional audio MMEAPI is also the most commonly used
  • xAudio2: C++/COM API, mainly for game development, is the basis of DirectSound

After Windows Vista, more powerfulWASAPI , and encapsulate MME and DirectSound API with WASAPI.

For the previous two APIs, there are the following packages under the .net platform:

  • NAudio
  • Sharpdx

WSAPI may be more complex and does not have any perfect encapsulation. An article on codeproject introduces how to simply encapsulate WSAPI:Recording and playing PCM audio on Windows 8 (VB)

Recently, PCM file playback was used in a project. I originally wanted to use NAudio to implement it, but during the use, I found that the BlockAlignReductionStream it provides itself provides for playing real-time data is not very effective (see this article for the method.article), there are always some lags.

The reason is that the buffer mechanism requires that buffers be filled every time. This is not a problem for file playback, but for real-time pcm data, if the buffer is too large and the buffer is too small to lose data.

So, I studied Microsoft's MMEAPI, the official documentation:Using Waveform and Auxiliary Audio. I found that MMEAPI is not complicated either. A simple example is as follows

#include <>
#include <>
#pragma comment(lib, "")
 
int main()
{
  const int buf_size = 1024 * 1024 * 30;
  char* buf = new char[buf_size];
 
  FILE* thbgm; //document 
  fopen_s(&thbgm, R"(r:\re_sample.pcm)", "rb");
  fread(buf, sizeof(char), buf_size, thbgm); //Pre-read file  fclose(thbgm);
 
  WAVEFORMATEX wfx = {0};
   = WAVE_FORMAT_PCM; //Set the format of waveform sound   = 2;      //Set the number of channels of audio files   = 44100; //Set the sample frequency when playing and recording each channel   = 16;  //The size of each sampling point 
   =  *  / 8;
   =  * ;
 
  HANDLE wait = CreateEvent(NULL, 0, 0, NULL);
  HWAVEOUT hwo;
  waveOutOpen(&hwo, WAVE_MAPPER, &wfx, (DWORD_PTR)wait, 0L, CALLBACK_EVENT); //Open a given waveform audio output device for playback 
  int data_size = 20480;
  char* data_ptr = buf;
  WAVEHDR wh;
 
  while (data_ptr - buf < buf_size)
  {
    //This part needs special attention. You cannot spend too long to read data after the loop is back, otherwise there will be "da da" noise in the gap between each loop.     = data_ptr;
     = data_size;
     = 0L;
     = 1L;
 
    data_ptr += data_size;
 
    waveOutPrepareHeader(hwo, &wh, sizeof(WAVEHDR)); //Prepare a waveform data block for playback    waveOutWrite(hwo, &wh, sizeof(WAVEHDR)); //Play the data specified by the second function wh in the audio media 
    WaitForSingleObject(wait, INFINITE); //wait  }
  waveOutClose(hwo);
  CloseHandle(wait);
 
  
  return 0;
}

Here, first read the pcm file to memory, and then write the sound data synchronously through event callbacks. The entire playback process only uses five or six APIs, and the main process is as follows:

Set audio parameters

The audio parameters are defined in a WAVEFORMATEX object. Here we only introduce the PCM setting method, mainly setting the number of channels, sampling rate, and sampling bits.

WAVEFORMATEX    wfx = { 0 };
 = WAVE_FORMAT_PCM;    //Set the format of waveform sound = 2;                    //Set the number of audio files = 44100;            //Set the sample frequency when playing and recording each channel = 16;            //The size of each sampling point

In addition, two parameters nBlockAlign and nAvgBytesPerSec need to be set. For PCM, their calculation formula is as follows:

 =  *  / 8; 
 =  * ; 

For more information, please refer to the MSDN documentation:
/en-us/library/windows/desktop/dd757713(v=vs.85).aspx

Turn on audio output

To open the audio output, you need to define a HWAVEOUT object, which represents a waveform object, and open it through the waveOutOpen function.

HWAVEOUT hwo;
waveOutOpen(&hwo, WAVE_MAPPER, &wfx, (DWORD_PTR)wait, 0L, CALLBACK_EVENT); 

The first three parameters of this function are waveform objects, output devices (WAVE_MAPPER is -1, indicating the default output device), and audio parameters. The last three parameters are callback-related parameters, because the audio data is only written to a small section at a time, and the playback is performed by the system in another thread. When the data playback is completed, new data needs to be notified to be written through callback.

MMEAPI supports multiple callback methods. For details, please refer to the MSDN documentation:waveOutOpen function. There are several common callback methods:

  • CALLBACK_NULL         No callback, you need to actively grasp the timing of writing data, and is often used in real-time audio streaming
  • CALLBACK_EVENT       Write an event when data is needed, and wait for the event to write data on another independent thread.
  • CALLBACK_FUNCTION        Execute the callback function when data is needed and write the data in the callback function

Here is an example callback through event

Write audio data

The audio playback operation is a producer consumer model. After calling waveOutOpen, the system will start a playback thread in the background (the WinForm program can also be set to use UI threads). When data is needed, call the callback function and write the corresponding data.

First define a WAVEHDR object:

int data_size = 20480;
char* data_ptr = buf;
WAVEHDR wh;

The operation process for each write is as follows:

 = data_ptr;
 = data_size;
 = 0L;
 = 1L;

data_ptr += data_size;

waveOutPrepareHeader(hwo, &wh, sizeof(WAVEHDR)); //Prepare a waveform data block for playbackwaveOutWrite(hwo, &wh, sizeof(WAVEHDR)); //Play the second function in the audio mediawhSpecified data

Write is mainly done through two functions waveOutPrepareHeader and waveOutWrite. There are two places to pay attention to

  1. Don't write data_size too small every time, as it will cause the sound to be unsmooth.
  2. The time interval between calling the callback to writing cannot be too long, otherwise there will be a clatter sound caused by the sound interruption.

The reason for these two places is actually the same, the consumer thread does not have enough data. To solve this problem, we need to adopt a buffer model to read the data source in advance.

In addition, the two functions of the write operations waveOutPrepareHeader and waveOutWrite do not require that they must be executed after waiting for notification. When the write speed and playback speed are inconsistent, the sound fast forward will play slowly.

Turn off audio output

To turn off the audio output, you only need to use the interface.

waveOutClose(hwo);

.net interface packaging

After understanding the functions of each interface, it is easier to encapsulate one by yourself. It's much more convenient to use.

WinAPI encapsulation:

using HWAVEOUT = IntPtr;

  class winmm
  {
    [StructLayout()]
    public struct WAVEFORMATEX
    {
      /// <summary>
      /// Format of waveform sound      /// </summary>
      public WaveFormat wFormatTag;

      /// <summary>
      /// Number of channels of audio files      /// </summary>
      public UInt16 nChannels; /* number of channels (. mono, stereo...) */

      /// <summary>
      /// Sampling frequency      /// </summary>
      public UInt32 nSamplesPerSec; /* sample rate */

      /// <summary>
      /// Buffer per second      /// </summary>
      public UInt32 nAvgBytesPerSec; /* for buffer estimation */


      public UInt16 nBlockAlign;  /* block size of data */
      public UInt16 wBitsPerSample; /* number of bits per sample of mono data */
      public UInt16 cbSize;     /* the count in bytes of the size of */
    }

    [StructLayout()]
    public struct WAVEHDR
    {
      /// <summary>
      /// Buffer pointer      /// </summary>
      public IntPtr lpData;

      /// <summary>
      /// Buffer length      /// </summary>
      public UInt32 dwBufferLength;
      public UInt32 dwBytesRecorded; /* used for input only */
      public IntPtr dwUser;     /* for client's use */

      /// <summary>
      /// Set flags      /// </summary>
      public UInt32 dwFlags; 

      /// <summary>
      /// Cycle control      /// </summary>
      public UInt32 dwLoops; 

      /// <summary>
      /// Reserve fields      /// </summary>
      public IntPtr lpNext; 

      /// <summary>
      /// Reserve fields      /// </summary>
      public IntPtr reserved;
    }


    [Flags]
    public enum WaveOpenFlags
    {
      CALLBACK_NULL   = 0,
      CALLBACK_FUNCTION = 0x30000,
      CALLBACK_EVENT  = 0x50000,
      CallbackWindow  = 0x10000,
      CallbackThread  = 0x20000,
    }

    public enum WaveMessage
    {
      WIM_OPEN = 0x3BE,
      WIM_CLOSE = 0x3BF,
      WIM_DATA = 0x3C0,
      WOM_CLOSE = 0x3BC,
      WOM_DONE = 0x3BD,
      WOM_OPEN = 0x3BB
    }


    [Flags]
    public enum WaveHeaderFlags
    {
      WHDR_BEGINLOOP = 0x00000004,
      WHDR_DONE   = 0x00000001,
      WHDR_ENDLOOP  = 0x00000008,
      WHDR_INQUEUE  = 0x00000010,
      WHDR_PREPARED = 0x00000002
    }

    public enum WaveFormat : ushort
    {
      WAVE_FORMAT_PCM = 0x0001,
    }


    /// <summary>
    /// Default device    /// </summary>
    public static IntPtr WAVE_MAPPER { get; } = (IntPtr)(-1);

    public delegate void WaveCallback(IntPtr hWaveOut, WaveMessage message, IntPtr dwInstance, WAVEHDR wavhdr,
                     IntPtr dwReserved);

    [DllImport("")]
    public static extern int waveOutOpen(out HWAVEOUT hWaveOut,  IntPtr uDeviceID, in WAVEFORMATEX lpFormat,
                       WaveCallback dwCallback, IntPtr dwInstance, WaveOpenFlags  dwFlags);

    [DllImport("")]
    public static extern int waveOutOpen(out HWAVEOUT hWaveOut,  IntPtr uDeviceID, in WAVEFORMATEX lpFormat,
                       IntPtr    dwCallback, IntPtr dwInstance, WaveOpenFlags  dwFlags);

    [DllImport("")]
    public static extern int waveOutSetVolume(HWAVEOUT hwo, ushort dwVolume);

    [DllImport("")]
    public static extern int waveOutClose(in HWAVEOUT hWaveOut);

    [DllImport("")]
    public static extern int waveOutPrepareHeader(HWAVEOUT hWaveOut, in WAVEHDR lpWaveOutHdr, int uSize);

    [DllImport("")]
    public static extern int waveOutUnprepareHeader(HWAVEOUT hWaveOut, in WAVEHDR lpWaveOutHdr, int uSize);

    [DllImport("")]
    public static extern int waveOutWrite(HWAVEOUT hWaveOut, in WAVEHDR lpWaveOutHdr, int uSize);
  }

  class kernel32
  {
    [DllImport("")]
    public static extern IntPtr CreateEvent(IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName);

    [DllImport("")]
    public static extern int WaitForSingleObject(IntPtr hHandle, int dwMilliseconds);

    [DllImport("")]
    public static extern bool CloseHandle(IntPtr hHandle);
  }

PCM player:

/// <summary>
  /// Pcm player  /// </summary>
  public unsafe class PcmPlayer
  {
    /// <param name="channels">Number of channels</param>    /// <param name="sampleRate">Sampling frequency</param>    /// <param name="sampleSize">Sample size (bits)</param>    public PcmPlayer(int channels, int sampleRate, int sampleSize)
    {
      _wfx = new 
      {
        wFormatTag   = .WAVE_FORMAT_PCM,
        nChannels   = (ushort)channels,
        nSamplesPerSec = (ushort)sampleRate,
        wBitsPerSample = (ushort)sampleSize
      };

      _wfx.nBlockAlign   = (ushort)(_wfx.nChannels * _wfx.wBitsPerSample / 8);
      _wfx.nAvgBytesPerSec = _wfx.nBlockAlign * _wfx.nSamplesPerSec;
    }

     _wfx;
    IntPtr    _hwo;

    /// &lt;summary&gt;
    /// Open the device in an event callback    /// &lt;/summary&gt;
    /// &lt;param name="waitEvent"&gt;&lt;/param&gt;
    public void OpenEvent(IntPtr waitEvent)
    {
      (out _hwo, winmm.WAVE_MAPPER, _wfx, waitEvent, , .CALLBACK_EVENT);
      (_hwo != );
    }

    public void OpenNone()
    {
      (out _hwo, winmm.WAVE_MAPPER, _wfx, , , .CALLBACK_NULL);
      (_hwo != );
    }


     _wh;
    public void WriteData(ReadOnlyMemory&lt;byte&gt; buffer)
    {
      var hwnd = ();

      _wh.lpData     = (IntPtr);
      _wh.dwBufferLength = (uint);
      _wh.dwFlags    = 0;
      _wh.dwLoops    = 1;

      (_hwo, _wh, sizeof()); //Prepare a waveform data block for playback      (_hwo, _wh, sizeof());     //Play the data specified by the second function wh in the audio media      ();
    }

    public void Dispose()
    {
      (_hwo, _wh, sizeof());
      (_hwo);
      _hwo = ;
    }
  }

  public class WaitObject : IDisposable
  {

    public IntPtr Hwnd { get; set; }

    public WaitObject()
    {
      Hwnd = (, false, false, null);
    }

    public void Wait()
    {
      (Hwnd, -1);
    }

    public void Dispose()
    {
      (Hwnd);
      Hwnd = ;
    }
  }

The above is the detailed content of c# playing PCM sound through WinAPI. For more information about c# playing PCM sound, please follow my other related articles!