C++ 使用waveOut实现声音播放

24 篇文章 3 订阅


前言

在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使用的播放功能还是可以使用的,对于一般的音频文件的播放是满足的,对于实时流则有待验证。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodeOfCC

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值