使用WaveOut API播放WAV音频文件(解决卡顿)
虽然waveout已经过时,但是其api简单,有些时候也还是需要用到。
其实还是自己上msdn查阅相应api最靠谱,waveout也有提供暂停、设置音量等接口的,这里给个链接,需要的可以自己查找:
https://msdn.microsoft.com/en-us/library/windows/desktop/dd743834(v=vs.85).aspx
waveout播放音频流程:
- 初始化设备并获得句柄,调用waveOutOpen
- 初始化WAVEHDR结构体,包含了要播放的音频数据,调用waveOutPrepareHeader
- 播放WAVEHDR指定的数据,调用waveOutWrite
- 播放结束,调用waveOutClose
函数介绍:
1.waveOutOpen,初始化waveout,指定音频的格式、回调方式等
MMRESULT waveOutOpen(
LPHWAVEOUT phwo,
UINT_PTR uDeviceID,
LPWAVEFORMATEX pwfx,
DWORD_PTR dwCallback,
DWORD_PTR dwCallbackInstance,
DWORD fdwOpen
);
phwo是返回的waveOut的句柄,后面的waveOutWrite等函数都需要传入该句柄。
uDeviceID是播放设备的ID,不知道是什么就填WAVE_MAPPER,系统自动选择。
pwfx传入一个WAVEFORMATEX结构体,该结构体包含了待播放音频的格式(采样率、声道数等)。
dwCallback表示回调方式,因为在调用waveOutWrite之后,系统会在另外一个线程中播放所传入的音频数据,当系统播放完这一段音频后会通过回调的方式来通知我们进行下一步操作。该值可以是:一个waveOutProc回调函数;一个窗口的句柄;一个线程的ID;一个EVENT句柄;NULL。本次我使用的EVENT句柄,网上的代码都是使用回调函数的方式,其实使用EVENT要更方便。
dwCallbackInstance用于在回调之间传递数据,比如说向回调函数传递waveOut的句柄。
fdwOpen标志,一般用于表示dwCallback的类型,比如CALLBACK_FUNCTION表示dwCallback是回调函数、CALLBACK_EVENT表示dwCallback是EVEN句柄。
返回值:返回MMSYSERR_NOERROR表示成功,其他表示失败。
2.waveOutPrepareHeader,初始化一个WAVEHDR结构体
MMRESULT waveOutPrepareHeader(
HWAVEOUT hwo,
LPWAVEHDR pwh,
UINT cbwh
);
hwo为waveOutOpen返回的的句柄。
pwh传入一个WAVEHDR结构体,包括了要播放的音频数据以及相应的一些信息。在调用该函数之前需要设置dwFlags(填0),dwBufferLength(待播放音频数据的长度),lpData(待播放的音频数据)这三个字段。需要注意的是:要确保系统在播放这一段音频的过程中该结构体有效并且不要有改动;音频数据的缓存由自己申请,并且在调用播放函数后系统不会对其进行拷贝,所以在此过程中也不要对该缓存进行改动;在释放lpData的内存前需要调用waveOutUnprepareHeader。
cbwh填sizeof(WAVEHDR)即可。
3.waveOutWrite,播放WAVEHDR中指定的音频数据
MMRESULT waveOutWrite(
HWAVEOUT hwo,
LPWAVEHDR pwh,
UINT cbwh
);
hwo为waveOutOpen返回的的句柄。
pwh传入使用waveOutPrepareHeader初始化过的WAVEHEDR结构体。
cbwh填sizeof(WAVEHDR)即可。
播放的同步:
由于系统播放时是在另一个线程中执行,所以需要用到线程同步相关知识,上面所提到的“回调”就是解决这个问题。本次使用的是EVENT。
EVENT有两种状态:激活、未激活。调用WaitForSingleObject(event, INFINITE)会阻塞进程直至event变为激活状态;调用SetEvent(event)可设置event为激活状态;调用ResetEvent(event)可设置event为未激活状态。当waveout播放完成之后,系统会将我们在waveOutOpen中指定的EVENT置为激活状态。
了解event的机制之后,我们可以这样:①设置event为未激活状态;②调用waveOutWrite播放指定音频;③调用WaitForSingleObject等待event被激活(等待播放完成);④回到第“②”步,如此循环。
避免卡顿:
在播放的时候有很重要的一点:播放的缓存至少需要两个。因为在调用waveOutWirite后系统内核会将其加入“播放队列”,与此同时,还有一个播放线程依次从该队列取出数据并播放,并且每播放完一个节点就会调用上面所说的“回调”。只要保持“播放队列”里面至少有两个节点就不会造成卡顿。
因此,我们只需要在开始播放时调用两次waveOutWrite,然后在“回调”中调用一次waveOutWrite。这样也就保持“播放队列”中(几乎)始终会有两个节点。
为此,我设计了一个类WaveOut,下面是完整代码:
1 #include <windows.h> 2 #pragma comment(lib, "winmm.lib") 3 4 #define MAX_BUFFER_SIZE (1024 * 8 * 16) 5 6 class WaveOut 7 { 8 private: 9 HANDLE hEventPlay; 10 HWAVEOUT hWaveOut; 11 WAVEHDR wvHeader[2]; 12 CHAR* bufCaching; 13 INT bufUsed; 14 INT iCurPlaying; /* index of current playing in 'wvHeader'. */ 15 BOOL hasBegan; 16 public: 17 WaveOut(); 18 ~WaveOut(); 19 int open(DWORD nSamplesPerSec, WORD wBitsPerSample, WORD nChannels); 20 void close(); 21 int push(const CHAR* buf, int size); /* push buffer into 'bufCaching', if fulled, play it. */ 22 int flush(); /* play the buffer in 'bufCaching'. */ 23 private: 24 int play(const CHAR* buf, int size); 25 }; 26 27 WaveOut::WaveOut() : hWaveOut(NULL) 28 { 29 wvHeader[0].dwFlags = 0; 30 wvHeader[1].dwFlags = 0; 31 wvHeader[0].lpData = (CHAR*)malloc(MAX_BUFFER_SIZE); 32 wvHeader[1].lpData = (CHAR*)malloc(MAX_BUFFER_SIZE); 33 wvHeader[0].dwBufferLength = MAX_BUFFER_SIZE; 34 wvHeader[1].dwBufferLength = MAX_BUFFER_SIZE; 35 36 bufCaching = (CHAR*)malloc(MAX_BUFFER_SIZE); 37 hEventPlay = CreateEvent(NULL, FALSE, FALSE, NULL); 38 } 39 WaveOut::~WaveOut() 40 { 41 close(); 42 free(wvHeader[0].lpData); 43 free(wvHeader[1].lpData); 44 free(bufCaching); 45 CloseHandle(hEventPlay); 46 } 47 int WaveOut::open(DWORD nSamplesPerSec, WORD wBitsPerSample, WORD nChannels) 48 { 49 WAVEFORMATEX wfx; 50 51 if (!bufCaching || !hEventPlay || !wvHeader[0].lpData || !wvHeader[1].lpData) 52 { 53 return -1; 54 } 55 56 wfx.wFormatTag = WAVE_FORMAT_PCM; 57 wfx.nChannels = nChannels; 58 wfx.nSamplesPerSec = nSamplesPerSec; 59 wfx.wBitsPerSample = wBitsPerSample; 60 wfx.cbSize = 0; 61 wfx.nBlockAlign = wfx.wBitsPerSample * wfx.nChannels / 8; 62 wfx.nAvgBytesPerSec = wfx.nChannels * wfx.nSamplesPerSec * wfx.wBitsPerSample / 8; 63 64 /* queries the device if it supports the given format.*/ 65 // if (waveOutOpen(NULL, 0, &wfx, NULL, NULL, WAVE_FORMAT_QUERY)) 66 // { 67 // return -1; 68 // } 69 /* 'waveOutOpen' will call 'SetEvent'. */ 70 if (waveOutOpen(&hWaveOut, WAVE_MAPPER, &wfx, (DWORD_PTR)hEventPlay, 0, CALLBACK_EVENT)) 71 { 72 return -1; 73 } 74 75 waveOutPrepareHeader(hWaveOut, &wvHeader[0], sizeof(WAVEHDR)); 76 waveOutPrepareHeader(hWaveOut, &wvHeader[1], sizeof(WAVEHDR)); 77 78 if (!(wvHeader[0].dwFlags & WHDR_PREPARED) || !(wvHeader[1].dwFlags & WHDR_PREPARED)) 79 { 80 return -1; 81 } 82 83 bufUsed = 0; 84 iCurPlaying = 0; 85 hasBegan = 0; 86 87 return 0; 88 } 89 void WaveOut::close() 90 { 91 waveOutUnprepareHeader(hWaveOut, &wvHeader[0], sizeof(WAVEHDR)); 92 waveOutUnprepareHeader(hWaveOut, &wvHeader[1], sizeof(WAVEHDR)); 93 waveOutClose(hWaveOut); 94 hWaveOut = NULL; 95 } 96 int WaveOut::push(const CHAR* buf, int size) 97 { 98 again: 99 if (bufUsed + size < MAX_BUFFER_SIZE) 100 { 101 memcpy(bufCaching + bufUsed, buf, size); 102 bufUsed += size; 103 } 104 else 105 { 106 memcpy(bufCaching + bufUsed, buf, MAX_BUFFER_SIZE - bufUsed); 107 108 if (!hasBegan) 109 { 110 if (0 == iCurPlaying) 111 { 112 memcpy(wvHeader[0].lpData, bufCaching, MAX_BUFFER_SIZE); 113 iCurPlaying = 1; 114 } 115 else 116 { 117 ResetEvent(hEventPlay); 118 memcpy(wvHeader[1].lpData, bufCaching, MAX_BUFFER_SIZE); 119 120 waveOutWrite(hWaveOut, &wvHeader[0], sizeof(WAVEHDR)); 121 waveOutWrite(hWaveOut, &wvHeader[1], sizeof(WAVEHDR)); 122 123 hasBegan = 1; 124 iCurPlaying = 0; 125 } 126 } 127 else if (play(bufCaching, MAX_BUFFER_SIZE) < 0) 128 { 129 return -1; 130 } 131 132 size -= MAX_BUFFER_SIZE - bufUsed; 133 buf += MAX_BUFFER_SIZE - bufUsed; 134 bufUsed = 0; 135 136 if (size > 0) goto again; 137 } 138 return 0; 139 } 140 int WaveOut::flush() 141 { 142 if (bufUsed > 0 && play(bufCaching, bufUsed) < 0) 143 { 144 return -1; 145 } 146 return 0; 147 } 148 int WaveOut::play(const CHAR* buf, int size) 149 { 150 WaitForSingleObject(hEventPlay, INFINITE); 151 152 wvHeader[iCurPlaying].dwBufferLength = size; 153 memcpy(wvHeader[iCurPlaying].lpData, buf, size); 154 155 if (waveOutWrite(hWaveOut, &wvHeader[iCurPlaying], sizeof(WAVEHDR))) 156 { 157 SetEvent(hEventPlay); 158 return -1; 159 } 160 iCurPlaying = !iCurPlaying; 161 162 return 0; 163 }
主函数,从WAV文件读取其采样率、采样位深、声道数并播放该WAV:
1 #include <iostream> 2 #include <fstream> 3 #include "waveout.h" 4 5 int main(int argc, char *argv[]) 6 { 7 char buffer[1000 * 8]; 8 int nRead; 9 unsigned short channels = 1; 10 unsigned long sampleRate = 8000; 11 unsigned short bitsPerSample = 16; 12 std::ifstream ifile("D:\\record\\blow.wav", std::ifstream::binary); 13 WaveOut wvOut; 14 15 if (!ifile) 16 { 17 std::cout << "failed to open file.\n"; 18 return 0; 19 } 20 21 ifile.seekg(22); 22 ifile.read((char*)&channels, 2); 23 ifile.seekg(24); 24 ifile.read((char*)&sampleRate, 4); 25 ifile.seekg(34); 26 ifile.read((char*)&bitsPerSample, 2); 27 ifile.seekg(44); 28 29 std::cout << "sample rate: " << sampleRate 30 << ", channels: " << channels 31 << ", bits per sample: " << bitsPerSample << std::endl; 32 33 if (wvOut.open(sampleRate, bitsPerSample, channels) < 0) 34 { 35 std::cout << "waveout open failed.\n"; 36 return 0; 37 } 38 39 while (ifile.read(buffer, sizeof(buffer))) 40 { 41 nRead = ifile.gcount(); 42 // std::cout << "read " << nRead << " bytes.\n"; 43 if (wvOut.push(buffer, nRead) < 0) 44 std::cout << "play failed.\n"; 45 } 46 if (wvOut.flush() < 0) 47 std::cout << "flush failed\n"; 48 std::cout << "play done.\n"; 49 50 system("pause"); 51 return 0; 52 }
原创文章,转载请注明。