在研究这个问题时,我发现网上多次提到以下场景,在编程论坛上都是未回答的问题。我希望在这里张贴这篇文章至少能记录我的发现。
首先,症状:当运行使用waveOutWrite()输出PCM音频的非常标准的代码时,我有时会在调试器下运行时遇到这种情况:
ntdll.dll!_DbgBreakPoint@0()
ntdll.dll!_RtlpBreakPointHeap@4() + 0x28 bytes
ntdll.dll!_RtlpValidateHeapEntry@12() + 0x113 bytes
ntdll.dll!_RtlDebugGetUserInfoHeap@20() + 0x96 bytes
ntdll.dll!_RtlGetUserInfoHeap@20() + 0x32743 bytes
kernel32.dll!_GlobalHandle@4() + 0x3a bytes
wdmaud.drv!_waveCompleteHeader@4() + 0x40 bytes
wdmaud.drv!_waveThread@4() + 0x9c bytes
kernel32.dll!_BaseThreadStart@8() + 0x37 bytes
虽然明显的怀疑可能是代码中其他地方的堆损坏,但我发现事实并非如此。此外,我能够使用以下代码重现这个问题(这是基于对话框的MFC应用程序的一部分:)
void CwaveoutDlg::OnBnClickedButton1()
{
WAVEFORMATEX wfx;
wfx.nSamplesPerSec = 44100; /* sample rate */
wfx.wBitsPerSample = 16; /* sample size */
wfx.nChannels = 2;
wfx.cbSize = 0; /* size of _extra_ info */
wfx.wFormatTag = WAVE_FORMAT_PCM;
wfx.nBlockAlign = (wfx.wBitsPerSample >> 3) * wfx.nChannels;
wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec;
waveOutOpen(&hWaveOut,
WAVE_MAPPER,
&wfx,
(DWORD_PTR)m_hWnd,
0,
CALLBACK_WINDOW );
ZeroMemory(&header, sizeof(header));
header.dwBufferLength = 4608;
header.lpData = (LPSTR)GlobalLock(GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE | GMEM_ZEROINIT, 4608));
waveOutPrepareHeader(hWaveOut, &header, sizeof(header));
waveOutWrite(hWaveOut, &header, sizeof(header));
}
afx_msg LRESULT CwaveoutDlg::OnWOMDone(WPARAM wParam, LPARAM lParam)
{
HWAVEOUT dev = (HWAVEOUT)wParam;
WAVEHDR *hdr = (WAVEHDR*)lParam;
waveOutUnprepareHeader(dev, hdr, sizeof(WAVEHDR));
GlobalFree(GlobalHandle(hdr->lpData));
ZeroMemory(hdr, sizeof(*hdr));
hdr->dwBufferLength = 4608;
hdr->lpData = (LPSTR)GlobalLock(GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE | GMEM_ZEROINIT, 4608));
waveOutPrepareHeader(hWaveOut, &header, sizeof(WAVEHDR));
waveOutWrite(hWaveOut, hdr, sizeof(WAVEHDR));
return 0;
}
在有人对此发表评论之前,是的,示例代码会回放未初始化的内存。不要在扬声器一直调高的情况下尝试这种方法。
一些调试显示了以下信息:waveOutPrepareHeader()用一个指针填充header.reReserved,该指针指向的结构似乎至少包含两个指针作为其前两个成员。第一个指针设置为NULL。在调用waveOutWrite()之后,该指针被设置为在全局堆上分配的指针。在伪代码中,它看起来像这样:
struct Undocumented { void *p1, *p2; } /* This might have more members */
MMRESULT waveOutPrepareHeader( handle, LPWAVEHDR hdr, ...) {
hdr->reserved = (Undocumented*)calloc(sizeof(Undocumented));
/* Do more stuff... */
}
MMRESULT waveOutWrite( handle, LPWAVEHDR hdr, ...) {
/* The following assignment fails rarely, causing the problem: */
hdr->reserved->p1 = malloc( /* chunk of private data */ );
/* Probably more code to initiate playback */
}
通常情况下,头由waveCompleteHeader()返回给应用程序,这是wdmaud.dll内部的一个函数。waveCompleteHeaders()试图通过调用GlobalHandle()/GlobalUnlock()和friends来释放waveOutWrite()分配的指针。有时,GlobalHandle()会爆炸,如上所示。
现在,GlobalHandle()爆炸的原因并不像我一开始怀疑的那样是由于堆损坏,而是因为waveOutWrite()返回时没有将内部结构中的第一个指针设置为有效指针。我怀疑它在返回之前释放了该指针指向的内存,但我还没有分解它。
只有当波形播放系统的缓冲区不足时才会出现这种情况,这就是为什么我使用单个标头来再现这种情况。
在这一点上,我有一个很好的理由来反对这是我的应用程序中的一个错误——毕竟,我的应用软件甚至还没有运行。以前有人见过这个吗?
我在Windows XP SP2上看到这个。声卡来自SigmaTel,驱动程序版本为5.10.0.4995。
注:
为了防止将来出现混淆,我想指出的是,认为问题在于使用malloc()/free()来管理正在播放的缓冲区的答案是错误的。你会注意到,我修改了上面的代码来反映这个建议,以防止更多的人犯同样的错误——这没有什么区别。waveCompleteHeader()释放的缓冲区不是包含PCM数据的缓冲区,释放PCM缓冲区的责任在于应用程序,并且不要求以任何特定方式分配。
此外,我确保我使用的waveOut API调用都不会失败。
我目前认为这要么是Windows中的错误,要么是音频驱动程序中的错误。持不同意见总是受欢迎的。
现在,GlobalHandle()炸弹不是由于堆积腐败,正如我一开始所怀疑的那样——这是因为waveOutWrite()返回时没有在中设置第一个指针有效指针的内部结构。我怀疑它释放了内存之前那个指针指向的回来了,但我还没有拆开它还没有。
我可以在我的系统上用你的代码复制这个。我看到了一些类似约翰内斯报道的东西。调用WaveOutWrite后,hdr->reserved通常会保存一个指向已分配内存的指针(其中似乎包含unicode中的wave-out设备名称等)。
但偶尔,从WaveOutWrite()返回后,hdr->reserved
指向的字节会设置为0。这通常是该指针的最低有效字节。hdr->reserved
中的其余字节是正常的,它通常指向的内存块仍然是分配的并且没有损坏。
它可能正被另一个线程破坏——我可以在调用WaveOutWrite()后立即用条件断点捕获更改。系统调试断点发生在另一个线程中,而不是消息处理程序中。
但是,如果我使用回调函数而不是windows消息泵,我就不会导致系统调试断点发生。(WaveOutOpen()中的fdwOpen = CALLBACK_FUNCTION
)当我这样做时,我的OnWOMDone处理程序会被另一个线程调用——可能是另一个负责损坏的线程。
因此,我认为在windows或驱动程序中存在一个错误,但我认为您可以通过使用回调函数而不是windows消息泵来处理WOM_DONE来解决问题。
您并不是唯一一个遇到此问题的人:http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=100589
我看到了同样的问题,我自己也做了一些分析:
waveOutWrite()分配(即GlobalAlloc)一个指向354字节堆区域的指针,并将其正确存储在header.reReserved所指向的数据区域中。
但是当这个堆区域要再次释放时(根据您的分析,在waveCompleteHeader()中;我自己没有wdmaud.drv的符号),指针的最低有效字节被设置为零,从而使指针无效(而堆还没有损坏)。换句话说,发生的事情类似于:
- (BYTE*)(标头。保留)=0
所以我在一点上不同意你的说法:waveOutWrite()首先存储一个有效的指针;指针稍后只会从另一个线程损坏。可能是同一个线程(mxdmessage)后来试图释放这个堆区域,但我还没有找到存储零字节的点。
这种情况并不经常发生,而且以前已经成功地分配和释放了相同的堆区域(相同的地址)。我确信这是系统代码中的一个错误。
不确定这个特定的问题,但您是否考虑过使用更高级别的跨平台音频库?Windows音频编程有很多怪癖,这些库可以帮你省去很多麻烦。
示例包括PortAudio、RtAudio和SDL。
我要做的第一件事是检查waveOutX函数的返回值。如果他们中的任何一个失败了——考虑到你描述的情况,这并非不合理——而且你不顾一切地继续下去,那么事情开始出错也就不足为奇了。我的猜测是waveOutWrite在某个时刻返回MMSYSERR_NOMEM。
使用应用程序验证程序来了解发生了什么,如果你做了可疑的事情,它会更早地发现。
查看Wine的源代码可能会有所帮助,尽管Wine可能已经修复了存在的任何错误,也可能Wine中有其他错误。相关文件有dlls/winmm/winmm.c、dlls/winnmm/lolvldrv.c,以及可能的其他文件。祝你好运
不允许从回调中调用winmm函数的事实如何?MSDN并没有提到对窗口消息的限制,但窗口消息的使用类似于回调函数。可能,在内部,它被实现为来自驱动程序的回调函数,该回调执行SendMessage。在内部,waveout必须维护使用waveOutWrite写入的标题的链接列表;所以,我想:
hdr->reserved = (Undocumented*)calloc(sizeof(Undocumented));
设置链接列表的上一个/下一个指针或类似的内容。如果你写更多的缓冲区,那么如果你检查指针,如果其中任何一个指向另一个,那么我的猜测很可能是正确的。
网络上的多个消息来源提到,您不需要重复取消准备/准备相同的标头。如果您在原始示例中注释掉Prepare/unpare标头,则它似乎可以正常工作,没有任何问题。
我通过轮询声音播放和延迟来解决问题:
WAVEHDR header = { buffer, sizeof(buffer), 0, 0, 0, 0, 0, 0 };
waveOutPrepareHeader(hWaveOut, &header, sizeof(WAVEHDR));
waveOutWrite(hWaveOut, &header, sizeof(WAVEHDR));
/*
* wait a while for the block to play then start trying
* to unprepare the header. this will fail until the block has
* played.
*/
while (waveOutUnprepareHeader(hWaveOut,&header,sizeof(WAVEHDR)) == WAVERR_STILLPLAYING)
Sleep(100);
waveOutClose(hWaveOut);
使用waveOut接口在Windows中播放音频