我对Microsoft的WaveOut API有问题:
edit1:添加了指向示例项目的链接:编辑2:删除链接,它不代表问题
播放一些音频后,当我想终止给定的播放流时,我调用该函数:
waveOutClose(hWaveOut_);
但是,即使在调用 waveOutClose() 之后,有时库仍然会访问之前由 waveOutWrite() 传递给它的内存,从而导致内存访问无效。
然后,我尝试确保在释放缓冲区之前将所有缓冲区标记为已完成:
PcmPlayback::~PcmPlayback()
{
if(hWaveOut_ == nullptr)
return;
waveOutReset(hWaveOut_); // infinite-loops, never returns
for(auto it = buffers_.begin(); it != buffers_.end(); ++it)
waveOutUnprepareHeader(hWaveOut_, &it->wavehdr_, sizeof(WAVEHDR));
while( buffers_.empty() == false ) // infinite loops
removeCompletedBuffers();
waveOutClose(hWaveOut_);
//Unhandled exception at 0x75629E80 (msvcrt.dll) in app.exe:
// 0xC0000005: Access violation reading location 0xFEEEFEEE.
}
void PcmPlayback::removeCompletedBuffers()
{
for(auto it = buffers_.begin(); it != buffers_.end();)
{
if( it->wavehdr_.dwFlags & WHDR_DONE )
{
waveOutUnprepareHeader(hWaveOut_, &it->wavehdr_, sizeof(WAVEHDR));
it = buffers_.erase(it);
}
else
++it;
}
}
但是,这种情况永远不会发生 - 缓冲区永远不会变为空。wavehdr_.dwFlags == 18 将剩下 4-5 个块(我相信这意味着这些块仍然标记为播放中)
如何解决此问题?
@ Martin Schlott ("你能提供将缓冲区写入waveOutWrite的循环吗?")它不是一个循环,相反,我有一个函数,每当我通过网络收到音频数据包时都会调用该函数:
void PcmPlayback::addData(const std::vector<short> &rhs)
{
removeCompletedBuffers();
if(rhs.empty())
return;
// add new data
buffers_.push_back(Buffer());
Buffer & buffer = buffers_.back();
buffer.data_ = rhs;
ZeroMemory(&buffers_.back().wavehdr_, sizeof(WAVEHDR));
buffer.wavehdr_.dwBufferLength = buffer.data_.size() * sizeof(short);
buffer.wavehdr_.lpData = (char *)(buffer.data_.data());
waveOutPrepareHeader(hWaveOut_, &buffer.wavehdr_, sizeof(WAVEHDR)); // prepare block for playback
waveOutWrite(hWaveOut_, &buffer.wavehdr_, sizeof(WAVEHDR));
}
如果不调用,可能会发生所描述的行为
waveOutUnprepareHeader
到使用前使用的每个缓冲区
waveOutClose
标志字段_dwFlags似乎指示缓冲区仍在排队 (WHDR_INQUEUE |WHDR_PREPARED) 尝试:
waveOutReset
在取消准备缓冲区之前。
在分析了您的代码后,我发现了两个与waveOut无关的问题/错误(有趣的是,您使用C++11但最古老的媒体界面)。使用向量作为缓冲区。在某些调用操作期间,向量被复制!我发现的一个错误是:
typedef std::function<void(std::vector<short>)> CALLBACK_FN;
而不是:
typedef std::function<void(std::vector<short>&)> CALLBACK_FN;
这会强制复制矢量。如果您希望主要将其用作原始缓冲区,请尽量避免使用向量。最好使用 std::unique_pointer 作为缓冲区指针。
记录器中的回调不受互斥锁监控,也不会检查是否已调用析构函数。析构发生在回调期间(主要是),这会导致异常。
对于您的测试程序,在责怪 waveOut 之前返回并使用原始指针和静态回调。你的代码还不错,但第一个错误已经表明,一个小错误会导致不可预测的错误。 由于您还在 std::array 中组织缓冲区,我会在那里搜索错误。我猜,你无意中复制了整个缓冲区数组,取消了错误的缓冲区。
我没有时间深入挖掘,但我想这些都是问题所在。
我最终设法找到了我的问题,它是由多个错误和死锁引起的。我将记录这里发生的事情,以便人们将来可以从中学习。当我修复示例中的错误时,我
了解发生了什么:- 在 ~Recorder 中的 waveInClose() 之前调用 waveInStop(.cpp
- 等待所有缓冲区具有WHDR_DONE标志,然后再在 ~PcmPlayback 中调用 waveOutClose()。
执行此操作后,示例工作正常,并且未显示从未标记WHDR_DONE标志的行为。
在我的主程序中,该行为是由以下情况下发生的死锁引起的:
- 我
- 有一个对象向量,代表我正在流式传输音频的每个对等方
- 每个对象拥有一个播放类
- 此向量受互斥锁保护
记录器回调:
- mutex.lock()
- 将音频数据包发送到每个对等方。
删除对等体:
- mutex.lock()
- ~pcm回放
- 等待标记WHDR_DONE标志
当我删除对等体时,锁定互斥锁并且记录器回调也尝试获取锁时会发生死锁。
- 请注意,这种情况经常发生,因为播放缓冲区通常为 (~4 * 20ms),而录音机的节奏为 20ms。
- 在 ~pcmPlay 中,缓冲区永远不会被标记为 WHDR_DONE,并且对 WaveOut API 的任何调用都不会返回,因为 WaveOut API 正在等待 Recorder 回调完成,而 Recorder 回调又在等待 mutex.lock(),从而导致死锁。