2

I have a problem with Microsoft's WaveOut API:

edit1: Added Link to sample project: edit2: removed link, its not representative of the issue

After playing some audio, when I want to terminate a given playback stream, I call the function:

waveOutClose(hWaveOut_);

However, even after waveOutClose() is called, sometimes the library will still access memory previously passed to it by waveOutWrite(), causing an invalid memory access.

I then tried to ensure all the buffers are marked as done before freeing the buffer:

PcmPlayback::~PcmPlayback()
{
if(hWaveOut_ == nullptr)
    return;

    waveOutReset(hWaveOut_); // infinite-loops, never returns

for(auto it = buffers_.begin(); it != buffers_.end(); ++it)
    waveOutUnprepareHeader(hWaveOut_, &it->wavehdr_, sizeof(WAVEHDR));

while( buffers_.empty() == false ) // infinite loops
    removeCompletedBuffers();

waveOutClose(hWaveOut_);

//Unhandled exception at 0x75629E80 (msvcrt.dll) in app.exe: 
// 0xC0000005: Access violation reading location 0xFEEEFEEE.
}

void PcmPlayback::removeCompletedBuffers()
{
for(auto it = buffers_.begin(); it != buffers_.end();)
{
    if( it->wavehdr_.dwFlags & WHDR_DONE )
    {
        waveOutUnprepareHeader(hWaveOut_, &it->wavehdr_, sizeof(WAVEHDR));
        it = buffers_.erase(it);
    }
    else
        ++it;
}
}

However, this situation never happens - the buffer never becomes empty. There will be 4-5 blocks remaining with wavehdr_.dwFlags == 18 (I believe this means the blocks are still marked as in playback)

How can I resolve this issue?

@ Martin Schlott ("Can you provide the loop where you write the buffer to waveOutWrite?") Its not quite a loop, instead I have a function that is called whenever I receive an audio packet over the network:

void PcmPlayback::addData(const std::vector<short> &rhs)
{
removeCompletedBuffers();

if(rhs.empty())
    return;

// add new data
buffers_.push_back(Buffer());

Buffer & buffer = buffers_.back();
buffer.data_ = rhs;
ZeroMemory(&buffers_.back().wavehdr_, sizeof(WAVEHDR));
buffer.wavehdr_.dwBufferLength = buffer.data_.size() * sizeof(short);
buffer.wavehdr_.lpData = (char *)(buffer.data_.data());
waveOutPrepareHeader(hWaveOut_, &buffer.wavehdr_, sizeof(WAVEHDR)); // prepare block for playback
waveOutWrite(hWaveOut_, &buffer.wavehdr_, sizeof(WAVEHDR));
}
aCuria
  • 6,935
  • 14
  • 53
  • 89

2 Answers2

2

The described behavior can happen if you do not call

waveOutUnprepareHeader

to every buffer you used before you use

waveOutClose

The flagfield _dwFlags seems to indicate that the buffers are still enqueued (WHDR_INQUEUE | WHDR_PREPARED) try:

waveOutReset

before unprepare buffers.

After analyses your code, I found two problems/bugs which are not related to waveOut (funny, you use C++11 but the oldest media interface). You use a vector as buffer. During some calling operations, the vector is copied! One bug I found is:

typedef std::function<void(std::vector<short>)> CALLBACK_FN;

instead of:

typedef std::function<void(std::vector<short>&)> CALLBACK_FN;

which forces a copy of the vector. Try to avoid using vectors if you expect to use it mostly as rawbuffer. Better use std::unique_pointer as buffer pointer.

Your callback in the recorder is not monitored by a mutex, nor does it check if a destructor was already called. The destructing happens during the callback (mostly) which leads to an exception.

For your test program, go back and use raw pointer and static callbacks before blaming waveOut. Your code is not bad, but the first bug already shows, that a small bug will lead to unpredictical errors. As you also organize your buffers in a std::array, I would search for bugs there. I guess, you make a unintentional copy of your whole buffer array, unpreparing the wrong buffers.

