为什么waveOutWrite()会在调试堆中导致异常?

时间:2008-10-12 16:59:55

标签: c++ windows audio waveoutwrite

在研究这个问题时,我在网上发现了以下几种情况的提及,总是作为编程论坛上未解答的问题。我希望在此发布此内容至少可以记录我的发现。

首先,症状:在运行使用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 */
}

通常,标头由waveCompleteHeader()返回应用程序,waveCompleteHeader是Wdmaud.dll内部的一个函数。 waveCompleteHeader()尝试通过调用GlobalHandle()/ GlobalUnlock()和朋友来释放waveOutWrite()分配的指针。有时,GlobalHandle()炸弹,如上所示。

现在,GlobalHandle()炸弹的原因不是因为我首先怀疑的堆损坏 - 这是因为waveOutWrite()返回时没有将内部结构中的第一个指针设置为有效指针。我怀疑它在返回之前释放了指针所指向的内存,但我还没有拆解它。

只有在波形播放系统缓冲区不足时才会出现这种情况,这就是我使用单个标头重现这一点的原因。

此时我有一个非常好的例子反对这是我的应用程序中的一个错误 - 毕竟,我的应用程序甚至没有运行。有没有人见过这个?

我在Windows XP SP2上看到了这一点。声卡来自SigmaTel,驱动程序版本为5.10.0.4995。

注意:

为了防止将来发生混淆,我想指出,建议问题在于使用malloc()/ free()来管理正在播放的缓冲区的答案是完全错误的。你会注意到我改变了上面的代码来反映这个建议,以防止更多的人犯同样的错误 - 这没有什么区别。 waveCompleteHeader()释放的缓冲区不是包含PCM数据的缓冲区,释放PCM缓冲区的责任在于应用程序,并且不要求以任何特定方式分配它。

另外,我确保我使用的waveOut API调用都没有失败。

我目前假设这是Windows中的错误或音频驱动程序中的错误。不时欢迎不同意见。

9 个答案:

答案 0 :(得分:3)

  

现在,GlobalHandle()的原因   炸弹不是因为堆腐败,   正如我最初所怀疑的 - 这是因为   waveOutWrite()没有返回   设置第一个指针   内部结构到有效指针。   我怀疑它释放了记忆   之前指针指向   回来了,但我没有拆解   它呢。

我可以使用您系统上的代码重现这一点。我看到类似于约翰内斯报道的东西。在调用WaveOutWrite之后,hdr-> reserved通常会保存一个指向已分配内存的指针(其中包含unicode中的wave out设备名称等)。

但偶尔从WaveOutWrite()返回后,hdr->reserved指向的字节设置为0.这通常是该指针的最低有效字节。 hdr->reserved中的其余字节都可以,并且它通常指向的内存块仍然是已分配且未损坏的。

它可能被另一个线程破坏 - 我可以在调用WaveOutWrite()之后立即用条件断点捕获更改。系统调试断点发生在另一个线程中,而不是消息处理程序。

但是,如果我使用回调函数而不是windows messsage泵,我不会导致系统调试断点发生。 (WaveOutOpen()中的fdwOpen = CALLBACK_FUNCTION) 当我这样做时,我的OnWOMDone处理程序由另一个线程调用 - 可能是另一个负责损坏的线程。

所以我认为在Windows或驱动程序中存在一个错误,但我认为您可以使用回调函数而不是Windows消息泵来处理WOM_DONE。

答案 1 :(得分:2)

答案 2 :(得分:1)

我遇到了同样的问题并且自己做了一些分析:

waveOutWrite()分配(即GlobalAlloc)一个指向354字节堆区域的指针,并将其正确存储在header.reserved指向的数据区域中。

但是当要再次释放这个堆区域时(在waveCompleteHeader()中,根据你的分析;我自己没有wdmaud.drv的符号),指针的最低有效字节已被设置为零,从而使指针无效(当堆没有被破坏时)。换句话说,会发生什么:

  • (BYTE *)(header.reserved)= 0

所以我不同意你的陈述:waveOutWrite()首先存储一个有效的指针;指针只会在以后从另一个线程中被破坏。 可能是后来尝试释放这个堆区域的同一个线程(mxdmessage),但我还没有找到存储零字节的点。

这种情况不会经常发生,并且之前已成功分配和解除分配相同的堆区域(相同的地址)。 我确信这是系统代码中的某个错误。

答案 3 :(得分:0)

不确定此特定问题,但您是否考虑过使用更高级别的跨平台音频库? Windows音频编程有很多怪癖,这些库可以为您节省很多麻烦。

示例包括PortAudioRtAudioSDL

答案 4 :(得分:0)

我要做的第一件事就是检查waveOutX函数的返回值。如果它们中的任何一个失败 - 根据你所描述的场景这不是不合理的 - 并且你继续坚持下去,事情开始出错就不足为奇了。我的猜测是waveOutWrite会在某个时刻返回MMSYSERR_NOMEM。

答案 5 :(得分:0)

使用应用程序验证程序来确定发生了什么,如果你做了可疑的事情,它会更早地发现它。

答案 6 :(得分:0)

查看source code for Wine可能会有所帮助,虽然Wine可能修复了任何错误,但Wine也可能有其他错误。相关文件是dlls / winmm / winmm.c,dlls / winmm / lolvldrv.c以及其他可能的文件。祝你好运!

答案 7 :(得分:0)

如果不允许您在回调中调用winmm函数,那该怎么办? MSDN没有提到关于窗口消息的这种限制,但是窗口消息的使用类似于回调函数。可能在内部它被实现为来自驱动程序的回调函数,并且该回调执行SendMessage。 在内部,waveout必须维护使用waveOutWrite编写的标题的链表;所以,我想:

hdr->reserved = (Undocumented*)calloc(sizeof(Undocumented));

设置链接列表的上一个/下一个指针或类似的东西。如果你写了更多的缓冲区,那么如果你检查指针,如果它们中的任何一个指向另一个,那么我的猜测很可能是正确的。

网络上的多个来源提到您不需要反复取消准备/准备相同的标头。如果您在原始示例中注释掉Prepare / unrepare标题,那么它似乎没有任何问题。

答案 8 :(得分:0)

我通过轮询声音播放和延迟来解决问题:

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);

Playing Audio in Windows using waveOut Interface