文章目录
- 前言
- 一、需要的对象及方法
- 1.对象
- 2.方法
- 二、整体流程
- 三、关键实现
- 1.声音格式
- 2.对象池
- 四、封装成对象
- 1.接口设计
- 2.具体实现
- 五、使用示例
- 总结
前言
在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使用的播放功能还是可以使用的,对于一般的音频文件的播放是满足的,对于实时流则有待验证。

















