views:

1748

answers:

10

While researching this issue, I found multiple mentions of the following scenario online, invariably as unanswered questions on programming forums. I hope that posting this here will at least serve to document my findings.

First, the symptom: While running pretty standard code that uses waveOutWrite() to output PCM audio, I sometimes get this when running under the debugger:

 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

While the obvious suspect would be a heap corruption somewhere else in the code, I found out that that's not the case. Furthermore, I was able to reproduce this problem using the following code (this is part of a dialog based MFC application:)

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

Before anyone comments on this, yes - the sample code plays back uninitialized memory. Don't try this with your speakers turned all the way up.

Some debugging revealed the following information: waveOutPrepareHeader() populates header.reserved with a pointer to what appears to be a structure containing at least two pointers as its first two members. The first pointer is set to NULL. After calling waveOutWrite(), this pointer is set to a pointer allocated on the global heap. In pseudo code, that would look something like this:

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 */
}

Normally, the header is returned to the application by waveCompleteHeader(), a function internal to wdmaud.dll. waveCompleteHeader() tries to deallocate the pointer allocated by waveOutWrite() by calling GlobalHandle()/GlobalUnlock() and friends. Sometimes, GlobalHandle() bombs, as shown above.

Now, the reason that GlobalHandle() bombs is not due to a heap corruption, as I suspected at first - it's because waveOutWrite() returned without setting the first pointer in the internal structure to a valid pointer. I suspect that it frees the memory pointed to by that pointer before returning, but I haven't disassembled it yet.

This only appears to happen when the wave playback system is low on buffers, which is why I'm using a single header to reproduce this.

At this point I have a pretty good case against this being a bug in my application - after all, my application is not even running. Has anyone seen this before?

I'm seeing this on Windows XP SP2. The audio card is from SigmaTel, and the driver version is 5.10.0.4995.

Notes:

To prevent confusion in the future, I'd like to point out that the answer suggesting that the problem lies with the use of malloc()/free() to manage the buffers being played is simply wrong. You'll note that I changed the code above to reflect the suggestion, to prevent more people from making the same mistake - it doesn't make a difference. The buffer being freed by waveCompleteHeader() is not the one containing the PCM data, the responsibility to free the PCM buffer lies with the application, and there's no requirement that it be allocated in any specific way.

Also, I make sure that none of the waveOut API calls I use fail.

I'm currently assuming that this is either a bug in Windows, or in the audio driver. Dissenting opinions are always welcome.

A: 

I have no idea about waveOutXXX stuff, but in your code I see something strange - you allocate the memory that you put in hdr->lpData using malloc() but fom your analysis it appears that you are expected to use the GlobalXXX functions (specifically GlobalAlloc()) for the allocation/deallocation of this buffer.

A look in this MSDN code sample pretty much confirms my suspicion.

EDIT: as additional reading material I recommend Raymond Chen's "history of GlobalLock" series, parts 1, 2, 3 and 4. Enjoy

gnobal
No, you must always match your alloc and free functions. Any memory allocated with `malloc()` must be freed with `free()`; any memory allocated with `GlobalAlloc()` must be freed with `GlobalFree()`.
Adam Rosenfield
Adam, I completely agree. It appears I wasn't clear enough: the buffer needs to be allocated with GlobalAlloc() and released with GlobalFree(). The use of _both_ malloc() and free() seems to be the problem.
gnobal
You're confusing which buffer I'm looking at in my analysis. GlobalHandle() is called by Windows to free the buffer that was allocated by waveOutWrite(), not the PCM buffer. THe PCM buffer isn't freed by Windows - it's returned to the application via the callback.
Ori Pessach
OK, I confirmed it - changing the memory allocation functions to GlobalAlloc()/GlobalFree() doesn't make a difference. Indeed, it shouldn't - just because the MS sample code dates back to Windows 3.1 doesn't mean that it's relevant.
Ori Pessach
A: 

Not sure about this particular problem, but have you considered using a higher-level, cross-platform audio library? There are a lot of quirks with Windows audio programming, and these libraries can save you a lot of headaches.

