1. 程式人生 > 程式設計 >c# 通過WinAPI播放PCM聲音

c# 通過WinAPI播放PCM聲音

在Windows平臺上,播放PCM聲音使用的API通常有如下兩種。

  • waveOut and waveIn:傳統的音訊MMEAPI,也是使用的最多的
  • xAudio2:C++/COM API,主要針對遊戲開發,是DirectSound的基礎

在Windows Vista以後,推出了更加強大的WASAPI,並用WASAPI封裝了MME以及DirectSound API。

對於前面的兩個API,在.net平臺下有如下封裝:

  • NAudio
  • Sharpdx

WSAPI可能由於更加複雜,沒有什麼比較完善的封裝,codeproject上有篇文章介紹瞭如何簡單的封裝WSAPI: Recording and playing PCM audio on Windows 8 (VB)

最近一個專案中使用到了PCM檔案的播放,本來想用NAudio實現的,但使用過程中發現它自己提供的BlockAlignReductionStream播放實時資料是效果不是蠻好(方法可以參考這篇文章),總是有一些卡頓的現象。

究其原因是其Buffer的機制,要求每次都填充滿buffer,對於檔案播放這個不是問題,但對於實時pcm資料,buffer過大播放的時候得不到足夠的資料,buffer過小丟資料的情況。

於是,我便研究了一下微軟的MMEAPI,官方文件:Using Waveform and Auxiliary Audio。發現MMEAPI也並不複雜,一個簡單的示例如下

#include <Windows.h>
#include <stdio.h>
#pragma comment(lib,"winmm.lib")
 
int main()
{
  const int buf_size = 1024 * 1024 * 30;
  char* buf = new char[buf_size];
 
  FILE* thbgm; //檔案
 
  fopen_s(&thbgm,R"(r:\re_sample.pcm)","rb");
  fread(buf,sizeof(char),buf_size,thbgm); //預讀取檔案
  fclose(thbgm);
 
  WAVEFORMATEX wfx = {0};
  wfx.wFormatTag = WAVE_FORMAT_PCM; //設定波形聲音的格式
  wfx.nChannels = 2;      //設定音訊檔案的通道數量
  wfx.nSamplesPerSec = 44100; //設定每個聲道播放和記錄時的樣本頻率
  wfx.wBitsPerSample = 16;  //每隔取樣點所佔的大小
 
  wfx.nBlockAlign = wfx.nChannels * wfx.wBitsPerSample / 8;
  wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec;
 
  HANDLE wait = CreateEvent(NULL,NULL);
  HWAVEOUT hwo;
  waveOutOpen(&hwo,WAVE_MAPPER,&wfx,(DWORD_PTR)wait,0L,CALLBACK_EVENT); //開啟一個給定的波形音訊輸出裝置來進行回放
 
  int data_size = 20480;
  char* data_ptr = buf;
  WAVEHDR wh;
 
  while (data_ptr - buf < buf_size)
  {
    //這一部分需要特別注意的是在迴圈回來之後不能花太長的時間去做讀取資料之類的工作,不然在每個迴圈的間隙會有“噠噠”的噪音
    wh.lpData = data_ptr;
    wh.dwBufferLength = data_size;
    wh.dwFlags = 0L;
    wh.dwLoops = 1L;
 
    data_ptr += data_size;
 
    waveOutPrepareHeader(hwo,&wh,sizeof(WAVEHDR)); //準備一個波形資料塊用於播放
    waveOutWrite(hwo,sizeof(WAVEHDR)); //在音訊媒體中播放第二個函式wh指定的資料
 
    WaitForSingleObject(wait,INFINITE); //等待
  }
  waveOutClose(hwo);
  CloseHandle(wait);
 
  
  return 0;
}

這裡是首先預讀pcm檔案到記憶體,然後通過事件回撥的方式同步寫入聲音資料。整個播放過程大概也就用到了五六個API,主要過程如下:

設定音訊引數

音訊引數定義在一個WAVEFORMATEX物件中,這裡只介紹PCM的設定方法,主要設定聲道數、取樣率、和取樣位數。

