赞
踩
在Windows上实现声音播放比较简单的方法是使用winmm,其中的waveOut模块就可以打开声音设备,播放PCM数据。本文将介绍waveOut声音播放的具体实现,其实现相较于waveIn的采集简单很多,不需要通过开启子线程避免死锁,对于消息也只需要监听WOM_DONE。
需要用到的头文件
#include"windows.h"
#include <mmsystem.h>
#pragma comment(lib, "winmm.lib ")
//声音播放对象
HWAVEOUT _waveOut;
//声音数据的缓存
WAVEHDR _wavehdrs[2];
//声音格式
WAVEFORMATEX _waveFormat;
//打开声音播放设备
waveOutOpen
//注册缓冲区
waveOutPrepareHeader
//注销缓冲区
waveOutUnprepareHeader
//缓冲区加入使用
waveOutWrite
//重置数据
waveOutReset
//关闭设备
waveOutClose
整体流程大致如下:
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;
}
由于写入需要使用多个缓存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; } }
将采集功能封装成一个通用工具,方便在任意地方使用。
接口设计如下:
#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; }; }
下面连接的资源包含了上述接口的具体实现,及测试程序和使用示例。
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使用的播放功能还是可以使用的,对于一般的音频文件的播放是满足的,对于实时流则有待验证。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。