问题描述
我正在尝试可视化WASAPI环回捕获的声波,但是发现我记录的数据包放在一起时不会形成平滑的声波。
我对WASAPI捕获客户端如何工作的理解是,当我调用pCaptureClient->GetBuffer(&pData,&numFramesAvailable,&flags,NULL,NULL)
时,缓冲区pData
从头开始填充了numFramesAvailable
个数据点。每个数据点都是浮点数,它们按通道交替。因此,要获取所有可用的数据点,我应该将pData
强制转换为浮点指针,并采用第一个channels * numFramesAvailable
值。一旦释放缓冲区并再次调用GetBuffer
,它将提供下一个数据包。我以为这些数据包会互相接续,但事实并非如此。
我的猜测是,我对pData
中的音频数据格式做出了不正确的假设,或者捕获客户端丢失或重叠了帧。但是不知道如何检查这些。
为了使下面的代码尽可能简短,我删除了错误状态检查和清除之类的内容。
捕获客户端的初始化:
const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioClient = __uuidof(IAudioClient);
const IID IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient);
pAudioClient = NULL;
IMMDeviceEnumerator * pDeviceEnumerator = NULL;
IMMDevice * pDeviceEndpoint = NULL;
IAudioClient *pAudioClient = NULL;
IAudioCaptureClient *pCaptureClient = NULL;
int channels;
// Initialize audio device endpoint
CoInitialize(nullptr);
CoCreateInstance(CLSID_MMDeviceEnumerator,CLSCTX_ALL,IID_IMMDeviceEnumerator,(void**)&pDeviceEnumerator );
pDeviceEnumerator ->GetDefaultAudioEndpoint(eRender,eConsole,&pDeviceEndpoint );
// init audio client
WAVEFORMATEX *pwfx = NULL;
REFERENCE_TIME hnsRequestedDuration = 10000000;
REFERENCE_TIME hnsActualDuration;
audio_device_endpoint->Activate(IID_IAudioClient,(void**)&pAudioClient);
pAudioClient->GetMixFormat(&pwfx);
pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED,AUDCLNT_STREAMFLAGS_LOOPBACK,hnsRequestedDuration,pwfx,NULL);
channels = pwfx->nChannels;
pAudioClient->GetService(IID_IAudioCaptureClient,(void**)&pCaptureClient);
pAudioClient->Start(); // Start recording.
数据包捕获(请注意std::mutex packet_buffer_mutex
和vector<vector<float>> packet_buffer
已经被另一个线程定义并用于安全地显示数据):
UINT32 packetLength = 0;
BYTE *pData = NULL;
UINT32 numFramesAvailable;
DWORD flags;
int max_packets = 8;
std::unique_lock<std::mutex>write_guard(packet_buffer_mutex,std::defer_lock);
while (true) {
pCaptureClient->GetNextPacketSize(&packetLength);
while (packetLength != 0)
{
// Get the available data in the shared buffer.
pData = NULL;
pCaptureClient->GetBuffer(&pData,NULL);
if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
{
pData = NULL; // Tell copyData to write silence.
}
write_guard.lock();
if (packet_buffer.size() == max_packets) {
packet_buffer.pop_back();
}
if (pData) {
float * pfData = (float*)pData;
packet_buffer.emplace(packet_buffer.begin(),pfData,pfData + channels * numFramesAvailable);
} else {
packet_buffer.emplace(packet_buffer.begin());
}
write_guard.unlock();
hpCaptureClient->ReleaseBuffer(numFramesAvailable);
pCaptureClient->GetNextPacketSize(&packetLength);
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
我将数据包存储在vector<vector<float>>
(每个vector<float>
是一个数据包)中,删除最后一个数据包,并在开始处插入最新的数据包,以便按顺序遍历它们。
下面是捕获的正弦波的结果,绘制了交替的值,因此它仅代表一个通道。很明显,包在哪里缝合在一起。
解决方法
某些事情正在对Windows产生正弦波。您正在音频环回中记录正弦波;而您返回的正弦波并不是真正的正弦波。
您几乎肯定会遇到小故障。出现故障的最可能原因是:
- 无论对Windows产生什么正弦波,都无法及时将数据发送到Windows,因此缓冲区正在耗尽。
- 从Windows读取回送数据的任何操作都无法及时读取数据,因此缓冲区已满。
- 在向Windows播放正弦波并回读正弦波之间出现了问题。
可能有不止一种情况发生。
IAudioCaptureClient::GetBuffer调用将告诉您是否读取数据太晚。特别是,它将设置*pdwFlags
以便设置AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY
位。
看看您的代码,我发现您正在执行GetBuffer和WriteBuffer之间的以下操作:
- 等待锁
- 有时会做一个叫做“ pop_back”的事情
- 做一个叫做“ emplace”的事情
我引用了以上链接的文档:
客户端应避免获取数据包的GetBuffer调用与释放数据包的ReleaseBuffer调用之间的过度延迟。音频引擎的实现假定GetBuffer调用和相应的ReleaseBuffer调用在同一缓冲区处理周期内发生。延迟释放数据包一个以上时间的客户端可能会丢失示例数据。
尤其是,GetBuffer
和ReleaseBuffer
之间的任何操作都不应该做,因为它们最终会导致故障:
- 等待上锁
- 等待其他任何操作
- 读取或写入文件
- 分配内存
相反,请在调用IAudioClient::Start
之前预先分配一堆内存。当每个缓冲区到达时,写入该存储器。在侧面,有一个定期计划的工作项,该工作项会占用已写入的内存,并将其写入磁盘或您正在使用它进行的任何操作。