develop ← emezeske:pr/trailing-audio-callback-after-stop
opened 09:05PM - 14 Aug 26 UTC
On Windows, changing or closing the audio device while WASAPI is running can cra…sh the audio thread inside AudioProcessorPlayer with an access violation on a null pointer. I have one report of it from the field, so it is not just a theoretical race.
To reproduce you want a WASAPI device with more inputs than outputs (mine is 8 in, 2 out) driving a standalone app through AudioDeviceManager and AudioProcessorPlayer. Start audio, then switch the device in the audio settings or quit. It is intermittent because it needs the device thread to be parked on its client event at the moment stop() runs.
The faulting frame and the registers from the minidump:
```
EXCEPTION_ACCESS_VIOLATION_WRITE at 0x20
memcpy
initialiseIoBuffers juce_AudioProcessorPlayer.cpp:94
AudioProcessorPlayer::audioDeviceIOCallbackWithContext juce_AudioProcessorPlayer.cpp:245
rbx=3 (i) r12=2 (outs.size()) rsi=8 (totalNumChannels) r14=0x780 (1920 bytes, 480 samples)
r8=0x760 with the fault at 0x20, so the destination was null plus the 32 byte head
```
What happens is that the device delivers one more callback after audioDeviceStopped() has already run. audioDeviceStopped() shrinks tempBuffer with setSize (1, 1), and initialiseIoBuffers then asks that buffer for a write pointer for every processor channel that has no matching system output. AudioBuffer null terminates its channel array and getWritePointer does not bounds check in release builds, so channel 3 of 8 came back null and took a full block memcpy. Channel 2 had already overrun the one sample buffer just before it.
There are two commits. The first makes AudioProcessorPlayer return silence when currentDevice is null, which is the state audioDeviceStopped() leaves behind, and does it before the buffers are set up rather than relying on the jassert that is already there. The second fixes the WASAPI side, which is where the trailing callback comes from: the run loop tests flagStarted against a snapshot taken at the top of the iteration, then blocks for up to a second waiting on the device event before it reaches that test, and in that gap stop() can clear the flag, release startStopLock and call audioDeviceStopped(). Re-reading the flags inside the try lock closes it. The AudioProcessorPlayer change is still worth having on its own, since any backend that can deliver a late callback hits the same buffers.