c# 通过WinAPI播放PCM声音
在Windows平台上,播放PCM声音使用的API通常有如下两种。
- waveOutandwaveIn:传统的音频MMEAPI,也是使用的最多的
- xAudio2:C++/COMAPI,主要针对游戏开发,是DirectSound的基础
在WindowsVista以后,推出了更加强大的WASAPI ,并用WASAPI封装了MME以及DirectSoundAPI。
对于前面的两个API,在.net平台下有如下封装:
- NAudio
- Sharpdx
WSAPI可能由于更加复杂,没有什么比较完善的封装,codeproject上有篇文章介绍了如何简单的封装WSAPI:RecordingandplayingPCMaudioonWindows8(VB)
最近一个项目中使用到了PCM文件的播放,本来想用NAudio实现的,但使用过程中发现它自己提供的BlockAlignReductionStream播放实时数据是效果不是蛮好(方法可以参考这篇文章),总是有一些卡顿的现象。
究其原因是其Buffer的机制,要求每次都填充满buffer,对于文件播放这个不是问题,但对于实时pcm数据,buffer过大播放的时候得不到足够的数据,buffer过小丢数据的情况。
于是,我便研究了一下微软的MMEAPI,官方文档:UsingWaveformandAuxiliaryAudio。发现MMEAPI也并不复杂,一个简单的示例如下
#include#include #pragmacomment(lib,"winmm.lib") intmain() { constintbuf_size=1024*1024*30; char*buf=newchar[buf_size]; FILE*thbgm;//文件 fopen_s(&thbgm,R"(r:\re_sample.pcm)","rb"); fread(buf,sizeof(char),buf_size,thbgm);//预读取文件 fclose(thbgm); WAVEFORMATEXwfx={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; HANDLEwait=CreateEvent(NULL,0,0,NULL); HWAVEOUThwo; waveOutOpen(&hwo,WAVE_MAPPER,&wfx,(DWORD_PTR)wait,0L,CALLBACK_EVENT);//打开一个给定的波形音频输出装置来进行回放 intdata_size=20480; char*data_ptr=buf; WAVEHDRwh; while(data_ptr-buf 这里是首先预读pcm文件到内存,然后通过事件回调的方式同步写入声音数据。 整个播放过程大概也就用到了五六个API,主要过程如下:
设置音频参数
音频参数定义在一个WAVEFORMATEX对象中,这里只介绍PCM的设置方法,主要设置声道数、采样率、和采样位数。
WAVEFORMATEX wfx = { 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函数打开它。
HWAVEOUT hwo; waveOutOpen(&hwo, WAVE_MAPPER, &wfx, (DWORD_PTR)wait, 0L, CALLBACK_EVENT);这个函数前三个参数分别是波形对象,输出设备(WAVE_MAPPER为-1,表示默认输出设备),音频参数。后面三个参数分别是回调相关参数,因为音频数据一次只写入一小段,播放是由系统在另一个线程中进行的,当数据播放完成后,需要通过回调的方式通知写入新数据。
MMEAPI支持多种回调方式。具体参看MSDN文档:waveOutOpenfunction。具体常见的回调方式有如下几种:
- CALLBACK_NULL 不回调,需要主动掌握写入数据时机,常用于实时音频流
- CALLBACK_EVENT 需要数据时写事件,在另外一个独立的线程上等待该事件写入数据
- CALLBACK_FUNCTION 需要数据时执行回调函数,在回调函数中写入数据
这里是示例通过事件的方式回调的
写入音频数据
音频的播放操作是一个生产者消费者模型,调用waveOutOpen后,系统会在后台启动一个播放线程(WinForm程序也可以设置为使用UI线程)。当需要数据时,调用回调函数,写入相应的数据。
首先定义一个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)); //准备一个波形数据块用于播放 waveOutWrite(hwo, &wh, sizeof(WAVEHDR)); //在音频媒体中播放第二个函数wh指定的数据写入主要是通过两个函数waveOutPrepareHeader和waveOutWrite进行。这里有两个地方需要注意
- 每次写入data_size不要太小,太小了会出现声音不流畅
- 从它调用回调到写入的时间间隔不能过长,否则会出现声音断流而出现的哒哒声。
这两个地方的原因实际上都是一个,消费者线程没有足够的数据。要解决这个问题需要采取缓冲模型,对数据源预读。
另外,写入操作waveOutPrepareHeader和waveOutWrite这两个函数是并不要求一定非要在等待通知后才执行的,当写入的速度和播放的速度不一致时,出现声音快进会慢速播放现象。
关闭音频输出
关闭音频输出只需要使用接口即可。
waveOutClose(hwo);.net接口封装
了解各接口功能后,自己封装一个也比较简单了。用起来也方便多了。
WinAPI封装:
usingHWAVEOUT=IntPtr; classwinmm { [StructLayout(LayoutKind.Sequential)] publicstructWAVEFORMATEX { //////波形声音的格式 /// publicWaveFormatwFormatTag; //////音频文件的通道数量 /// publicUInt16nChannels;/*numberofchannels(i.e.mono,stereo...)*/ //////采样频率 /// publicUInt32nSamplesPerSec;/*samplerate*/ //////每秒缓冲区 /// publicUInt32nAvgBytesPerSec;/*forbufferestimation*/ publicUInt16nBlockAlign;/*blocksizeofdata*/ publicUInt16wBitsPerSample;/*numberofbitspersampleofmonodata*/ publicUInt16cbSize;/*thecountinbytesofthesizeof*/ } [StructLayout(LayoutKind.Sequential)] publicstructWAVEHDR { //////缓冲区指针 /// publicIntPtrlpData; //////缓冲区长度 /// publicUInt32dwBufferLength; publicUInt32dwBytesRecorded;/*usedforinputonly*/ publicIntPtrdwUser;/*forclient'suse*/ //////设置标志 /// publicUInt32dwFlags; //////循环控制 /// publicUInt32dwLoops; //////保留字段 /// publicIntPtrlpNext; //////保留字段 /// publicIntPtrreserved; } [Flags] publicenumWaveOpenFlags { CALLBACK_NULL=0, CALLBACK_FUNCTION=0x30000, CALLBACK_EVENT=0x50000, CallbackWindow=0x10000, CallbackThread=0x20000, } publicenumWaveMessage { WIM_OPEN=0x3BE, WIM_CLOSE=0x3BF, WIM_DATA=0x3C0, WOM_CLOSE=0x3BC, WOM_DONE=0x3BD, WOM_OPEN=0x3BB } [Flags] publicenumWaveHeaderFlags { WHDR_BEGINLOOP=0x00000004, WHDR_DONE=0x00000001, WHDR_ENDLOOP=0x00000008, WHDR_INQUEUE=0x00000010, WHDR_PREPARED=0x00000002 } publicenumWaveFormat:ushort { WAVE_FORMAT_PCM=0x0001, } //////默认设备 /// publicstaticIntPtrWAVE_MAPPER{get;}=(IntPtr)(-1); publicdelegatevoidWaveCallback(IntPtrhWaveOut,WaveMessagemessage,IntPtrdwInstance,WAVEHDRwavhdr, IntPtrdwReserved); [DllImport("winmm.dll")] publicstaticexternintwaveOutOpen(outHWAVEOUThWaveOut,IntPtruDeviceID,inWAVEFORMATEXlpFormat, WaveCallbackdwCallback,IntPtrdwInstance,WaveOpenFlagsdwFlags); [DllImport("winmm.dll")] publicstaticexternintwaveOutOpen(outHWAVEOUThWaveOut,IntPtruDeviceID,inWAVEFORMATEXlpFormat, IntPtrdwCallback,IntPtrdwInstance,WaveOpenFlagsdwFlags); [DllImport("winmm.dll")] publicstaticexternintwaveOutSetVolume(HWAVEOUThwo,ushortdwVolume); [DllImport("winmm.dll")] publicstaticexternintwaveOutClose(inHWAVEOUThWaveOut); [DllImport("winmm.dll")] publicstaticexternintwaveOutPrepareHeader(HWAVEOUThWaveOut,inWAVEHDRlpWaveOutHdr,intuSize); [DllImport("winmm.dll")] publicstaticexternintwaveOutUnprepareHeader(HWAVEOUThWaveOut,inWAVEHDRlpWaveOutHdr,intuSize); [DllImport("winmm.dll")] publicstaticexternintwaveOutWrite(HWAVEOUThWaveOut,inWAVEHDRlpWaveOutHdr,intuSize); } classkernel32 { [DllImport("kernel32.dll")] publicstaticexternIntPtrCreateEvent(IntPtrlpEventAttributes,boolbManualReset,boolbInitialState,stringlpName); [DllImport("kernel32.dll")] publicstaticexternintWaitForSingleObject(IntPtrhHandle,intdwMilliseconds); [DllImport("kernel32.dll")] publicstaticexternboolCloseHandle(IntPtrhHandle); }PCM播放器:
//////Pcm播放器 /// publicunsafeclassPcmPlayer { ///声道数目 /// 采样频率 /// 采样大小(bits) publicPcmPlayer(intchannels,intsampleRate,intsampleSize) { _wfx=newwinmm.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; /// ///以事件回调的方式打开设备 /// ///publicvoidOpenEvent(IntPtrwaitEvent) { winmm.waveOutOpen(out_hwo,winmm.WAVE_MAPPER,_wfx,waitEvent,IntPtr.Zero,winmm.WaveOpenFlags.CALLBACK_EVENT); Debug.Assert(_hwo!=IntPtr.Zero); } publicvoidOpenNone() { winmm.waveOutOpen(out_hwo,winmm.WAVE_MAPPER,_wfx,IntPtr.Zero,IntPtr.Zero,winmm.WaveOpenFlags.CALLBACK_NULL); Debug.Assert(_hwo!=IntPtr.Zero); } winmm.WAVEHDR_wh; publicvoidWriteData(ReadOnlyMemory buffer) { varhwnd=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,_wh,sizeof(winmm.WAVEHDR));//在音频媒体中播放第二个函数wh指定的数据 hwnd.Dispose(); } publicvoidDispose() { winmm.waveOutPrepareHeader(_hwo,_wh,sizeof(winmm.WAVEHDR)); winmm.waveOutClose(_hwo); _hwo=IntPtr.Zero; } } publicclassWaitObject:IDisposable { publicIntPtrHwnd{get;set;} publicWaitObject() { Hwnd=kernel32.CreateEvent(IntPtr.Zero,false,false,null); } publicvoidWait() { kernel32.WaitForSingleObject(Hwnd,-1); } publicvoidDispose() { kernel32.CloseHandle(Hwnd); Hwnd=IntPtr.Zero; } } 以上就是c#通过WinAPI播放PCM声音的详细内容,更多关于c#播放PCM声音的资料请关注毛票票其它相关文章!