在研究这个问题时,我在网上发现了多次提到以下场景,总是在编程论坛上作为未回答的问题。我希望在这里发布这个至少可以用来记录我的发现。
首先,症状:在运行使用 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.reserved,该指针指向一个似乎包含至少两个指针作为其前两个成员的结构。第一个指针设置为 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 */
}
通常,标头由 wdmaud.dll 的内部函数 waveCompleteHeader() 返回给应用程序。waveCompleteHeader() 尝试通过调用 GlobalHandle()/GlobalUnlock() 和朋友来释放由 waveOutWrite() 分配的指针。有时,GlobalHandle() 会爆炸,如上所示。
现在,GlobalHandle() 炸弹的原因并不是因为堆损坏,正如我一开始所怀疑的那样 - 这是因为 waveOutWrite() 在没有将内部结构中的第一个指针设置为有效指针的情况下返回。我怀疑它在返回之前释放了该指针指向的内存,但我还没有反汇编它。
这似乎仅在波形播放系统缓冲区不足时发生,这就是为什么我使用单个标头来重现它的原因。
在这一点上,我有一个很好的理由反对这是我的应用程序中的一个错误——毕竟,我的应用程序甚至没有运行。有没有人见过这个?
我在 Windows XP SP2 上看到了这个。声卡来自SigmaTel,驱动版本为5.10.0.4995。
笔记:
为了防止将来出现混淆,我想指出,问题在于使用 malloc()/free() 来管理正在播放的缓冲区的答案是完全错误的。您会注意到我更改了上面的代码以反映建议,以防止更多人犯同样的错误——这没有什么区别。waveCompleteHeader() 释放的缓冲区不是包含 PCM 数据的缓冲区,释放 PCM 缓冲区的责任在于应用程序,并且不要求以任何特定方式分配它。
此外,我确保我使用的所有 waveOut API 调用都不会失败。
我目前假设这要么是 Windows 中的错误,要么是音频驱动程序中的错误。反对意见总是受欢迎的。