c# 通過(guò)WinAPI播放PCM聲音
在Windows平臺(tái)上,播放PCM聲音使用的API通常有如下兩種。
- waveOut and waveIn:傳統(tǒng)的音頻MMEAPI,也是使用的最多的
- xAudio2:C++/COM API,主要針對(duì)游戲開(kāi)發(fā),是DirectSound的基礎(chǔ)
在Windows Vista以后,推出了更加強(qiáng)大的WASAPI ,并用WASAPI封裝了MME以及DirectSound API。
對(duì)于前面的兩個(gè)API,在.net平臺(tái)下有如下封裝:
WSAPI可能由于更加復(fù)雜,沒(méi)有什么比較完善的封裝,codeproject上有篇文章介紹了如何簡(jiǎn)單的封裝WSAPI: Recording and playing PCM audio on Windows 8 (VB)
最近一個(gè)項(xiàng)目中使用到了PCM文件的播放,本來(lái)想用NAudio實(shí)現(xiàn)的,但使用過(guò)程中發(fā)現(xiàn)它自己提供的BlockAlignReductionStream播放實(shí)時(shí)數(shù)據(jù)是效果不是蠻好(方法可以參考這篇文章),總是有一些卡頓的現(xiàn)象。
究其原因是其Buffer的機(jī)制,要求每次都填充滿buffer,對(duì)于文件播放這個(gè)不是問(wèn)題,但對(duì)于實(shí)時(shí)pcm數(shù)據(jù),buffer過(guò)大播放的時(shí)候得不到足夠的數(shù)據(jù),buffer過(guò)小丟數(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); //打開(kāi)一個(gè)給定的波形音頻輸出裝置來(lái)進(jìn)行回放
int data_size = 20480;
char* data_ptr = buf;
WAVEHDR wh;
while (data_ptr - buf < buf_size)
{
//這一部分需要特別注意的是在循環(huán)回來(lái)之后不能花太長(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)存,然后通過(guò)事件回調(diào)的方式同步寫(xiě)入聲音數(shù)據(jù)。 整個(gè)播放過(guò)程大概也就用到了五六個(gè)API,主要過(guò)程如下:
設(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
打開(kāi)音頻輸出
打開(kāi)音頻輸出需要定義一個(gè)HWAVEOUT對(duì)象,它代表一個(gè)波形對(duì)象,通過(guò)waveOutOpen函數(shù)打開(kāi)它。
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ù)一次只寫(xiě)入一小段,播放是由系統(tǒng)在另一個(gè)線程中進(jìn)行的,當(dāng)數(shù)據(jù)播放完成后,需要通過(guò)回調(diào)的方式通知寫(xiě)入新數(shù)據(jù)。
MMEAPI支持多種回調(diào)方式。具體參看MSDN文檔: waveOutOpen function。具體常見(jiàn)的回調(diào)方式有如下幾種:
- CALLBACK_NULL 不回調(diào),需要主動(dòng)掌握寫(xiě)入數(shù)據(jù)時(shí)機(jī),常用于實(shí)時(shí)音頻流
- CALLBACK_EVENT 需要數(shù)據(jù)時(shí)寫(xiě)事件,在另外一個(gè)獨(dú)立的線程上等待該事件寫(xiě)入數(shù)據(jù)
- CALLBACK_FUNCTION 需要數(shù)據(jù)時(shí)執(zhí)行回調(diào)函數(shù),在回調(diào)函數(shù)中寫(xiě)入數(shù)據(jù)
這里是示例通過(guò)事件的方式回調(diào)的
寫(xiě)入音頻數(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ù),寫(xiě)入相應(yīng)的數(shù)據(jù)。
首先定義一個(gè)WAVEHDR對(duì)象:
int data_size = 20480; char* data_ptr = buf; WAVEHDR wh;
每次寫(xiě)入的操作過(guò)程如下:
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ù)
寫(xiě)入主要是通過(guò)兩個(gè)函數(shù)waveOutPrepareHeader和waveOutWrite進(jìn)行。這里有兩個(gè)地方需要注意
- 每次寫(xiě)入data_size不要太小,太小了會(huì)出現(xiàn)聲音不流暢
- 從它調(diào)用回調(diào)到寫(xiě)入的時(shí)間間隔不能過(guò)長(zhǎng),否則會(huì)出現(xiàn)聲音斷流而出現(xiàn)的噠噠聲。
這兩個(gè)地方的原因?qū)嶋H上都是一個(gè),消費(fèi)者線程沒(méi)有足夠的數(shù)據(jù)。要解決這個(gè)問(wèn)題需要采取緩沖模型,對(duì)數(shù)據(jù)源預(yù)讀。
另外,寫(xiě)入操作waveOutPrepareHeader和waveOutWrite這兩個(gè)函數(shù)是并不要求一定非要在等待通知后才執(zhí)行的,當(dāng)寫(xiě)入的速度和播放的速度不一致時(shí),出現(xiàn)聲音快進(jìn)會(huì)慢速播放現(xiàn)象。
關(guān)閉音頻輸出
關(guān)閉音頻輸出只需要使用接口即可。
waveOutClose(hwo);
.net接口封裝
了解各接口功能后,自己封裝一個(gè)也比較簡(jiǎn)單了。用起來(lái)也方便多了。
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)的方式打開(kāi)設(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# 通過(guò)WinAPI播放PCM聲音的詳細(xì)內(nèi)容,更多關(guān)于c# 播放PCM聲音的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- C# 調(diào)用WebApi的實(shí)現(xiàn)
- c# 調(diào)用Win32Api關(guān)閉當(dāng)前應(yīng)用的方法
- c# webapi 配置swagger的方法
- C#調(diào)用Win32的API函數(shù)--User32.dll
- C#調(diào)用新浪微博API實(shí)例代碼
- C#調(diào)用百度API實(shí)現(xiàn)活體檢測(cè)的方法
- C# WebApi Get請(qǐng)求方式傳遞實(shí)體參數(shù)的方法示例
- C#凈化版WebApi框架的實(shí)現(xiàn)
- C# WebApi 路由機(jī)制剖析
- C# WebApi 接口傳參詳解
- c#在WebAPI使用Session的方法
- c# 常見(jiàn)文件路徑Api的使用示例
相關(guān)文章
c#之圓形無(wú)標(biāo)題欄橢圓窗體的實(shí)現(xiàn)詳解
本篇文章是對(duì)c#中圓形無(wú)標(biāo)題欄橢圓窗體的實(shí)現(xiàn)方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06
使用C#實(shí)現(xiàn)讀取PDF中所有文本內(nèi)容
這篇文章主要為大家詳細(xì)介紹了如何使用C#實(shí)現(xiàn)讀取PDF中所有文本內(nèi)容,文中的示例代碼簡(jiǎn)潔易懂,具有一定的學(xué)習(xí)價(jià)值,有需要的小伙伴可以了解下2024-02-02
淺談C#單例模式的實(shí)現(xiàn)和性能對(duì)比
這篇文章主要介紹了淺談C#單例模式的實(shí)現(xiàn)和性能對(duì)比的相關(guān)資料,詳細(xì)的介紹了6種實(shí)現(xiàn)方式,需要的朋友可以參考下2017-09-09
C# Ado.net實(shí)現(xiàn)讀取SQLServer數(shù)據(jù)庫(kù)存儲(chǔ)過(guò)程列表及參數(shù)信息示例
這篇文章主要介紹了C# Ado.net實(shí)現(xiàn)讀取SQLServer數(shù)據(jù)庫(kù)存儲(chǔ)過(guò)程列表及參數(shù)信息,結(jié)合實(shí)例形式總結(jié)分析了C#針對(duì)SQLServer數(shù)據(jù)庫(kù)存儲(chǔ)過(guò)程及參數(shù)信息的各種常見(jiàn)操作技巧,需要的朋友可以參考下2019-02-02
C#入門學(xué)習(xí)之集合、比較和轉(zhuǎn)換
本文詳細(xì)講解了C#中的集合、比較和轉(zhuǎn)換,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05
Unity UGUI的RectMask2D遮罩組件的介紹使用
這篇文章主要為大家介紹了Unity UGUI的RectMask2D遮罩組件的介紹使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07