WAVEFORMATEXwfx={0};
wfx.wFormatTag=WAVE_FORMAT_PCM;//設定波形聲音的格式
wfx.nChannels=2;//設定音訊檔案的道數量
wfx.nSamplesPerSec=44100;//設定每個聲道播放和記錄時的樣本頻率
wfx.wBitsPerSample=16;//每隔取樣點所佔的大小

除此之外,還需要設定兩個引數nBlockAlign和nAvgBytesPerSec。對於PCM,它們的計算公式如下:

wfx.nBlockAlign=wfx.nChannels*wfx.wBitsPerSample/8;
wfx.nAvgBytesPerSec=wfx.nBlockAlign*wfx.nSamplesPerSec;

更多資訊請參看MSDN文件:
https://msdn.microsoft.com/en-us/library/windows/desktop/dd757713(v=vs.85).aspx

開啟音訊輸出

開啟音訊輸出需要定義一個HWAVEOUT物件,它代表一個波形物件,通過waveOutOpen函式開啟它。

HWAVEOUThwo;
waveOutOpen(&hwo,CALLBACK_EVENT);

這個函式前三個引數分別是波形物件,輸出裝置(WAVE_MAPPER為-1,表示預設輸出裝置),音訊引數。 後面三個引數分別是回撥相關引數,因為音訊資料一次只寫入一小段,播放是由系統在另一個執行緒中進行的,當資料播放完成後,需要通過回撥的方式通知寫入新資料。

MMEAPI支援多種回撥方式。具體參看MSDN文件: waveOutOpen function。具體常見的回撥方式有如下幾種:

  • CALLBACK_NULL不回撥,需要主動掌握寫入資料時機,常用於實時音訊流
  • CALLBACK_EVENT需要資料時寫事件,在另外一個獨立的執行緒上等待該事件寫入資料
  • CALLBACK_FUNCTION需要資料時執行回撥函式,在回撥函式中寫入資料

這裡是示例通過事件的方式回撥的

寫入音訊資料

音訊的播放操作是一個生產者消費者模型,呼叫waveOutOpen後,系統會在後臺啟動一個播放執行緒(WinForm程式也可以設定為使用UI執行緒)。當需要資料時,呼叫回撥函式,寫入相應的資料。

首先定義一個WAVEHDR物件:

intdata_size=20480;
char*data_ptr=buf;
WAVEHDRwh;

每次寫入的操作過程如下:

wh.lpData=data_ptr;
wh.dwBufferLength=data_size;
wh.dwFlags=0L;
wh.dwLoops=1L;

data_ptr+=data_size;

waveOutPrepareHeader(hwo,sizeof(WAVEHDR));//準備一個波形資料塊用於播放
waveOutWrite(hwo,sizeof(WAVEHDR));//在音訊媒體中播放第二個函式wh指定的資料

寫入主要是通過兩個函式waveOutPrepareHeader和waveOutWrite進行。這裡有兩個地方需要注意

  1. 每次寫入data_size不要太小,太小了會出現聲音不流暢
  2. 從它呼叫回撥到寫入的時間間隔不能過長,否則會出現聲音斷流而出現的噠噠聲。

這兩個地方的原因實際上都是一個,消費者執行緒沒有足夠的資料。要解決這個問題需要採取緩衝模型,對資料來源預讀。

另外,寫入操作waveOutPrepareHeader和waveOutWrite這兩個函式是並不要求一定非要在等待通知後才執行的,當寫入的速度和播放的速度不一致時,出現聲音快進會慢速播放現象。

關閉音訊輸出

關閉音訊輸出只需要使用介面即可。

waveOutClose(hwo);

.net介面封裝

瞭解各介面功能後,自己封裝一個也比較簡單了。用起來也方便多了。

WinAPI封裝:

