双缓冲方法实现waveOut的连续无卡顿播放以及即收即放
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];
//清空WAVEHDR
ZeroMemory(&wavehdr, sizeof(WAVEHDR));
//设置WAVEHDR
wavehdr.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 & prepareHeader
void Start();
//填入数据,若当前没有空余缓冲区,则先阻塞,填充后返回
//write
void PlayAudio(char* buf, unsigned int nSize);
//reset -> 等待线程播放完成并退出 -> unprepare -> close
void 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 & close
void Close();
inline void WaitForPlayingEnd();
inline void SetThreadSymbol(bool running);
//若传入的指针指向wavehdr1,则将isplaying1设为false
inline 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];
//清空WAVEHDR
ZeroMemory(&wavehdr1, sizeof(WAVEHDR));
ZeroMemory(&wavehdr2, sizeof(WAVEHDR));
//设置WAVEHDR
wavehdr1.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 _PRINT
printf("PlayAudio::waitting\n");
#endif
continue;
}
else
{
//一旦出现任意一个可写入,则break
#ifdef _PRINT
printf("PlayAudio::break\n");
#endif
break;
}
}
//将没有在播放的hdr设为当前hdr
char* 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 _PRINT
printf("ThreadProc::enter\n");
#endif
WaveOut* 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 _PRINT
printf("ThreadProc::exit\n");
#endif
return 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 _PRINT
printf("StopThread::waiting\n");
#endif
Sleep(1);
}
else
{
#ifdef _PRINT
printf("StopThread::break\n");
#endif
break;
}
}
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 {打开选项}// long120823
MMRESULT 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 _PRINT
printf("Stop::waitting\n");
#endif
Sleep(1);
}
else
{
#ifdef _PRINT
printf("Stop::break\n");
#endif
break;
}
}
}
void WaveOut::Stop()
{
//先reset
MMRESULT 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 _PRINT
printf("1 is finished.\n");
#endif
}
else
{
isplaying2 = false;
#ifdef _PRINT
printf("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种死法, 及其解决方案