前言
在Windows上实现声音播放比较简单的方法是使用winmm,其中的waveOut模块就可以打开声音设备,播放PCM数据。本文将介绍waveOut声音播放的具体实现,其实现相较于waveIn的采集简单很多,不需要通过开启子线程避免死锁,对于消息也只需要监听WOM_DONE。
一、需要的对象及方法
需要用到的头文件
#include"windows.h"
#include <mmsystem.h>
#pragma comment(lib, "winmm.lib ")
1.对象
//声音播放对象
HWAVEOUT _waveOut;
//声音数据的缓存
WAVEHDR _wavehdrs[2];
//声音格式
WAVEFORMATEX _waveFormat;
2.方法
//打开声音播放设备
waveOutOpen
//注册缓冲区
waveOutPrepareHeader
//注销缓冲区
waveOutUnprepareHeader
//缓冲区加入使用
waveOutWrite
//重置数据
waveOutReset
//关闭设备
waveOutClose
二、整体流程
整体流程大致如下:
三、关键实现
1.声音格式
WAVEFORMATEX WaveInitFormat(WORD nCh, DWORD nSampleRate, WORD bitsPerSample)
{
WAVEFORMATEX waveFormat;
waveFormat.wFormatTag = WAVE_FORMAT_PCM;
waveFormat.nChannels = nCh;
waveFormat.nSamplesPerSec = nSampleRate;
waveFormat.nAvgBytesPerSec = nSampleRate * nCh * bitsPerSample / 8;
waveFormat.nBlockAlign = nCh * bitsPerSample / 8;
waveFormat.wBitsPerSample = bitsPerSample;
waveFormat.cbSize = 0;
return waveFormat;
}
2.对象池
由于写入需要使用多个缓存WAVEHDR,为了不让内存不受控的增长,需要对缓存数量加以限定,这就需要用到对象池的概念了,对象池可以复用固定数量的对象。关于对象池可以参考:《C++ 实现对象池》
(1)初始化
初始化缓存和对象池,如下使用了10个WAVEHDR缓存
ObjectPoolGeneric<WAVEHDR>_opg;
WAVEHDR _wavehdrs[10];
//构造方法:初始化对象池,使用对象池管理_wavehdrs数组,参数:数组对象,数组长度
SoundPlay() :_opg(_wavehdrs,10){
}
(2)申请缓存
写入时需要申请缓存
void Write(unsigned char* data, int length)
{
//_opg为对象池,Applicate方法在对象池中申请一个对象,当对象池为空时会等待,直到有对象才返回。
WAVEHDR* whd = _opg.Applicate(timeoutms);
whd->dwBufferLength = length;
memcpy(whd->lpData, data, length);
waveOutWrite(_waveOut, whd, sizeof(WAVEHDR));
}
(3)归还缓存
播放完成时归还缓存
static void CALLBACK waveOutProc(HWAVEOUT hWaveOut, UINT uMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2)
{
switch (uMsg)
{
case WOM_OPEN:
break;
case WOM_DONE:
{
WAVEHDR* whd = (WAVEHDR*)dwParam1;
//将对象归还给对象池
_this->_opg.ReturnBack(whd);
}
break;
case WOM_CLOSE:
break;
}
}
四、封装成对象
将采集功能封装成一个通用工具,方便在任意地方使用。
1.接口设计
接口设计如下:
#pragma once
#include<string>
#include<functional>
#include<vector>
/************************************************************************
* @Project: AC::SoundPlay
* @Decription: 音频播放工具
* @Verision: v1.0.0.0
* @Author: Xin Nie
* @Create: 2022/1/8 13:05:00
* @LastUpdate: 2022/1/13 17:02:00
************************************************************************
* Copyright @ 2023. All rights reserved.
************************************************************************/
namespace AC {
/// <summary>
/// 声音格式
/// </summary>
class SoundFormat {
public:
/// <summary>
/// 声道数
/// </summary>
int Channels;
/// <summary>
/// 采样率
/// </summary>
int SampleRate;
/// <summary>
/// 位深
/// </summary>
int BitsPerSample;
};
/// <summary>
/// 声音采集设备
/// </summary>
class SoundDevice {
public:
/// <summary>
/// 设备Id
/// </summary>
int Id;
/// <summary>
/// 设备名称
/// </summary>
std::string Name;
/// <summary>
/// 声道数
/// </summary>
int Channels;
/// <summary>
/// 支持的格式
/// </summary>
std::vector<SoundFormat> SupportedFormats;
};
/// <summary>
/// 声音播放对象
/// </summary>
class SoundPlay
{
public:
/// <summary>
/// 错误事件参数参数
/// </summary>
class ErrorEventArgs
{
public:
/// <summary>
/// 错误内容
/// </summary>
std::string Message;
};
/// <summary>
/// 播放开始事件参数参数
/// </summary>
class StartedEventArgs
{
public:
/// <summary>
/// 播放声音数据的格式
/// </summary>
SoundFormat Format;
};
/// <summary>
/// 播放数据到达事件参数
/// </summary>
class DataArrivedEventArgs :public StartedEventArgs
{
public:
/// <summary>
/// 声音数据
/// </summary>
unsigned char* Data;
/// <summary>
/// 数据长度
/// </summary>
int DataLength;
};
/// <summary>
/// 打开事件
/// </summary>
std::function<void(void*, StartedEventArgs*)> Opened;
/// <summary>
/// 播放数据完成事件
/// </summary>
std::function<void(void*, DataArrivedEventArgs*)> DataDone;
/// <summary>
/// 关闭事件
/// </summary>
std::function<void(void*, void*)> Closed;
/// <summary>
/// 错误事件
/// </summary>
std::function<void(void*, ErrorEventArgs*)> Error;
SoundPlay();
SoundPlay(int deviceId);
~SoundPlay();
/// <summary>
/// 打开播放设备
/// </summary>
/// <param name="channels">声道数</param>
/// <param name="sampleRate">采样率</param>
/// <param name="bitsPerSample">位深</param>
bool Open(int channels, int sampleRate, int bitsPerSample);
/// <summary>
/// 关闭
/// 不可以在DataDone事件中调用
/// </summary>
void Close();
/// <summary>
/// 写入数据
/// 如果缓冲区满了则会等待,超时会返回false
/// 不可以在DataDone事件中调用
/// </summary>
/// <param name="data">声音数据</param>
/// <param name="length">数据长度</param>
/// <param name="timeoutms">超时时间,-1为永不超时</param>
/// <returns>是否写入成功</returns>
bool Write(unsigned char*data,int length,int timeoutms=30000);
/// <summary>
/// 获取声道数
/// </summary>
/// <returns>声道数</returns>
int GetChannels();
/// <summary>
/// 获取采样率
/// </summary>
/// <returns>采样率,单位:hz</returns>
int GetSampleRate();
/// <summary>
/// 获取位深
/// </summary>
/// <returns>位深,单位:bits</returns>
int GetBitsPerSample();
/// <summary>
/// 获取设备是否已开启
/// </summary>
/// <returns>是否已开启</returns>
bool GetIsOpened();
/// <summary>
/// 获取当前设备Id
/// </summary>
/// <returns>设备Id</returns>
int GetDeviceId();
/// <summary>
/// 获取声音设备列表
/// </summary>
/// <returns>设备列表</returns>
static std::vector<SoundDevice> GetDeives();
private:
void* _implement;
};
}
2.具体实现
下面连接的资源包含了上述接口的具体实现,及测试程序和使用示例。
https://download.csdn.net/download/u013113678/75702230
五、使用示例
播放wav文件,其中的WavFileReader 对象参考《C++ 读取wav文件中的PCM数据》
#include"WavFileReader.h"
#include"SoundPlay.h"
int main(int argc, char** argv) {
AC::SoundPlay sp;
AC::WavFileReader read;
unsigned char buf[1024];
//打开wav文件
if (read.OpenWavFile("test_music.wav"))
{
//注册事件
sp.Opened = [&](auto s, auto e) {
printf("打开设备:Channels %d SampleRate %d BitsPerSample %d\n", e->Format.Channels, e->Format.SampleRate, e->Format.BitsPerSample);
};
sp.DataDone = [&](auto s, auto e) {
printf("%p数据播放完成:长度%d\n",e->Data,e->DataLength);
};
sp.Closed = [&](auto s, auto e) {
printf("关闭播放\n");
};
sp.Error = [&](auto s, auto e) {
printf("%s\n",e->Message.c_str());
};
//打开设备
sp.Open(read.GetChannels(), read.GetSampleRate(), read.GetBitsPerSample());
int size;
do
{
//读取音频数据
size = read.ReadData(buf, 1024);
if (size > 0)
{
//写入播放设备
sp.Write(buf, size);
}
} while (size);
}
return 0;
}
总结
以上就是今天要讲的内容,使用waveOut实现声音播放,实现过程还是相对较简单的,但还是有些细节需要注意,比如使用对象池管理缓存。waveOut出现死锁的情况较少,所及基本不用特殊实现处理,只需要确保避免一些调用方式即可。总得来说,用waveOut使用的播放功能还是可以使用的,对于一般的音频文件的播放是满足的,对于实时流则有待验证。