using HWAVEOUT = IntPtr;

  class winmm
  {
    [StructLayout(LayoutKind.Sequential)]
    public struct WAVEFORMATEX
    {
      /// <summary>
      /// 波形聲音的格式
      /// </summary>
      public WaveFormat wFormatTag;

      /// <summary>
      /// 音訊檔案的通道數量
      /// </summary>
      public UInt16 nChannels; /* number of channels (i.e. mono,stereo...) */

      /// <summary>
      /// 取樣頻率
      /// </summary>
      public UInt32 nSamplesPerSec; /* sample rate */

      /// <summary>
      /// 每秒緩衝區
      /// </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(LayoutKind.Sequential)]
    public struct WAVEHDR
    {
      /// <summary>
      /// 緩衝區指標
      /// </summary>
      public IntPtr lpData;

      /// <summary>
      /// 緩衝區長度
      /// </summary>
      public UInt32 dwBufferLength;
      public UInt32 dwBytesRecorded; /* used for input only */
      public IntPtr dwUser;     /* for client's use */

      /// <summary>
      /// 設定標誌
      /// </summary>
      public UInt32 dwFlags; 

      /// <summary>
      /// 迴圈控制
      /// </summary>
      public UInt32 dwLoops; 

      /// <summary>
      /// 保留欄位
      /// </summary>
      public IntPtr lpNext; 

      /// <summary>
      /// 保留欄位
      /// </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>
    /// 預設裝置
    /// </summary>
    public static IntPtr WAVE_MAPPER { get; } = (IntPtr)(-1);

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

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

    [DllImport("winmm.dll")]
    public static extern int waveOutOpen(out HWAVEOUT hWaveOut,IntPtr    dwCallback,WaveOpenFlags  dwFlags);

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

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

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

    [DllImport("winmm.dll")]
    public static extern int waveOutUnprepareHeader(HWAVEOUT hWaveOut,int uSize);

    [DllImport("winmm.dll")]
    public static extern int waveOutWrite(HWAVEOUT hWaveOut,int uSize);
  }

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

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

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

PCM播放器:

/// <summary>
  /// Pcm播放器
  /// </summary>
  public unsafe class PcmPlayer
  {
    /// <param name="channels">聲道數目</param>
    /// <param name="sampleRate">取樣頻率</param>
    /// <param name="sampleSize">取樣大小(bits)</param>
    public PcmPlayer(int channels,int sampleRate,int sampleSize)
    {
      _wfx = new winmm.WAVEFORMATEX
      {
        wFormatTag   = winmm.WaveFormat.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;
    }

    winmm.WAVEFORMATEX _wfx;
    IntPtr    _hwo;

    /// <summary>
    /// 以事件回撥的方式開啟裝置
    /// </summary>
    /// <param name="waitEvent"></param>
    public void OpenEvent(IntPtr waitEvent)
    {
      winmm.waveOutOpen(out _hwo,winmm.WAVE_MAPPER,_wfx,waitEvent,IntPtr.Zero,winmm.WaveOpenFlags.CALLBACK_EVENT);
      Debug.Assert(_hwo != IntPtr.Zero);
    }

    public void OpenNone()
    {
      winmm.waveOutOpen(out _hwo,winmm.WaveOpenFlags.CALLBACK_NULL);
      Debug.Assert(_hwo != IntPtr.Zero);
    }


    winmm.WAVEHDR _wh;
    public void WriteData(ReadOnlyMemory<byte> buffer)
    {
      var hwnd = buffer.Pin();

      _wh.lpData     = (IntPtr)hwnd.Pointer;
      _wh.dwBufferLength = (uint)buffer.Length;
      _wh.dwFlags    = 0;
      _wh.dwLoops    = 1;

      winmm.waveOutPrepareHeader(_hwo,_wh,sizeof(winmm.WAVEHDR)); //準備一個波形資料塊用於播放
      winmm.waveOutWrite(_hwo,sizeof(winmm.WAVEHDR));     //在音訊媒體中播放第二個函式wh指定的資料
      hwnd.Dispose();
    }

    public void Dispose()
    {
      winmm.waveOutPrepareHeader(_hwo,sizeof(winmm.WAVEHDR));
      winmm.waveOutClose(_hwo);
      _hwo = IntPtr.Zero;
    }
  }

  public class WaitObject : IDisposable
  {

    public IntPtr Hwnd { get; set; }

    public WaitObject()
    {
      Hwnd = kernel32.CreateEvent(IntPtr.Zero,false,null);
    }

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

    public void Dispose()
    {
      kernel32.CloseHandle(Hwnd);
      Hwnd = IntPtr.Zero;
    }
  }

以上就是c# 通過WinAPI播放PCM聲音的詳細內容,更多關於c# 播放PCM聲音的資料請關注我們其它相關文章!