1. waveOut基本使用方法
waveOut是一套历史悠久的Windows音频API,虽然古老,但至今仍运行良好,且支持老旧系统(原生支持Windows XP)。
waveOut虽然不像DirectSound那样自带混音功能,但也可以通过同时开多个播放线程实现同时播放多个声音的目的,达到事实上的混音效果。(关于waveOut混音效果可以看我的一个视频演示:FC音乐格式nsf多线程播放程序演示)
waveOut的使用一般遵循Open - Prepare - Write - Reset - Unprepare - Close的步骤。本文提到的函数均省略前缀waveOut,例如Write实际上指的是waveOutWrite函数。

如上图所示。waveOut在播放音频期间不能调用Unprepare, Write等函数,如要暂停播放可使用Pause函数,或者Reset函数。播放期间调用Reset会使声音立即停止,接着调用Prepare, Write函数,可以再次播放。
通过此法可以实现《帝国时代2》中“伐伐伐伐伐木工”的效果。
WaveOutEffect.h:
#pragma once#include <Windows.h>
#include <mmsystem.h>class WaveOutEffect
{
public:WaveOutEffect(PWAVEFORMATEX pWaveformat, int buf_ms = 80);~WaveOutEffect();void PlayAudio(char* in_buf, unsigned int in_bufsize);
private:char* buf;unsigned int buf_size;WAVEFORMATEX m_Waveformat;WAVEHDR wavehdr;HWAVEOUT m_hWaveOut;MMRESULT mRet;void Open();void Reset();
};
WaveOutEffect.cpp:
#include "WaveOutEffect.h"#include "tstring.h"#pragma comment(lib,"winmm.lib")#define _PRINT
#include <stdexcept>using namespace std;WaveOutEffect::WaveOutEffect(PWAVEFORMATEX pWaveformat, int buf_ms) :m_hWaveOut(0)
{//初始化音频格式memcpy(&m_Waveformat, pWaveformat, sizeof(WAVEFORMATEX));m_Waveformat.nBlockAlign = (m_Waveformat.wBitsPerSample * m_Waveformat.nChannels) >> 3;m_Waveformat.nAvgBytesPerSec = m_Waveformat.nBlockAlign * m_Waveformat.nSamplesPerSec;//分配缓冲区buf_size = buf_ms * m_Waveformat.nSamplesPerSec * m_Waveformat.nBlockAlign / 1000;buf = new char[buf_size];//清空WAVEHDRZeroMemory(&wavehdr, sizeof(WAVEHDR));//设置WAVEHDRwavehdr.lpData = buf;wavehdr.dwBufferLength = buf_size;Open();
}WaveOutEffect::~WaveOutEffect()
{Reset();mRet = waveOutUnprepareHeader(m_hWaveOut, &wavehdr, sizeof(WAVEHDR));if (mRet != MMSYSERR_NOERROR){TCHAR info[260];waveOutGetErrorText(mRet, info, 260);throw runtime_error(to_string(info));}mRet = waveOutClose(m_hWaveOut);if (mRet != MMSYSERR_NOERROR){throw runtime_error("waveOutClose fail");}delete[] buf;
}void WaveOutEffect::PlayAudio(char* in_buf, unsigned int in_bufsize)
{if (in_bufsize > buf_size)//传入buf大于内置缓冲区,抛出异常{throw runtime_error("input buffer size is bigger than self");}if (in_bufsize <= buf_size){wavehdr.dwBufferLength = in_bufsize;}memcpy(buf, in_buf, in_bufsize);Reset();mRet = waveOutPrepareHeader(m_hWaveOut, &wavehdr, sizeof(WAVEHDR));if (mRet != MMSYSERR_NOERROR){TCHAR info[260];waveOutGetErrorText(mRet, info, 260);throw runtime_error(to_string(info));}mRet = waveOutWrite(m_hWaveOut, &wavehdr, sizeof(WAVEHDR));if (mRet != MMSYSERR_NOERROR){TCHAR info[260];waveOutGetErrorText(mRet, info, 260);throw runtime_error(to_string(info));}
}void WaveOutEffect::Open()
{mRet = waveOutOpen(&m_hWaveOut, WAVE_MAPPER, &m_Waveformat, NULL, 0, CALLBACK_NULL);if (mRet != MMSYSERR_NOERROR){throw runtime_error("waveOutOpen fail");}
}void WaveOutEffect::Reset()
{mRet = waveOutReset(m_hWaveOut);if (mRet != MMSYSERR_NOERROR){TCHAR info[260];waveOutGetErrorText(mRet, info, 260);throw runtime_error(to_string(info));}
}
2. 双缓冲实现waveOut的即取即放
如果我们要播放一大段音频怎么办?或者音频数据是以流的方式获得,比如网络中接收到的,怎么办?
一大段音频倒是可以一次性分配几十MB内存,全部加载,一股脑地都用waveOutWrite调用了。但音频流就没有办法了。
网络上流传很广的一篇,由英文翻译得到的waveOut教程,上面使用了20个数组轮流送进waveOut的方法,我个人觉得很不直观,又繁琐。
但waveOut的线程控制较为复杂,稍不注意就会触发死锁。这方面更深入的剖析可以看参考文献2。直到我看到参考文献1,才得到真正的解决方法。
2.1 播放

