DirectSound播放实时音频

本文详细介绍了在DirectSound中如何处理实时音频播放时的缓冲问题,以确保播放的实时性和避免延迟或重复播放。通过创建一个音频播放缓冲并使用事件通知机制,当从网络接收音频数据不足时,用静音数据填充,保持播放循环的固定频率运行,从而解决数据不足导致的播放问题。同时,文章还展示了如何枚举DirectSound的输入输出设备,并提供了音量和播放速率控制的方法。
摘要由CSDN通过智能技术生成

DirectSound播放音频

播放文件

参考雷神:最简单的视音频播放示例8:DirectSound播放PCM

播放实时音频

使用上面的例子播放文件是没问题的,但是用DS播放实时音频数据还是有一点坑的,我遇到的问题和这位老兄一样:DirectSoundBuffer使用的实时性问题请教

具体原因如下:

Qt Audio模块播放音频有两种方式,pull和push,DS用的是事件通知机制,相当于pull模式,即由设备在需要时主动从给定的缓冲中读取数据。上面例子中是从文件读取,所有数据均已准备就绪,可以保证每次都能读到需要的数据。

从网络流接收数据时,我们一般将数据存入缓冲队列,然后在另一个线程中读取,但网络传输的速率并不总是恒定的,不能保证每次队列的读写都刚好一致,有时候可能队列中还没有存入足够的数据,但读取操作是由DS播放事件触发的,我们又无法控制。

DS的播放机制是同时读写一个环形缓冲区,每次写入数据覆盖其中的一段内存,如果没有数据写入或者写入数据不够,那缺失的那部分就还是上一次写入的数据,这就导致了没有新的数据进来就会重放上一次收到的数据,如果数据晚到一点,就会让上一次的数据播放了两次,这样慢慢下来声音播放就会越来越不实时了,而是延迟越来越明显。

按照评论区的指点总结,要保证音频播放的实时性,并且避免上述问题,那么当启动播放循环后,每次读取我们都要保证写入指定大小的数据,没有则填充静音数据,而不能阻塞或者跳过本次读取。

具体操作如下,首先将解码后的PCM数据存入队列(此处使用ffmpeg的audio fifo)

// 解码后数据写入到音频播放缓冲
std::lock_guard<std::mutex> lock(_mutex);
av_audio_fifo_write(fifo_input, (void **)audioframe->data, audioframe->nb_samples);

然后按指定大小读取,对上面读文件的例子稍作修改,此 play_loop 在单独的线程中执行:

void CAudioPlay::play_loop()
{
	LPVOID buf = NULL;
	DWORD  buf_len = 0;
	DWORD res = WAIT_OBJECT_0;
	DWORD offset = BUFFERNOTIFYSIZE;
	DWORD freq = 0;

	while (_running)
	{
		if ((res >= WAIT_OBJECT_0) && (res <= WAIT_OBJECT_0 + 3)) {
			m_pDSBuffer8->Lock(offset, BUFFERNOTIFYSIZE, &buf, &buf_len, NULL, NULL, 0);
			std::lock_guard<std::mutex> lock(_mutex);
			int fifo_size = av_audio_fifo_size(fifo_input); // 获取可用数据大小
			int available_size = fifo_size * 2; // 每个采样点2字节
			int read_size = buf_len / 2; // 需要读取采样点数
			if (available_size < buf_len) // 数据不足
			{
				// |            buf_len           |
				// |-----------------|------------|
				// |  available_size | blank_size |
				int blank_size = buf_len - available_size;
				memset((char *)buf + available_size, 0, blank_size); // 将后半部分数据填充0
				printf("数据不足,需要 %d 字节,实际 %d 字节, 缺 %d 字节,填充静音\n", buf_len, available_size, blank_size);
				read_size = fifo_size; // 实际读取采样点数
			}
			av_audio_fifo_read(fifo_input, (void**)&buf, read_size);
			m_pDSBuffer8->Unlock(buf, buf_len, NULL, 0);
			offset += buf_len;
			offset %= (BUFFERNOTIFYSIZE * MAX_AUDIO_BUF);
		}
		res = WaitForMultipleObjects(MAX_AUDIO_BUF, m_event, FALSE, INFINITE);
	}
}

如此一来播放循环始终以固定的频率运行,当队列输入数据不足时则播放静音,就不会造成播放延时或者重复播放问题,而是断断续续。

需要补充的部分

参考:

  1. DirectSound采集播放声音技术文档
  2. DirectSound入门指南(0)播放声音
  3. 通过DirectSound实时播放PCM+混音

枚举设备

不论是采集还是播放,要先枚举设备,判断是否有 GUID 不为NULL的输入输出设备,再初始化DS。注意,设备名称需要用Unicode字符集,简单的枚举步骤如下:

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <stdio.h>
#include <stdint.h>
#include <windows.h>
#include <mmeapi.h>
#include <dsound.h>
#include <string>
#include <vector>

#pragma comment (lib,"dsound.lib")
#pragma comment (lib,"dxguid.lib")

using namespace std;

// 枚举设备执行的回调函数,注意,LPCSTR 需要使用多字节字符集,LPCWSTR使用Unicode字符集
// 设备名称需要使用Unicode字符集,否则可能无法显示
static BOOL CALLBACK DSEnumCallback(
	LPGUID lpGuid,
	LPCWSTR lpcstrDescription,
	LPCWSTR	lpcstrModule,
	LPVOID lpContext
);