I did not have the time to dig deeper, but I guess those are the problems.

Martin Schlott
  • 4,369
  • 3
  • 25
  • 49
  • Calling waveOutUnprepareHeader() on every buffer before waveOutClose() does not resolve the issue. I still get an access violation: Unhandled exception at 0x75629E80 (msvcrt.dll) in app.exe: 0xC0000005: Access violation reading location 0xFEEEFEEE. – aCuria Oct 21 '13 at 10:15
  • Microsoft's Documentation states "This function must be called after the device driver is finished with a data block", which I take to mean when the WHDR_DONE flag is set? (which does not happen in ~PcmPlayback()) – aCuria Oct 21 '13 at 10:34
  • 1
    Thats right. Unpreparing only work with "done" buffer. Try waveOutReset before ass I mentioned it in may extended answer. – Martin Schlott Oct 21 '13 at 10:40
  • This does not work either - waveOutReset() never returns and instead infinite-loops somewhere inside ntdll.dll I added the location where I added waveOutReset() to my question. – aCuria Oct 21 '13 at 10:45
  • 1
    Check the "loop" field in your buffer.wavehdr_. Did you zeroset the whole structure before setting your values? – Martin Schlott Oct 21 '13 at 10:52
  • I also noticed that during standard operation the buffer size is roughly 6-8. if I stop calling addData(), the buffer will empty itself as expected. The issue occurs when the dtor is called and the buffer is not empty. For some reason the buffer will simply not empty itself inside the dtor... – aCuria Oct 21 '13 at 11:03
  • Do you use the callback which signals a buffer change? Inside this callback winmm operations are not allowed. Also, your buffers seems very small (if you talk in bytes). I got at least 100ms in my buffers (ages ago). – Martin Schlott Oct 21 '13 at 12:20
  • No, I do not use callbacks at all, instead I poll the header. The buffers are small (20ms) to reduce latency. Theres nothing stopping me from using a larger buffer, I have tried 10s. – aCuria Oct 21 '13 at 12:42
  • 1
    @aCuria. I got your sample, but it will take a while before I can look into it. – Martin Schlott Oct 22 '13 at 06:29
  • I wrote little more to my answer – Martin Schlott Oct 22 '13 at 07:44
  • @ Martin Schlott Regarding CALLBACK_FN, you are right, this forces a copy and is inefficient. I should have been more careful here. – aCuria Oct 23 '13 at 08:54
0

I managed to find my problem in the end, it was caused by multiple bugs and a deadlock. I will document what happened here so people can learn from this in the future I was clued in to what was happening when I fixed the bugs in the sample:

  • call waveInStop() before waveInClose() in ~Recorder.cpp
  • wait for all buffers to have the WHDR_DONE flag before calling waveOutClose() in ~PcmPlayback.

After doing this, the sample worked fine and did not display the behavior of the WHDR_DONE flag never being marked.

In my main program, that behavior was caused by a deadlock that occurs in the following situation:

  • I have a vector of objects representing each peer I am streaming audio with
  • Each Object owns a Playback class
  • This vector is protected by a mutex

Recorder callback:

  • mutex.lock()
  • send audio packet to each peer.

Remove Peer:

  • mutex.lock()
  • ~PcmPlayback
  • wait for WHDR_DONE flags to be marked

A deadlock occurs when I remove a peer, locking the mutex and the recorder callback tries to acquire a lock too.

  • Note that this will happen often because the playback buffer is usually (~4 * 20ms) while the recorder has a cadence of 20ms.
  • In ~PcmPlayback, the buffers will never be marked as WHDR_DONE and any calls to the WaveOut API will never return because the WaveOut API is waiting for the Recorder callback to complete, which is in turn waiting on mutex.lock(), causing a deadlock.
aCuria
  • 6,935
  • 14
  • 53
  • 89