spatialaudio / python-sounddevice

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

Sounddevice Thread handle count increases every time sd.play is called #140

Open joedeller opened 6 years ago

joedeller commented 6 years ago

I have a long running script running python 3.6 32bit on windows (7 and 10, 64bit), using sounddevice sounddevice==0.3.11 It plays sine waves from a numpy array on a regular basis. However each time it does this, it creates two additional thread handles that persist after the sound has played. Whilst these are cleaned up when the python script ultimately exits, when it is running, I've seen the handle count go >20K, with the result that at various points in time the script just hangs, sometimes in sd.wait, sometimes in other parts of the script. Different systems seem to hang at different points, but generally as the python.exe handle count is in the thousands.

I've also noticed that

sd._terminate()
sd._initialize()

do the same thing, which is even more problematic for me as I have to call them just before I play a sound to ensure that the device and buffersize are correct, so I get at least 4 additional thread handles each time I play a sound.

I've got a cut down version of my script to show the problem. I appreciate that this may well be down in portaudio or another helper library.

I've had a quick dabble with PyAudio and it doesn't seem to suffer from the same problem, but it's a far less friendly library to use :-(

Regards,

Joe

import numpy as np
import sounddevice as sd
import time
from collections import defaultdict
import psutil as ps

start = ps.Process().num_handles()
print ("Start handles = {}".format(start))

# Set this to your sound card as appropriate
sd.default.device = "Focusrite USB ASIO"

sample_rates=[44100,48000]
wave_tables = defaultdict(dict)

def generate_wavetables():
    duration = 1
    volume = 0.4  

    for sample_rate in sample_rates:
        fade_time = int(sample_rate / 10)
        fade_in = np.arange(0., 1., 1 / fade_time)
        fade_out = np.arange(1., 0., -1 / fade_time)

        sine_wave = (volume * np.sin(2 * np.pi * np.arange(sample_rate * duration) * 997 /  sample_rate)).astype(
            np.float32)
        sine_wave[:fade_time] = np.multiply(sine_wave[:fade_time], fade_in)
        sine_wave[-fade_time:] = np.multiply(sine_wave[-fade_time:], fade_out)

        wave_tables[sample_rate][997] = sine_wave

def play_and_get_level(sample_rate=44100, freq=997, channels=2, inputs=2, check_locked=False):    
    sd.default.channels = 1
    sd.default.latency = 'low'    
    out_channels = np.linspace(1, channels, channels, dtype=int)    
    note = wave_tables[sample_rate][freq]

    print("About to play a sound of {}Hz at sample rate {}Hz".format(freq, sample_rate))    
    try:        
        sd.play(note, mapping=out_channels, blocking = True)        
    except Exception as e:
        print ("oops {}".format(e))        
        return None

generate_wavetables()

for i in range(100):
    play_and_get_level()
    print ("handles {}".format( ps.Process().num_handles()))
mgeier commented 6 years ago

Thanks for this bug report!

It seems to affect Windows only, since psutil.Process.num_handles() only exists in Windows.

I currently don't have access to a Windows system, so I can't test it myself.

I think you should try to reproduce the problem using sd.OutputStream instead of sd.play(). It probably doesn't matter if any sound is played at all. You should try what num_handles() returns after creating an OutputStream and after calling start(), stop() and close() on it.

It may matter, however, whether you use a callback function or not. You should try both cases.

You could then directly compare the behavior with PyAudio's behavior.

Importing sounddevice automatically does initialize PortAudio, while in PyAudio you have to explicitly initialize it with

p = pyaudio.PyAudio()

Creating a pyaudio.Stream works similarly, but the arguments are somewhat different. You can then use start_stream(), stop_stream() and close().

p.terminate() should do the same thing as sd._terminate().

If the behavior of num_handles() is the same in both, this looks like a PortAudio bug. If PyAudio doesn't show an increase in num_handles(), this is probably a bug in CFFI or in how sounddevice uses CFFI. You should also check if PyAudio and the sounddevice module use the same version of PortAudio.

joedeller commented 6 years ago

Hi Matthias, thanks for getting back to me. I've done a quick test using OutputStream without playing any sound and the handle count does increase by one each time. I will look at the other scenarios and get back to you when I have a bit more time. I do have a workaround by using subprocess to launch my sound playing code as a separate python process, which cleans up after it exits, so there isn't a steady leak. It's a bit klunky but works for now.

Thanks, Joe

def play_stream(): print ("Pre create handles {}".format( ps.Process().num_handles())) time.sleep(.5) stream =sd.OutputStream() print ("Post create, pre start handles {}".format( ps.Process().num_handles())) time.sleep(0.5) stream.start() print ("Post start handles {}".format( ps.Process().num_handles())) time.sleep(0.5) stream.stop() print ("Post stop handles {}".format( ps.Process().num_handles()))

stream.close()

The handle count does climb Start handles = 311 handles before stream init 313 Post init, pre start stream 321 Post start stream 339 Post stop stream 322 After closing stream 316 handles before stream init 316 Post init, pre start stream 322 Post start stream 340 Post stop stream 323 After closing stream 317 handles before stream init 317 Post init, pre start stream 323 Post start stream 341 Post stop stream 324 After closing stream 318 handles before stream init 318 Post init, pre start stream 324 Post start stream 342 Post stop stream 325 After closing stream 319 handles before stream init 319 Post init, pre start stream 325 Post start stream 343 Post stop stream 326 After closing stream 320

On Sat, May 26, 2018 at 10:25 AM, Matthias Geier notifications@github.com wrote:

Thanks for this bug report!

It seems to affect Windows only, since psutil.Process.num_handles() only exists in Windows.

I currently don't have access to a Windows system, so I can't test it myself.

I think you should try to reproduce the problem using sd.OutputStream instead of sd.play(). It probably doesn't matter if any sound is played at all. You should try what num_handles() returns after creating an OutputStream and after calling start(), stop() and close() on it.

It may matter, however, whether you use a callback function or not. You should try both cases.

You could then directly compare the behavior with PyAudio's behavior.

Importing sounddevice automatically does initialize PortAudio, while in PyAudio you have to explicitly initialize it with

p = pyaudio.PyAudio()

Creating a pyaudio.Stream https://people.csail.mit.edu/hubert/pyaudio/docs/#class-stream works similarly, but the arguments are somewhat different. You can then use start_stream() https://people.csail.mit.edu/hubert/pyaudio/docs/#pyaudio.Stream.start_stream, stop_stream() https://people.csail.mit.edu/hubert/pyaudio/docs/#pyaudio.Stream.stop_stream and close() https://people.csail.mit.edu/hubert/pyaudio/docs/#pyaudio.Stream.close.

p.terminate() should do the same thing as sd._terminate().

If the behavior of num_handles() is the same in both, this looks like a PortAudio bug. If PyAudio doesn't show an increase in num_handles(), this is probably a bug in CFFI or in how sounddevice uses CFFI. You should also check if PyAudio and the sounddevice module use the same version of PortAudio.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/spatialaudio/python-sounddevice/issues/140#issuecomment-392249389, or mute the thread https://github.com/notifications/unsubscribe-auth/AD-qEmOv7H2Iae5PfSLmpk7S7Mr6tjCbks5t2R-hgaJpZM4UL3qp .