Examples include PortAudio, RtAudio, and SDL.

dmazzoni
I need low level access, and Windows audio programming is dead simple - the waveOut API has been there forever, I know it extremely well (13 years of experience using it, writing multiple sound engines using it, starting with Windows 3.1) The code above is pretty much what it takes. That's it.
Ori Pessach
A: 

The first thing that I'd do would be to check the return values from the waveOutX functions. If any of them fail - which isn't unreasonable given the scenario you describe - and you carry on regardless then it isn't surprising that things start to go wrong. My guess would be that waveOutWrite is returning MMSYSERR_NOMEM at some point.

Stu Mackellar
In my production code, every single waveOutXXX function has an assert() making sure that the return value is MMSYSERR_NOERROR immediately after it. None of the calls fail.
Ori Pessach
Also, you should note that the failure happens after the buffer was played successfully, and right before it's returned to the application, so waveOutWrite() thinks it succeeded.
Ori Pessach
It is possible that this is caused by a driver fault. Have you tried to reproduce the same problem while using different audio hardware? What device are you using at the moment and which driver version?
Stu Mackellar
SigmaTel - Driver version 5.10.0.4995. I've seen this fail on other machines with other hardware, but I'd like to verify that over the weekend, and provide more information.
Ori Pessach
+1  A: 

I'm seeing the same problem and have done some analysis myself:

waveOutWrite() allocates (i.e. GlobalAlloc) a pointer to a heap area of 354 bytes and correctly stores it in the data area pointed to by header.reserved.

