13

在研究这个问题时,我在网上发现了多次提到以下场景,总是在编程论坛上作为未回答的问题。我希望在这里发布这个至少可以用来记录我的发现。

首先,症状:在运行使用 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 中的错误,要么是音频驱动程序中的错误。反对意见总是受欢迎的。

4

9 回答 9

3

现在,GlobalHandle() 炸弹的原因并不是因为堆损坏,正如我一开始所怀疑的那样 - 这是因为 waveOutWrite() 在没有将内部结构中的第一个指针设置为有效指针的情况下返回。我怀疑它在返回之前释放了该指针指向的内存,但我还没有反汇编它。

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

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

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

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

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

于 2009-02-05T02:37:38.107 回答
2

您并不孤单: http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID= 100589

于 2009-01-31T15:06:29.067 回答
1

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

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

但是当这个堆区域再次被释放时(根据你的分析,在 waveCompleteHeader() 中;我自己没有 wdmaud.drv 的符号),指针的最低有效字节已设置为零,因此无效指针(虽然堆还没有损坏)。换句话说,发生的事情是这样的:

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

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

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

于 2009-01-16T15:28:35.820 回答
0

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

示例包括PortAudioRtAudioSDL

于 2008-10-14T01:56:04.800 回答
0

我要做的第一件事是检查 waveOutX 函数的返回值。如果其中任何一个失败了——考虑到你描述的场景,这并不是不合理的——并且你继续进行,那么事情开始出错也就不足为奇了。我的猜测是 waveOutWrite 在某个时候返回 MMSYSERR_NOMEM。

于 2008-10-17T14:48:49.450 回答
0

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

于 2009-01-30T16:32:26.663 回答
0

查看Wine 的源代码可能会有所帮助,尽管 Wine 可能已经修复了任何错误,而且 Wine 也可能存在其他错误。相关文件是 dlls/winmm/winmm.c、dlls/winmm/lolvldrv.c,可能还有其他文件。祝你好运!

于 2009-01-31T15:45:19.627 回答
0

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

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

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

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

于 2009-08-20T02:47:25.853 回答
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);

在 Windows 中使用 waveOut 接口播放音频

于 2016-03-18T10:42:33.813 回答