双缓冲方法实现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种死法, 及其解决方案

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值