// 存放枚举设备信息结构体
typedef struct _DevItem
{
	string strName;
	GUID guid;
} DevItem;

// 用来保存枚举出来的采集设备对象
vector<DevItem> m_CapDevices;
vector<DevItem> m_RenDevices;

// Tool func
std::string GUID2Str(GUID &guid);
std::string WString2String(const std::wstring& ws);
std::wstring String2WString(const std::string& s);

int main()
{
	HRESULT hr = S_OK;

	// 枚举采集设备
	hr = DirectSoundCaptureEnumerate(DSEnumCallback, (LPVOID)&m_CapDevices);
	if (FAILED(hr))
	{
		printf("枚举设备失败\n");
	}
	printf("=================输入设备=================\n");
	for (size_t i = 0; i < m_CapDevices.size(); i++)
	{
		auto item = m_CapDevices[i];
		string strGuid = GUID2Str(item.guid);
		printf("[%d] device name: %s, guid: %s\n", i, item.strName.c_str(), strGuid.c_str());
	}

	printf("\n=================输出设备=================\n");
	// 枚举播放设备
	hr = DirectSoundEnumerate(DSEnumCallback, (LPVOID)&m_RenDevices);
	for (size_t i = 0; i < m_RenDevices.size(); i++)
	{
		auto item = m_RenDevices[i];
		string strGuid = GUID2Str(item.guid);
		printf("[%d] device name: %s, guid: %s\n", i, item.strName.c_str(), strGuid.c_str());
	}

	int dev_index = 0;
	printf("\n输入采集设备序号:");
	while (cin >> dev_index)
	{
		if (dev_index > m_CapDevices.size() - 1 || dev_index < 0)
		{
			printf("无效序号\n");
			continue;
		}
		break;
	}

	// 创建采集设备接口对象
	GUID strGuid = m_CapDevices[dev_index].guid;
	IDirectSoundCapture *m_pMicDevice;
	hr = DirectSoundCaptureCreate(&strGuid, &m_pMicDevice, 0);
	if (FAILED(hr))
	{
		printf("创建采集设备接口对象失败\n");
	}

	printf("创建采集设备成功\n");

	system("pause");
}

BOOL DSEnumCallback(LPGUID lpGuid, LPCWSTR lpcstrDescription, LPCWSTR lpcstrModule, LPVOID lpContext)
{
	vector<DevItem> *pList = (vector<DevItem> *)lpContext;
	if (pList)
	{
		DevItem item;
		memset(&item, 0, sizeof(item));
		item.strName = WString2String(lpcstrDescription);
		if (lpGuid)
			item.guid = *lpGuid;
		else
			item.guid = GUID_NULL;
		pList->push_back(item);
		return TRUE;
	}
	return FALSE;
}

// GUID->string
string GUID2Str(GUID & guid)
{
	char str[40] = { 0 };
	sprintf(str, "{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", 
		guid.Data1, guid.Data2, guid.Data3,
		guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3],
		guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]);
	return string(str);
}

// wstring => string
std::string WString2String(const std::wstring& ws)
{
	std::string strLocale = setlocale(LC_ALL, "");
	const wchar_t* wchSrc = ws.c_str();
	size_t nDestSize = wcstombs(NULL, wchSrc, 0) + 1;
	char *chDest = new char[nDestSize];
	memset(chDest, 0, nDestSize);
	wcstombs(chDest, wchSrc, nDestSize);
	std::string strResult = chDest;
	delete[]chDest;
	setlocale(LC_ALL, strLocale.c_str());
	return strResult;
}

// string => wstring
std::wstring String2WString(const std::string& s)
{
	std::string strLocale = setlocale(LC_ALL, "");
	const char* chSrc = s.c_str();
	size_t nDestSize = mbstowcs(NULL, chSrc, 0) + 1;
	wchar_t* wchDest = new wchar_t[nDestSize];
	wmemset(wchDest, 0, nDestSize);
	mbstowcs(wchDest, chSrc, nDestSize);
	std::wstring wstrResult = wchDest;
	delete[]wchDest;
	setlocale(LC_ALL, strLocale.c_str());
	return wstrResult;
}

音量控制

需要添加 DSBCAPS_CTRLVOLUME标识,例如:

DSBUFFERDESC dsbd;
memset(&dsbd, 0, sizeof(dsbd)); // 结构体的大小。必须初始化该值
dsbd.dwSize = sizeof(dsbd);
dsbd.dwFlags = DSBCAPS_CTRLVOLUME | DSBCAPS_GLOBALFOCUS | DSBCAPS_CTRLPOSITIONNOTIFY | DSBCAPS_GETCURRENTPOSITION2;

然后SetVolume(m_volume) 就会生效

void CAudioPlay::SetVolume(int volume)
{
	if (volume < 0 || volume > 100) return;
	// 将volume映射到[-10000, 0]
	m_volume = DSVOLUME_TO_DB(volume);
	m_pDSBuffer8->SetVolume(m_volume);
}

注意音量的设置范围是 [-1000, 0] ,需要手动转化为常用的百分制。

播放速率控制

需要添加 DSBCAPS_CTRLFREQUENCY 标识,然后用 m_pDSBuffer8->SetFrequency(freq) 函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值