c# 通過WinAPI播放PCM聲音
在Windows平臺(tái)上,播放PCM聲音使用的API通常有如下兩種。
- waveOut and waveIn:傳統(tǒng)的音頻MMEAPI,也是使用的最多的
- xAudio2:C++/COM API,主要針對(duì)游戲開發(fā),是DirectSound的基礎(chǔ)
在Windows Vista以后,推出了更加強(qiáng)大的WASAPI ,并用WASAPI封裝了MME以及DirectSound API。
對(duì)于前面的兩個(gè)API,在.net平臺(tái)下有如下封裝:
WSAPI可能由于更加復(fù)雜,沒有什么比較完善的封裝,codeproject上有篇文章介紹了如何簡(jiǎn)單的封裝WSAPI: Recording and playing PCM audio on Windows 8 (VB)
最近一個(gè)項(xiàng)目中使用到了PCM文件的播放,本來想用NAudio實(shí)現(xiàn)的,但使用過程中發(fā)現(xiàn)它自己提供的BlockAlignReductionStream播放實(shí)時(shí)數(shù)據(jù)是效果不是蠻好(方法可以參考這篇文章),總是有一些卡頓的現(xiàn)象。
究其原因是其Buffer的機(jī)制,要求每次都填充滿buffer,對(duì)于文件播放這個(gè)不是問題,但對(duì)于實(shí)時(shí)pcm數(shù)據(jù),buffer過大播放的時(shí)候得不到足夠的數(shù)據(jù),buffer過小丟數(shù)據(jù)的情況。
于是,我便研究了一下微軟的MMEAPI,官方文檔:Using Waveform and Auxiliary Audio。發(fā)現(xiàn)MMEAPI也并不復(fù)雜,一個(gè)簡(jiǎn)單的示例如下
#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è)置每個(gè)聲道播放和記錄時(shí)的樣本頻率
wfx.wBitsPerSample = 16; //每隔采樣點(diǎn)所占的大小
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); //打開一個(gè)給定的波形音頻輸出裝置來進(jìn)行回放
int data_size = 20480;
char* data_ptr = buf;
WAVEHDR wh;
while (data_ptr - buf < buf_size)
{
//這一部分需要特別注意的是在循環(huán)回來之后不能花太長(zhǎng)的時(shí)間去做讀取數(shù)據(jù)之類的工作,不然在每個(gè)循環(huán)的間隙會(huì)有“噠噠”的噪音
wh.lpData = data_ptr;
wh.dwBufferLength = data_size;
wh.dwFlags = 0L;
wh.dwLoops = 1L;
data_ptr += data_size;
waveOutPrepareHeader(hwo, &wh, sizeof(WAVEHDR)); //準(zhǔn)備一個(gè)波形數(shù)據(jù)塊用于播放
waveOutWrite(hwo, &wh, sizeof(WAVEHDR)); //在音頻媒體中播放第二個(gè)函數(shù)wh指定的數(shù)據(jù)
WaitForSingleObject(wait, INFINITE); //等待
}
waveOutClose(hwo);
CloseHandle(wait);
return 0;
}
這里是首先預(yù)讀pcm文件到內(nèi)存,然后通過事件回調(diào)的方式同步寫入聲音數(shù)據(jù)。 整個(gè)播放過程大概也就用到了五六個(gè)API,主要過程如下:
設(shè)置音頻參數(shù)
音頻參數(shù)定義在一個(gè)WAVEFORMATEX對(duì)象中,這里只介紹PCM的設(shè)置方法,主要設(shè)置聲道數(shù)、采樣率、和采樣位數(shù)。
WAVEFORMATEX wfx = { 0 };
wfx.wFormatTag = WAVE_FORMAT_PCM; //設(shè)置波形聲音的格式
wfx.nChannels = 2; //設(shè)置音頻文件的道數(shù)量
wfx.nSamplesPerSec = 44100; //設(shè)置每個(gè)聲道播放和記錄時(shí)的樣本頻率
wfx.wBitsPerSample = 16; //每隔采樣點(diǎn)所占的大小
除此之外,還需要設(shè)置兩個(gè)參數(shù)nBlockAlign和nAvgBytesPerSec。對(duì)于PCM,它們的計(jì)算公式如下:
wfx.nBlockAlign = wfx.nChannels * wfx.wBitsPerSample / 8; wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec;
更多信息請(qǐng)參看MSDN文檔:
https://msdn.microsoft.com/en-us/library/windows/desktop/dd757713(v=vs.85).aspx
打開音頻輸出
打開音頻輸出需要定義一個(gè)HWAVEOUT對(duì)象,它代表一個(gè)波形對(duì)象,通過waveOutOpen函數(shù)打開它。
HWAVEOUT hwo; waveOutOpen(&hwo, WAVE_MAPPER, &wfx, (DWORD_PTR)wait, 0L, CALLBACK_EVENT);
這個(gè)函數(shù)前三個(gè)參數(shù)分別是波形對(duì)象,輸出設(shè)備(WAVE_MAPPER為-1,表示默認(rèn)輸出設(shè)備),音頻參數(shù)。 后面三個(gè)參數(shù)分別是回調(diào)相關(guān)參數(shù),因?yàn)橐纛l數(shù)據(jù)一次只寫入一小段,播放是由系統(tǒng)在另一個(gè)線程中進(jìn)行的,當(dāng)數(shù)據(jù)播放完成后,需要通過回調(diào)的方式通知寫入新數(shù)據(jù)。
MMEAPI支持多種回調(diào)方式。具體參看MSDN文檔: waveOutOpen function。具體常見的回調(diào)方式有如下幾種:
- CALLBACK_NULL 不回調(diào),需要主動(dòng)掌握寫入數(shù)據(jù)時(shí)機(jī),常用于實(shí)時(shí)音頻流
- CALLBACK_EVENT 需要數(shù)據(jù)時(shí)寫事件,在另外一個(gè)獨(dú)立的線程上等待該事件寫入數(shù)據(jù)
- CALLBACK_FUNCTION 需要數(shù)據(jù)時(shí)執(zhí)行回調(diào)函數(shù),在回調(diào)函數(shù)中寫入數(shù)據(jù)
這里是示例通過事件的方式回調(diào)的
寫入音頻數(shù)據(jù)
音頻的播放操作是一個(gè)生產(chǎn)者消費(fèi)者模型,調(diào)用waveOutOpen后,系統(tǒng)會(huì)在后臺(tái)啟動(dòng)一個(gè)播放線程(WinForm程序也可以設(shè)置為使用UI線程)。當(dāng)需要數(shù)據(jù)時(shí),調(diào)用回調(diào)函數(shù),寫入相應(yīng)的數(shù)據(jù)。
首先定義一個(gè)WAVEHDR對(duì)象:
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)); //準(zhǔn)備一個(gè)波形數(shù)據(jù)塊用于播放 waveOutWrite(hwo, &wh, sizeof(WAVEHDR)); //在音頻媒體中播放第二個(gè)函數(shù)wh指定的數(shù)據(jù)
寫入主要是通過兩個(gè)函數(shù)waveOutPrepareHeader和waveOutWrite進(jìn)行。這里有兩個(gè)地方需要注意
- 每次寫入data_size不要太小,太小了會(huì)出現(xiàn)聲音不流暢
- 從它調(diào)用回調(diào)到寫入的時(shí)間間隔不能過長(zhǎng),否則會(huì)出現(xiàn)聲音斷流而出現(xiàn)的噠噠聲。
這兩個(gè)地方的原因?qū)嶋H上都是一個(gè),消費(fèi)者線程沒有足夠的數(shù)據(jù)。要解決這個(gè)問題需要采取緩沖模型,對(duì)數(shù)據(jù)源預(yù)讀。
另外,寫入操作waveOutPrepareHeader和waveOutWrite這兩個(gè)函數(shù)是并不要求一定非要在等待通知后才執(zhí)行的,當(dāng)寫入的速度和播放的速度不一致時(shí),出現(xiàn)聲音快進(jìn)會(huì)慢速播放現(xiàn)象。
關(guān)閉音頻輸出
關(guān)閉音頻輸出只需要使用接口即可。
waveOutClose(hwo);
.net接口封裝
了解各接口功能后,自己封裝一個(gè)也比較簡(jiǎn)單了。用起來也方便多了。
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ū)長(zhǎng)度
/// </summary>
public UInt32 dwBufferLength;
public UInt32 dwBytesRecorded; /* used for input only */
public IntPtr dwUser; /* for client's use */
/// <summary>
/// 設(shè)置標(biāo)志
/// </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>
/// 默認(rèn)設(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)); //準(zhǔn)備一個(gè)波形數(shù)據(jù)塊用于播放
winmm.waveOutWrite(_hwo, _wh, sizeof(winmm.WAVEHDR)); //在音頻媒體中播放第二個(gè)函數(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聲音的詳細(xì)內(nèi)容,更多關(guān)于c# 播放PCM聲音的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
c#不使用windows api函數(shù)打開我的電腦和獲取電腦驅(qū)動(dòng)器信息
這篇文章主要介紹了c#不使用windows api函數(shù)打開我的電腦和電腦驅(qū)動(dòng)器信息的方法,大家參考使用2013-12-12
C#9.0新特性詳解——頂級(jí)程序語句(Top-Level Programs)
這篇文章主要介紹了C#9.0新特性詳解——頂級(jí)程序語句(Top-Level Programs)的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)c#,感興趣的朋友可以了解下2020-12-12
Unity3d實(shí)現(xiàn)Flappy Bird游戲
這篇文章主要為大家詳細(xì)介紹了Unity3d實(shí)現(xiàn)Flappy Bird游戲,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12
C#實(shí)現(xiàn)將XML數(shù)據(jù)自動(dòng)化地寫入Excel文件
在現(xiàn)代企業(yè)級(jí)應(yīng)用中,數(shù)據(jù)處理與報(bào)表生成是核心環(huán)節(jié),本文將深入探討如何利用C#和一款優(yōu)秀的庫,將XML數(shù)據(jù)自動(dòng)化地寫入Excel文件,有需要的小伙伴可以了解下2025-12-12
C# 實(shí)現(xiàn)PPT 每一頁轉(zhuǎn)成圖片過程解析
這篇文章主要介紹了C# 實(shí)現(xiàn)PPT 每一頁轉(zhuǎn)成圖片過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09
C#操作配置文件app.config、web.config增刪改
這篇文章介紹了C#操作配置文件app.config、web.config增刪改的方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05

