python-sounddevice icon indicating copy to clipboard operation
python-sounddevice copied to clipboard

Crash with no exception/error when using OutputStream callback

Open W3AXL opened this issue 4 years ago • 1 comments

(I originally posted this on stackoverflow, but reposting here since I think it's something inside sounddevice crashing)

I've managed to somehow cause a hard crash of my python script with no exceptions or errors thrown, and could use some help determining if it's a sounddevice issue or not.

Here's a very basic outline of the part of the code that's crashing:

import sounddevice as sd
import numpy as np

micSampleQueue = queue.Queue()

audioStream = sd.OutputStream(samplerate=48000, channels=1, callback=outputCallback, blocksize=512, dtype=np.float32)
audioStream.start()

def outputCallback(outdata, frames, time, status):
    # print the device status if there is one
    if status:
        print(status)
    # only get samples if we've got a good buffer built up
    if micSampleQueue.qsize() > 2:
        print("outputCallback()")
        samples = micSampleQueue.get_nowait()
        outdata[:,0] = samples
    else:
        outdata.fill(0)

def handleMicData(dataString):
    print("handleMicData()")
    # Split into a list of strings
    stringList = dataString.split(",")
    # Remove empty strings
    stringList[:] = [item for item in stringList if item]
    # Convert string list to floats
    floatArray = np.asarray(stringList, dtype=np.float32)
    # put the data to the queue
    micSampleQueue.put_nowait(floatArray)

handleMicData() is called whenever a websocket listener in the script receives a new string of audio samples. It converts this comma-separated float string into an actual array of floats, and puts it into the micSampleQueue. The outputCallback get()s from this queue. The two log prints are in there so I can attempt to see what's happening. Here's an example of what a crash looks like in my console:

handleMicData()
handleMicData()
handleMicData()
outputCallback()
outputCallback()
outputCallback()handleMicData()

handleMicData()
handleMicData()
outputCallback()
outputCallback()
outputCallback()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
outputCallback()
outputCallback()
outputCallback()
handleMicData()
handleMicData()
outputCallback()
outputCallback()
outputCallback()handleMicData()

handleMicData()
handleMicData()
outputCallback()
outputCallback()
outputCallback()
handleMicData()
handleMicData()
handleMicData()
outputCallback()
outputCallback()
outputCallback()
handleMicData()
handleMicData()
handleMicData()
outputCallback()
outputCallback()
outputCallback()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
handleMicData()
(python-radio-console) PS H:\Documents\GitHub\python-radio-console>

No error or exception is being thrown as far as I can see, unless it's getting surpressed somewhere in the sounddevice library. At some point the outputCallback() stops getting called, the script hangs for a few seconds, and then exits to the terminal. The fact that my two print statements sometimes get put on the same line makes me think I'm hitting some kind of strange race condition.

The time before the crash happens also varies quite a bit. Sometimes it'll happen within a few sample callbacks, but other times I can go tens of seconds with perfectly fine audio before things break. Interestingly, if remove the log prints I can make it happen immediately almost every time. So it's definitely a very timing-dependent crash if the time it takes to print a log message affects things.

Any help or suggestions for additional troubleshooting would be greatly appreciated!

W3AXL avatar Aug 17 '21 22:08 W3AXL

One problem is that get_nowait() throws an Empty exception if the queue is empty and since this exception is not caught, it escapes from the callback function which means that the callback function isn't called anymore.

You should catch this exception (as shown in several of the example programs).

No error or exception is being thrown as far as I can see, unless it's getting surpressed somewhere in the sounddevice library.

See the documentation of the callback argument for how exceptions are handled: https://python-sounddevice.readthedocs.io/en/0.4.2/api/streams.html#sounddevice.Stream.

The fact that my two print statements sometimes get put on the same line makes me think I'm hitting some kind of strange race condition.

This is indeed a race condition, but it isn't very strange: The callback function runs in a separate thread, and if you print() from both threads, the output may appear mangled. This is very much expected, but in your finished code you should not print() in the callback function anyway, because it might lead to audio drop-outs (except in case of an error, where the audio output is probably broken already).

The time before the crash happens also varies quite a bit. Sometimes it'll happen within a few sample callbacks, but other times I can go tens of seconds with perfectly fine audio before things break.

That's also expected. Depending on system load, the queue might be filled faster or slower. You might want to add some pre-filling to avoid an empty queue (or more exactly: to reduce the probability of an empty queue).

Interestingly, if remove the log prints I can make it happen immediately almost every time.

Yes, printing can be surprisingly slow, which can have a big impact on multi-threaded programs.

mgeier avatar Aug 19 '21 18:08 mgeier