如上图所示,我首先使用了两个WAVEHDR,分别掌管2个缓冲区。然后开辟了一个线程,每当一个缓冲区播放完成时,线程2就将该缓冲区标记为“未播放”,每次调用PlayAudio时,都先寻找空闲的缓冲区,没有则阻塞,直到空闲缓冲区出现,则使用Write将当前音频片段送入缓冲待播放。因为两个WAVEHDR绑定到同一个HWAVEOUT,所以系统内核在一个缓冲播放完成时会自动接着放另一个,达到时间上的连续播放。
2.2 停止

停止功能如上图所示。首先调用Reset,让内核强制停止。然后阻塞等待playing1和playing2全部为false,确保没有音频正在播放。然后发送WM_QUIT至线程2,再阻塞等待线程2退出。之后就可以调用Unprepare和Close正常结束了。
在使用时,由于PlayAudio内部会自动阻塞,所以只要单独开一个线程,无脑地往PlayAudio里怼 读取/解码/接收 到的任何音频数据就可以了。机制会保证播放的连续性,不会出现“伐伐伐木工”这种情况。
下附代码。
WaveOut.h:
#pragma once#include <Windows.h>
#include <mmsystem.h>class WaveOut
{
public:WaveOut(PWAVEFORMATEX pWaveformat,int buf_ms=80);~WaveOut();//open & prepareHeadervoid Start();//填入数据,若当前没有空余缓冲区,则先阻塞,填充后返回//writevoid PlayAudio(char* buf, unsigned int nSize);//reset -> 等待线程播放完成并退出 -> unprepare -> closevoid Stop();
private:char* buf1,*buf2;unsigned int buf_size;bool isplaying1, isplaying2;bool thread_is_running;HANDLE m_hThread;DWORD m_ThreadID;BOOL m_bDevOpen;HWAVEOUT m_hWaveOut;int m_BufferQueue;WAVEFORMATEX m_Waveformat;WAVEHDR wavehdr1,wavehdr2;CRITICAL_SECTION m_Lock;static DWORD WINAPI ThreadProc(LPVOID lpParameter);void StartThread();void StopThread();void Open();//unprepare & closevoid Close();inline void WaitForPlayingEnd();inline void SetThreadSymbol(bool running);//若传入的指针指向wavehdr1,则将isplaying1设为falseinline void SetFinishSymbol(PWAVEHDR pWaveHdr);
};
WaveOut.cpp:
#include "WaveOut.h"
#include "tstring.h"#pragma comment(lib,"winmm.lib")#define _PRINT
#include <stdexcept>using namespace std;WaveOut::WaveOut(PWAVEFORMATEX pWaveformat, int buf_ms) :thread_is_running(false), m_hThread(0), m_ThreadID(0), m_bDevOpen(false), m_hWaveOut(0), m_BufferQueue(0), isplaying1(false), isplaying2(false)
{//初始化音频格式memcpy(&m_Waveformat, pWaveformat, sizeof(WAVEFORMATEX));m_Waveformat.nBlockAlign = (m_Waveformat.wBitsPerSample * m_Waveformat.nChannels) >> 3;m_Waveformat.nAvgBytesPerSec = m_Waveformat.nBlockAlign * m_Waveformat.nSamplesPerSec;//分配缓冲区buf_size = buf_ms * m_Waveformat.nSamplesPerSec * m_Waveformat.nBlockAlign / 1000;buf1 = new char[buf_size];buf2 = new char[buf_size];//清空WAVEHDRZeroMemory(&wavehdr1, sizeof(WAVEHDR));ZeroMemory(&wavehdr2, sizeof(WAVEHDR));//设置WAVEHDRwavehdr1.lpData = buf1;wavehdr1.dwBufferLength = buf_size;wavehdr2.lpData = buf2;wavehdr2.dwBufferLength = buf_size;InitializeCriticalSection(&m_Lock);}WaveOut::~WaveOut()
{WaitForPlayingEnd();StopThread();Close();delete[] buf1;delete[] buf2;DeleteCriticalSection(&m_Lock);
}void WaveOut::Start()
{StartThread();try{Open();}catch (runtime_error e){StopThread();throw e;}
}void WaveOut::PlayAudio(char* in_buf, unsigned int in_size)
{if (!m_bDevOpen){throw runtime_error("waveOut has not been opened");}//等待出现可写入缓存while (1){if (isplaying1 && isplaying2)//都不可写入,继续等待{Sleep(10);
#ifdef _PRINTprintf("PlayAudio::waitting\n");
#endifcontinue;}else{//一旦出现任意一个可写入,则break
#ifdef _PRINTprintf("PlayAudio::break\n");
#endifbreak;}}//将没有在播放的hdr设为当前hdrchar* now_buf=nullptr;WAVEHDR* now_wavehdr = nullptr;bool* now_playing = nullptr;if (isplaying1 == false){now_buf = buf1;now_wavehdr = &wavehdr1;now_playing = &isplaying1;}if (isplaying2 == false){now_buf = buf2;now_wavehdr = &wavehdr2;now_playing = &isplaying2;}if (in_size > buf_size)//传入buf大于内置缓冲区,抛出异常{throw runtime_error("input buffer size is bigger than self");}if (in_size <= buf_size){now_wavehdr->dwBufferLength = in_size;}memcpy(now_buf, in_buf, in_size);if (waveOutWrite(m_hWaveOut, now_wavehdr, sizeof(WAVEHDR)) != MMSYSERR_NOERROR){throw runtime_error("waveOutWrite fail");}EnterCriticalSection(&m_Lock);*now_playing = true;LeaveCriticalSection(&m_Lock);}DWORD __stdcall WaveOut::ThreadProc(LPVOID lpParameter)
{
#ifdef _PRINTprintf("ThreadProc::enter\n");
#endifWaveOut* pWaveOut = (WaveOut*)lpParameter;pWaveOut->SetThreadSymbol(true);MSG msg;while (GetMessage(&msg, 0, 0, 0)){switch (msg.message){case WOM_OPEN:break;case WOM_CLOSE:break;case WOM_DONE://标记完成符号WAVEHDR* pWaveHdr = (WAVEHDR*)msg.lParam;pWaveOut->SetFinishSymbol(pWaveHdr);break;}}pWaveOut->SetThreadSymbol(false);
#ifdef _PRINTprintf("ThreadProc::exit\n");
#endifreturn msg.wParam;
}void WaveOut::StartThread()
{if (thread_is_running){throw runtime_error("thread has been running");}m_hThread = CreateThread(0, 0, ThreadProc, this, 0, &m_ThreadID);if (!m_hThread){throw runtime_error("CreateThread fail");}
}void WaveOut::StopThread()
{if (!thread_is_running){return;}if (m_hThread){PostThreadMessage(m_ThreadID, WM_QUIT, 0, 0);while (1){if (thread_is_running){
#ifdef _PRINTprintf("StopThread::waiting\n");
#endifSleep(1);}else{
#ifdef _PRINTprintf("StopThread::break\n");
#endifbreak;}}TerminateThread(m_hThread, 0);m_hThread = 0;}
}void WaveOut::Open()
{if (m_bDevOpen){throw runtime_error("waveOut has been opened");}//lphWaveOut: PHWaveOut; {用于返回设备句柄的指针; 如果 dwFlags=WAVE_FORMAT_QUERY, 这里应是 nil}//uDeviceID: UINT; {设备ID; 可以指定为: WAVE_MAPPER, 这样函数会根据给定的波形格式选择合适的设备}//lpFormat: PWaveFormatEx; {TWaveFormat 结构的指针; TWaveFormat 包含要申请的波形格式}//dwCallback: DWORD {回调函数地址或窗口句柄; 若不使用回调机制, 设为 nil}//dwInstance: DWORD {给回调函数的实例数据; 不用于窗口}//dwFlags: DWORD {打开选项}// long120823MMRESULT mRet;mRet = waveOutOpen(0, WAVE_MAPPER, &m_Waveformat, 0, 0, WAVE_FORMAT_QUERY);if (mRet != MMSYSERR_NOERROR){throw runtime_error("waveOutOpen fail");}mRet = waveOutOpen(&m_hWaveOut, WAVE_MAPPER, &m_Waveformat, m_ThreadID, 0, CALLBACK_THREAD);if (mRet != MMSYSERR_NOERROR){throw runtime_error("waveOutOpen fail");}if (waveOutPrepareHeader(m_hWaveOut, &wavehdr1, sizeof(WAVEHDR)) != MMSYSERR_NOERROR){throw runtime_error("waveOutPrepareHeader fail");}if (waveOutPrepareHeader(m_hWaveOut, &wavehdr2, sizeof(WAVEHDR)) != MMSYSERR_NOERROR){throw runtime_error("waveOutPrepareHeader fail");}m_bDevOpen = TRUE;
}void WaveOut::Close()
{if (!m_bDevOpen){return;}if (!m_hWaveOut){return;}MMRESULT mRet;if ((mRet = waveOutUnprepareHeader(m_hWaveOut, &wavehdr1, sizeof(WAVEHDR))) != MMSYSERR_NOERROR){TCHAR info[260];waveOutGetErrorText(mRet, info, 260);throw runtime_error(to_string(info));}if ((mRet = waveOutUnprepareHeader(m_hWaveOut, &wavehdr2, sizeof(WAVEHDR))) != MMSYSERR_NOERROR){TCHAR info[260];waveOutGetErrorText(mRet, info, 260);throw runtime_error(to_string(info));}mRet = waveOutClose(m_hWaveOut);if (mRet != MMSYSERR_NOERROR){throw runtime_error("waveOutClose fail");}m_hWaveOut = 0;m_bDevOpen = FALSE;
}inline void WaveOut::WaitForPlayingEnd()
{while (1){if (isplaying1 || isplaying2){
#ifdef _PRINTprintf("Stop::waitting\n");
#endifSleep(1);}else{
#ifdef _PRINTprintf("Stop::break\n");
#endifbreak;}}
}void WaveOut::Stop()
{//先resetMMRESULT mRet;if ((mRet = waveOutReset(m_hWaveOut)) != MMSYSERR_NOERROR){TCHAR info[260];waveOutGetErrorText(mRet, info, 260);throw runtime_error(to_string(info));}//等待播放完成WaitForPlayingEnd();//向线程发送关闭信号,阻塞直到线程退出StopThread();Close();
}inline void WaveOut::SetThreadSymbol(bool running)
{EnterCriticalSection(&m_Lock);thread_is_running = running;LeaveCriticalSection(&m_Lock);
}inline void WaveOut::SetFinishSymbol(PWAVEHDR pWaveHdr)
{EnterCriticalSection(&m_Lock);if (pWaveHdr == &wavehdr1){isplaying1 = false;
#ifdef _PRINTprintf("1 is finished.\n");
#endif}else{isplaying2 = false;
#ifdef _PRINTprintf("2 is finished.\n");
#endif}LeaveCriticalSection(&m_Lock);
}
本文代码一部分参考自文献1。
代码中的tstring.h是我自己写的一个文件,主要内容是兼容Unicode和ANSI的映射宏。读者删掉对应#define行,TCHAR改成char,to_string去掉,就可以在ANSI编码上正常使用了。
参考
[1] 打造自己的wave音频播放器-使用waveOutOpen与waveOutWrite实现
[2] waveOutReset的N种死法, 及其解决方案
















