播放文件
参考雷神:最简单的视音频播放示例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);
}
}
如此一来播放循环始终以固定的频率运行,当队列输入数据不足时则播放静音,就不会造成播放延时或者重复播放问题,而是断断续续。
需要补充的部分:
参考:
枚举设备
不论是采集还是播放,要先枚举设备,判断是否有 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)
函数。