c# 通過WinAPI播放PCM聲音
在Windows平臺上,播放PCM聲音使用的API通常有如下兩種。
- waveOut and waveIn:傳統(tǒng)的音頻MMEAPI,也是使用的最多的
- xAudio2:C++/COM API,主要針對游戲開發(fā),是DirectSound的基礎(chǔ)
在Windows Vista以后,推出了更加強大的WASAPI ,并用WASAPI封裝了MME以及DirectSound API。
對于前面的兩個API,在.net平臺下有如下封裝:
WSAPI可能由于更加復(fù)雜,沒有什么比較完善的封裝,codeproject上有篇文章介紹了如何簡單的封裝WSAPI: Recording and playing PCM audio on Windows 8 (VB)
最近一個項目中使用到了PCM文件的播放,本來想用NAudio實現(xiàn)的,但使用過程中發(fā)現(xiàn)它自己提供的BlockAlignReductionStream播放實時數(shù)據(jù)是效果不是蠻好(方法可以參考這篇文章),總是有一些卡頓的現(xiàn)象。
究其原因是其Buffer的機制,要求每次都填充滿buffer,對于文件播放這個不是問題,但對于實時pcm數(shù)據(jù),buffer過大播放的時候得不到足夠的數(shù)據(jù),buffer過小丟數(shù)據(jù)的情況。
于是,我便研究了一下微軟的MMEAPI,官方文檔:Using Waveform and Auxiliary Audio。發(fā)現(xiàn)MMEAPI也并不復(fù)雜,一個簡單的示例如下
#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); //預(yù)讀取文件 fclose(thbgm); WAVEFORMATEX wfx = {0}; wfx.wFormatTag = WAVE_FORMAT_PCM; //設(shè)置波形聲音的格式 wfx.nChannels = 2; //設(shè)置音頻文件的通道數(shù)量 wfx.nSamplesPerSec = 44100; //設(shè)置每個聲道播放和記錄時的樣本頻率 wfx.wBitsPerSample = 16; //每隔采樣點所占的大小 wfx.nBlockAlign = wfx.nChannels * wfx.wBitsPerSample / 8; wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec; HANDLE wait = CreateEvent(NULL, 0, 0, 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) { //這一部分需要特別注意的是在循環(huán)回來之后不能花太長的時間去做讀取數(shù)據(jù)之類的工作,不然在每個循環(huán)的間隙會有“噠噠”的噪音 wh.lpData = data_ptr; wh.dwBufferLength = data_size; wh.dwFlags = 0L; wh.dwLoops = 1L; data_ptr += data_size; waveOutPrepareHeader(hwo, &wh, sizeof(WAVEHDR)); //準備一個波形數(shù)據(jù)塊用于播放 waveOutWrite(hwo, &wh, sizeof(WAVEHDR)); //在音頻媒體中播放第二個函數(shù)wh指定的數(shù)據(jù) WaitForSingleObject(wait, INFINITE); //等待 } waveOutClose(hwo); CloseHandle(wait); return 0; }
這里是首先預(yù)讀pcm文件到內(nèi)存,然后通過事件回調(diào)的方式同步寫入聲音數(shù)據(jù)。 整個播放過程大概也就用到了五六個API,主要過程如下:
設(shè)置音頻參數(shù)
音頻參數(shù)定義在一個WAVEFORMATEX對象中,這里只介紹PCM的設(shè)置方法,主要設(shè)置聲道數(shù)、采樣率、和采樣位數(shù)。
WAVEFORMATEX wfx = { 0 }; wfx.wFormatTag = WAVE_FORMAT_PCM; //設(shè)置波形聲音的格式 wfx.nChannels = 2; //設(shè)置音頻文件的道數(shù)量 wfx.nSamplesPerSec = 44100; //設(shè)置每個聲道播放和記錄時的樣本頻率 wfx.wBitsPerSample = 16; //每隔采樣點所占的大小
除此之外,還需要設(shè)置兩個參數(shù)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函數(shù)打開它。
HWAVEOUT hwo; waveOutOpen(&hwo, WAVE_MAPPER, &wfx, (DWORD_PTR)wait, 0L, CALLBACK_EVENT);
這個函數(shù)前三個參數(shù)分別是波形對象,輸出設(shè)備(WAVE_MAPPER為-1,表示默認輸出設(shè)備),音頻參數(shù)。 后面三個參數(shù)分別是回調(diào)相關(guān)參數(shù),因為音頻數(shù)據(jù)一次只寫入一小段,播放是由系統(tǒng)在另一個線程中進行的,當數(shù)據(jù)播放完成后,需要通過回調(diào)的方式通知寫入新數(shù)據(jù)。
MMEAPI支持多種回調(diào)方式。具體參看MSDN文檔: waveOutOpen function。具體常見的回調(diào)方式有如下幾種:
- CALLBACK_NULL 不回調(diào),需要主動掌握寫入數(shù)據(jù)時機,常用于實時音頻流
- CALLBACK_EVENT 需要數(shù)據(jù)時寫事件,在另外一個獨立的線程上等待該事件寫入數(shù)據(jù)
- CALLBACK_FUNCTION 需要數(shù)據(jù)時執(zhí)行回調(diào)函數(shù),在回調(diào)函數(shù)中寫入數(shù)據(jù)
這里是示例通過事件的方式回調(diào)的
寫入音頻數(shù)據(jù)
音頻的播放操作是一個生產(chǎn)者消費者模型,調(diào)用waveOutOpen后,系統(tǒng)會在后臺啟動一個播放線程(WinForm程序也可以設(shè)置為使用UI線程)。當需要數(shù)據(jù)時,調(diào)用回調(diào)函數(shù),寫入相應(yīng)的數(shù)據(jù)。
首先定義一個WAVEHDR對象:
int data_size = 20480; char* data_ptr = buf; WAVEHDR wh;
每次寫入的操作過程如下:
wh.lpData = data_ptr; wh.dwBufferLength = data_size; wh.dwFlags = 0L; wh.dwLoops = 1L; data_ptr += data_size; waveOutPrepareHeader(hwo, &wh, sizeof(WAVEHDR)); //準備一個波形數(shù)據(jù)塊用于播放 waveOutWrite(hwo, &wh, sizeof(WAVEHDR)); //在音頻媒體中播放第二個函數(shù)wh指定的數(shù)據(jù)
寫入主要是通過兩個函數(shù)waveOutPrepareHeader和waveOutWrite進行。這里有兩個地方需要注意
- 每次寫入data_size不要太小,太小了會出現(xiàn)聲音不流暢
- 從它調(diào)用回調(diào)到寫入的時間間隔不能過長,否則會出現(xiàn)聲音斷流而出現(xiàn)的噠噠聲。
這兩個地方的原因?qū)嶋H上都是一個,消費者線程沒有足夠的數(shù)據(jù)。要解決這個問題需要采取緩沖模型,對數(shù)據(jù)源預(yù)讀。
另外,寫入操作waveOutPrepareHeader和waveOutWrite這兩個函數(shù)是并不要求一定非要在等待通知后才執(zhí)行的,當寫入的速度和播放的速度不一致時,出現(xiàn)聲音快進會慢速播放現(xiàn)象。
關(guān)閉音頻輸出
關(guān)閉音頻輸出只需要使用接口即可。
waveOutClose(hwo);
.net接口封裝
了解各接口功能后,自己封裝一個也比較簡單了。用起來也方便多了。
WinAPI封裝:
using HWAVEOUT = IntPtr; class winmm { [StructLayout(LayoutKind.Sequential)] public struct WAVEFORMATEX { /// <summary> /// 波形聲音的格式 /// </summary> public WaveFormat wFormatTag; /// <summary> /// 音頻文件的通道數(shù)量 /// </summary> public UInt16 nChannels; /* number of channels (i.e. mono, stereo...) */ /// <summary> /// 采樣頻率 /// </summary> public UInt32 nSamplesPerSec; /* sample rate */ /// <summary> /// 每秒緩沖區(qū) /// </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> /// 緩沖區(qū)指針 /// </summary> public IntPtr lpData; /// <summary> /// 緩沖區(qū)長度 /// </summary> public UInt32 dwBufferLength; public UInt32 dwBytesRecorded; /* used for input only */ public IntPtr dwUser; /* for client's use */ /// <summary> /// 設(shè)置標志 /// </summary> public UInt32 dwFlags; /// <summary> /// 循環(huán)控制 /// </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> /// 默認設(shè)備 /// </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, IntPtr dwInstance, WaveOpenFlags dwFlags); [DllImport("winmm.dll")] public static extern int waveOutOpen(out HWAVEOUT hWaveOut, IntPtr uDeviceID, in WAVEFORMATEX lpFormat, IntPtr dwCallback, IntPtr dwInstance, 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, in WAVEHDR lpWaveOutHdr, int uSize); [DllImport("winmm.dll")] public static extern int waveOutWrite(HWAVEOUT hWaveOut, in WAVEHDR lpWaveOutHdr, 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">聲道數(shù)目</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> /// 以事件回調(diào)的方式打開設(shè)備 /// </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.WAVE_MAPPER, _wfx, IntPtr.Zero, IntPtr.Zero, 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)); //準備一個波形數(shù)據(jù)塊用于播放 winmm.waveOutWrite(_hwo, _wh, sizeof(winmm.WAVEHDR)); //在音頻媒體中播放第二個函數(shù)wh指定的數(shù)據(jù) hwnd.Dispose(); } public void Dispose() { winmm.waveOutPrepareHeader(_hwo, _wh, 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, false, null); } public void Wait() { kernel32.WaitForSingleObject(Hwnd, -1); } public void Dispose() { kernel32.CloseHandle(Hwnd); Hwnd = IntPtr.Zero; } }
以上就是c# 通過WinAPI播放PCM聲音的詳細內(nèi)容,更多關(guān)于c# 播放PCM聲音的資料請關(guān)注腳本之家其它相關(guān)文章!
- C# 調(diào)用WebApi的實現(xiàn)
- c# 調(diào)用Win32Api關(guān)閉當前應(yīng)用的方法
- c# webapi 配置swagger的方法
- C#調(diào)用Win32的API函數(shù)--User32.dll
- C#調(diào)用新浪微博API實例代碼
- C#調(diào)用百度API實現(xiàn)活體檢測的方法
- C# WebApi Get請求方式傳遞實體參數(shù)的方法示例
- C#凈化版WebApi框架的實現(xiàn)
- C# WebApi 路由機制剖析
- C# WebApi 接口傳參詳解
- c#在WebAPI使用Session的方法
- c# 常見文件路徑Api的使用示例
相關(guān)文章
C# Ado.net實現(xiàn)讀取SQLServer數(shù)據(jù)庫存儲過程列表及參數(shù)信息示例
這篇文章主要介紹了C# Ado.net實現(xiàn)讀取SQLServer數(shù)據(jù)庫存儲過程列表及參數(shù)信息,結(jié)合實例形式總結(jié)分析了C#針對SQLServer數(shù)據(jù)庫存儲過程及參數(shù)信息的各種常見操作技巧,需要的朋友可以參考下2019-02-02C#入門學(xué)習(xí)之集合、比較和轉(zhuǎn)換
本文詳細講解了C#中的集合、比較和轉(zhuǎn)換,文中通過示例代碼介紹的非常詳細。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-05-05Unity UGUI的RectMask2D遮罩組件的介紹使用
這篇文章主要為大家介紹了Unity UGUI的RectMask2D遮罩組件的介紹使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07