spatialaudio / python-sounddevice

:sound: Play and Record Sound with Python :snake:
https://python-sounddevice.readthedocs.io/
MIT License
980 stars 145 forks source link

Sounddevice + big dict + OOP = Segfault / Bus Error #513

Closed theredled closed 3 months ago

theredled commented 6 months ago

I'm having a very strange problem, which I managed to reduce as much as possible like this:

import sounddevice
import time

class SamplerBox:
    def __init__(self):
        self.samples = {}

    def audio_callback(self, outdata, frame_count, time_info, status):
        print('ac')

    def init(self):
        self.connect_audio_output()
        self.load_samples()
        time.sleep(20)

    def connect_audio_output(self):
        try:
            sd = sounddevice.OutputStream(callback=self.audio_callback)
            sd.start()
            print('Opened audio device')
        except:
            print('Invalid audio device')
            exit(1)

    def load_samples(self):
        for midinote in range(128):
            for velocity in range(128):
                self.samples[midinote, velocity] = Sound()

class Sound:
    def __init__(self):
        pass

sb = SamplerBox()
sb.init()

As soon as I create that big self.samples dict, and only create a new audio stream with an empty callback, I get "Bus Error 10" with Python 3.11.

With Python 3.9 I get "Illegal instruction 4"

In my original script (reduced here) I got "Segmentation Fault 11"

I'm running Homebrew Python 3.11 on MacOS 10.15.7.

Worst than that, written in a procedural way, it runs perfectly :

import sounddevice
import time

samples = {}

class Sound:
    def __init__(self):
        pass

def audio_callback(self, outdata, frame_count, time_info, status):
    print('ac')

try:
    sd = sounddevice.OutputStream(callback=audio_callback)
    sd.start()
    print('Opened audio device')
except:
    print('Invalid audio device')
    exit(1)

for midinote in range(128):
    for velocity in range(128):
        samples[midinote, velocity] = Sound()

time.sleep(20)

Any idea?

mgeier commented 6 months ago

This indeed looks quite curious, and I can reproduce it on Linux, however only if I increase the range of the for loops (I got a segmentation fault with 1280, but maybe it also crashes with lower values).

I don't really know what's happening and I think it shouldn't happen ... but I think I know how to avoid the problem:

In your first example, sd is a local variable that goes out of scope at the end of connect_audio_output(), and I guess at some point the Python interpreter destroys the stream object while the callback is still running or something.

You should assign the stream object as an instance variable of the SamplerBox object, something like this:

self.sd = sounddevice.OutputStream(...)
theredled commented 6 months ago

There probably is still something to fix but THANK YOU THIS WORKAROUND WORKS!

Probably some memory allocation mess between those two big data chunks (stream VS dict)? Like some rare bug, more likely to happen here because of their sizes.

mgeier commented 6 months ago

I would consider it a user error if you let an active stream go out of scope, so I don't think I want to "fix" the problem in the sounddevice module.

But this might be helpful to make the error easier to find: #514. Can you please try if that would give you more information in your case?

theredled commented 6 months ago

I would consider it a user error if you let an active stream go out of scope, so I don't think I want to "fix" the problem in the sounddevice module.

But a Segfault means something has not been properly destroyed, isn't it?

mgeier commented 6 months ago

A segfault can mean many things, but in this case I guess it means that something has been destroyed (the stream object), but something else (the audio thread) is still trying to access it.

Ideally, a segfault should never happen in pure Python code, even with completely wrong usage of an API, but I think in this case an exception is merited.

As with all resources in Python, you should ideally manage them with a context manager, and if that's not possible, you should make sure to explicitly close the stream object before letting it go out of scope.

Anything else is grossly negligent, and we cannot reasonably avert a segfault, but at least #514 tries to make it easier to diagnose the problem.

theredled commented 6 months ago

I mean that in pure Python (let's say), if you reference an object A in another object B and then destroy the object, reference in B will be automatically set to None. Or if you reference A.method somewhere, A will not be garbage-collected when out of scope.

But I get that it's probably more low-level than that, thus hard and/or unwanted to be changed. Not an expert so I trust you.

Thanks for #514, not sure how to test it so I'll let others do that!

mgeier commented 6 months ago

Thanks for #514, not sure how to test it

You should create a development installation (https://python-sounddevice.readthedocs.io/en/0.4.6/CONTRIBUTING.html#development-installation), check out the branch from #514, and then try the original situation where you encountered the problem.

so I'll let others do that!

I made #514 as a direct response to your problem, so you are uniquely suited to check if it would help in your situation or not.

I nobody is confirming that #514 would help significantly, I will not merge it.

mgeier commented 5 months ago

@theredled Can you please confirm if #514 improves the situation for you?

theredled commented 5 months ago

I'm sorry I can't test, for some reason I'm not even obtaining the original errors I was getting.

mgeier commented 5 months ago

I'm sorry I can't test, for some reason I'm not even obtaining the original errors I was getting.

Did you try increasing the range in the for-loops as I mentioned above?

mgeier commented 3 months ago

I'm closing this due to lack of response.

If there is any new information, please let me know.