我想在使用Win32的C++中以小的时间间隔连续无缝地将原始音频数据馈送到一个循环缓冲区中。 WAVEHDR
的header.lpdata
缓冲区包含原始音频数据,通过调用WaveInAddBuffer(wi,&header,sizeof(WAVEHDR));
,此缓冲区会在较小的时间间隔内循环覆盖。 下面的图像显示了问题:
虽然缓冲器被重复地以小块覆盖(从左到右,电流偏移由品红色线显示,并且在品红色线处具有不连续性的波中可见),但在波中随机存在额外的不连续性(黄色闪电)。 几年前,我在Java写过同样的东西,在那里它工作得完美无缺,没有音频输入的间断。
是我做错了什么,还是Win32音频库中的一个bug?
下面是我的C++代码的相关部分:
#define VC_EXTRALEAN
#pragma comment(lib,"winmm.lib")
#include <Windows.h>
const int sample_rate = 4*4096; // must be supported by microphone
const int sample_size = 4096; // must be a power of 2
const int buffer_size = 2*sample_size;
char* buffer = new char[buffer_size];
int offset = 0;
WAVEFORMATEX wfx = {};
wfx.wFormatTag = WAVE_FORMAT_PCM; // PCM is standard
wfx.nChannels = 1; // 1 channel (mono)
wfx.nSamplesPerSec = sample_rate; // sample_rate
wfx.nAvgBytesPerSec = 2*sample_rate; // sample_rate * 2 bytes per data point
wfx.wBitsPerSample = 16; // 16 bit samples
wfx.nBlockAlign = wfx.wBitsPerSample*wfx.nChannels/8;
wfx.nAvgBytesPerSec = wfx.nBlockAlign*wfx.nSamplesPerSec*wfx.nChannels;
wfx.cbSize = 0;
HWAVEIN wi; // open recording device
WAVEHDR header = {}; // initialize header empty
header.dwFlags = 0; // clear the 'done' flag
header.dwBytesRecorded = 0; // tell it no bytes have been recorded
header.lpData = buffer; // give it a pointer to our buffer
header.dwBufferLength = buffer_size; // tell it the size of that buffer in bytes
waveInOpen(&wi, WAVE_MAPPER, &wfx, NULL, NULL, CALLBACK_NULL|WAVE_FORMAT_DIRECT);
waveInStart(wi); // start recording
waveInPrepareHeader(wi, &header, sizeof(WAVEHDR)); // prepare header
while(true) {
waveInAddBuffer(wi, &header, sizeof(WAVEHDR)); // read in new audio data into buffer
offset = header.dwBytesRecorded; // get offset of to which point the buffer is overwritten
// process / draw buffer and offset
sleep(1.0/120.0); // time in seconds
}
问题:
>
您的线程正在与正在填充缓冲区并更新标头中字段的系统线程竞争。 当您读取DWBytesRecorded
字段时,可以得到一个小于缓冲区中实际字节数的值。 填充缓冲区的线程偶尔会更新DWBytesRecorded
,但随着记录的继续,该数字会在片刻后过期。 这是乐观的假设,当另一个线程可能正在写入DWORD时,读取它是安全的。
当您再次添加缓冲区时,音频系统认为这是一个新的缓冲区,当当前的缓冲区已满时立即切换到该缓冲区。 您正在传递相同的缓冲区给它,希望它从一开始就开始填充它。 但它也可能会干扰标头中的保留字段并创建不一致的状态。
我不确定您使用的是哪一个sleep
函数,但它们中的大多数不能/不等待精确的时间量。 Win32sleep
将至少等待指定的毫秒数,然后将线程标记为准备运行,但直到排定程序处理线程时,它才会实际运行。 实际上,这可能不是一个问题,因为您的缓冲区为500毫秒,这比休眠带来的不确定性大一个数量级。
实现这一点的典型方法是在两个(或更多)缓冲区之间进行乒乓运动。 您可以添加两个非常短的缓冲区,并等待第一个缓冲区在其头中设置WHDR_DONE
标志。 然后立即处理整个第一个缓冲区,同时系统继续记录到第二个缓冲区中。 处理完一个缓冲区后,重新添加它,然后等待另一个缓冲区准备就绪。
// Given two buffers `ping` and `pong` with corresponding WAVEHDRs
// `ping_header` and `pong_header`...
WAVEHDR *pCurrent = ping_header;
WAVEHDR *pNext = pong_header;
waveInAddBuffer(wi, pCurrent, sizeof(WAVEHDR));
waveInAddBuffer(wi, pNext, sizeof(WAVEHDR));
for (;;) {
// wait for the current buffer to fill
while ((pCurrent->dwFlags & WHDR_DONE) == 0) {}
// As recording continues with *pNext, process and draw
// the data from pCurrent->lpData.
// Now that we're done processing pCurrent, we can re-add it so
// the system has a place to record when pNext is full.
waveInAddBuffer(wi, pCurrent, sizeof(WAVEHDR));
// What was next becomes current, and the new next is the old current.
swap(pCurrent, pNext);
}
请注意,您的两个缓冲区可能相当短。 我建议16-20毫秒:比Windows上默认的15.6毫秒计时器要大,但仍在每次循环迭代中尝试处理的数据量的大致范围内。
这里的busy wait循环不是很好--它可以将核心驱动到100%,而不做有用的工作。 但如果处理时间接近记录下一个缓冲区所需的时间,那么它就不会旋转太多。 (从技术上讲,在另一个线程更新变量时,仍然存在读取变量的数据竞争问题,但我们只是在观察该位是否变为高电平,因此在实际操作中可能没有问题。)
wave audio API不是为极端高速处理而设计的。 它们是为Windows程序设计的。 您应该在窗口的窗口过程中处理MM_WIM_DATA消息,而不是忙着等待标志,这将避免忙着等待和数据竞争,但在每个缓冲区完成时会增加一点消息传递开销。