But when this heap area is to be freed again (in waveCompleteHeader(), according to your analysis; I don't have the symbols for wdmaud.drv myself), the least significant byte of the pointer has been set to zero, thus invalidating the pointer (while the heap is not corrupted yet). In other words, what happens is something like:

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

So I disagree with your statements in one point: waveOutWrite() stores a valid pointer first; the pointer only becomes corrupted later from another thread. Probably that's the same thread (mxdmessage) that later tries to free this heap area, but I did not yet find the point where the zero byte is stored.

This does not happen very often, and the same heap area (same address) has successfully been allocated and deallocated before. I'm quite convinced that this is a bug somewhere in the system code.

Johannes,If memory serves me, the pointer corruption I've seen varied between the whole pointer being set to 0 and being set to various garbage values. Obviously, you're seeing something else. I'd be really interested in hearing more about your debugging efforts.
Ori Pessach
Also, out of curiosity: Which audio driver are you using to reproduce this?
Ori Pessach
The low-byte pointer corruption here corresponds to the MS Connect link posted by Stefan above
Roddy
Yes, I see that now.
Ori Pessach
A: 

Use Application Verifier to figure out what's going on, if you do something suspicious, it will catch it much earlier.

Paul Betts
Link? The link from http://technet.microsoft.com/en-us/library/bb457063.aspx is broken.
Ori Pessach
Hmmm, weird - I'll report it to MSDN
Paul Betts
+2  A: 

You're not alone with this issue: http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=100589

Stefan
It's nice to see that the issue has been reported in 2005, and that it has been closed.
Ori Pessach
It's closed as "External" which means that it is either a bug inside a driver not from MS, or another component part that isn't developed my MS. My guess is that it's a driver bug - there are not many non-driver components in Windows that are not from MS.
Stefan
Gotcha. It would have been nice to get some more information about it, such as which driver it is that's causing this.
Ori Pessach
My analysis was bad, whups! :/
MSN
This probably isn't a driver issue, unless it happens with multiple drivers. The bug description also makes it clear that it's a Windows issue - the call to IsBadWritePtr() happens in the helper thread, not in the driver.
Ori Pessach
Pardon the thread revivification, but are you sure "closed as external" means that the issue is outside Microsoft in this case? The bug was filed against Visual Studio, and that page says that "closed as external" means it was an issue outside the control of this *product team*". It seems that if the issue is in Windows proper, then it would be considered "external" to the Visual Studio product team.
Tim Lesher
@Tim Lesher - I'm pretty sure it is in Windows. I reported it elsewhere (it's been a while, and I can't remember where) and it was closed as "dead code" or some such. I guess Microsoft isn't very keen on fixing Windows XP.
Ori Pessach
A: 

It may be helpful to look at the source code for Wine, although it's possible that Wine has fixed whatever bug there is, and it's also possible Wine has other bugs in it. The relevant files are dlls/winmm/winmm.c, dlls/winmm/lolvldrv.c, and possibly others. Good luck!

Adam Rosenfield
I usually do, actually - I contributed to Wine in the past, and I love having it around as a form of documentation. I doubt that Wine would have the same bug in this case, though - bug compatibility in Wine only goes so far.
Ori Pessach
+2  A: 

Now, the reason that GlobalHandle() bombs is not due to a heap corruption, as I suspected at first - it's because waveOutWrite() returned without setting the first pointer in the internal structure to a valid pointer. I suspect that it frees the memory pointed to by that pointer before returning, but I haven't disassembled it yet.

I can reproduce this with your code on my system. I see something similar to what Johannes reported. After the call to WaveOutWrite, hdr->reserved normally holds a pointer to allocated memory (which appears to contain the wave out device name in unicode, among other things).

But occasionally, after returning from WaveOutWrite(), the byte pointed to by hdr->reserved is set to 0. This is normally the least significant byte of that pointer. The rest of the bytes in hdr->reserved are ok, and the block of memory that it normally points to is still allocated and uncorrupted.

It probably is being clobbered by another thread - I can catch the change with a conditional breakpoint immediately after the call to WaveOutWrite(). And the system debug breakpoint is occurring in another thread, not the message handler.

However, I can't cause the system debug breakpoint to occur if I use a callback function instead of the windows messsage pump. (fdwOpen = CALLBACK_FUNCTION in WaveOutOpen() ) When I do it this way, my OnWOMDone handler is called by a different thread - possibly the one that's otherwise responsible for the corruption.

So I think there is a bug, either in windows or the driver, but I think you can work around by handling WOM_DONE with a callback function instead of the windows message pump.

AShelly
Check out Stefan's answer which points to a bug report that provides more information about this. You're right - it's either in Windows or in the Driver, and your analysis is nearly identical to the one in the bug report. Last bit of info I can't get from anyone here: Which audio driver do you use?
Ori Pessach
I'm using Analog Devices' SoundMAX Integrated Digital Audio 5.12.1.4060. I only read the bug report after I did most of this work. I posted anyway because I thought you might find the workaround useful.
AShelly
Thanks. I'm using a different driver, so either both drivers have the same bug, or this is in Windows, after all. Frankly, I don't see how it can be anything other than a Windows bug, based on the description in the bug report.
Ori Pessach
A: 

Any answer on this?

Yes. Check out the selected answer for a bug report that shows it's a Windows bug. If you're looking for a workaround, then rewriting the wave output code to use DirectAudio did the trick for me.
Ori Pessach
A: 

What about the fact that you are not allowed to call winmm functions from within callback? MSDN does not mention such restrictions about window messages, but usage of window messages is similar to callback function. Possibly, internally it's implemented as a callback function from the driver and that callback does SendMessage. Internally, waveout has to maintain linked list of headers that were written using waveOutWrite; So, I guess that:

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

sets previous/next pointers of the linked list or something like this. If you write more buffers, then if you check the pointers and if any of them point to one another then my guess is most likely correct.

Multiple sources on the web mention that you don't need to unprepare/prepare same headers repeatedly. If you comment out Prepare/unprepare header in the original example then it appears to work fine without any problems.

pps
The use of window messages is unlike using a callback function in one important respect: They're not invoked at interrupt time, and you can use pretty much any API from them.
Ori Pessach
In any case, even if you treat window message as a callback function it will still raise the same exception. Removing repeatative prepera/unprepare header seems like the only way to fix the problem.
pps
I'll have to review the documentation, but as far as I can remember the prepare/unprepare calls are necessary.
Ori Pessach