spatialaudio / python-sounddevice

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

Desired Latency #262

Open mattgwwalker opened 3 years ago

mattgwwalker commented 3 years ago

Hi,

I have been struggling with this issue for weeks, and yesterday found (the now obvious) solution.

I was regularly receiving input_underflows when connecting an HDMI output to a Mac in a full-duplex stream. Sometimes dozens of the errors, mostly they occurred at the beginning of the stream.

I was passing "high" as the desired latency to the Stream constructor. I believed that this would be well-defined to always provide sufficient buffering to ensure that the stream was stable. Golly me, how wrong was I?!

I dug into Audacity's code to see how they were achieving such stable performance on the same hardware and all that I could see they were doing was specifying a specific desired latency, which defaults to 100ms.

By copying their approach (dropping the use of "high" and specifying 100ms) I now have very much more robust audio.

Hopefully this comment will help someone else not waste weeks chasing their tail!

As an aside, asking for 100ms seems to be very very much a lower limit (at least on this Mac). My measurements of the total latency with an HDMI output (so from the time it's asked to play to the time it hears itself through the microphone) were 285ms +/- 2ms. The total latency versus the desired latency specified to the Stream constructor is also very non-linear:

image

Given my frustrating experience, perhaps it's worthwhile adding something into the documentation? How would you feel if I added the following into the section "latency" under Stream[1]:

[1] https://python-sounddevice.readthedocs.io/en/0.3.15/api/streams.html#sounddevice.Stream

mgeier commented 3 years ago

There's already a short hint in https://python-sounddevice.readthedocs.io/en/latest/api/module-defaults.html#sounddevice.default.latency:

'high' is typically more robust (i.e. buffer under-/overflows are less likely)

But it would be great if you could make this more clear in the docs!

What is desired_latency in your plot?

Is it the latency you requested from the stream or is it stream.latency?

The Stream docs say (mostly copied from the Portaudio docs):

Where practical, implementations should configure their latency based on this parameter, otherwise they may choose the closest viable latency instead. Unless the suggested latency is greater than the absolute upper limit for the device, implementations should round the latency up to the next practical value – i.e. to provide an equal or higher latency wherever possible. Actual latency values for an open stream may be retrieved using the latency attribute.

This text could definitely be improved as well!

mattgwwalker commented 3 years ago

Hi again,

I just sent a Pull Request with the changes to the documentation of desired latency.

"desired_latency" in the graph above is the number passed to the Stream constructor (measured in milliseconds).

mgeier commented 3 years ago

Thanks for the PR!

"desired_latency" in the graph above is the number passed to the Stream constructor (measured in milliseconds).

OK, then this is very much expected.

The PortAudio docs say (emphasis mine):

Where practical, implementations should configure their latency based on these parameters, otherwise they may choose the closest viable latency instead. Unless the suggested latency is greater than the absolute upper limit for the device implementations should round the suggestedLatency up to the next practical value - ie to provide an equal or higher latency than suggestedLatency wherever possible.

Your plot looks fully conformant, right?

It would probably be more interesting to compare the computed latency (https://python-sounddevice.readthedocs.io/en/0.3.15/api/streams.html#sounddevice.Stream.latency) and the measured latency.

wilsco commented 3 years ago

Hi, Not sure where else to leave this, but I noticed that setting a tuple for input and output latency in a stream isn't working correctly, because in python 3: The builtin basestring abstract type was removed. Use str instead So this could have been having an effect in your full duplex scenario. E.g. sd.Stream(channels=1, callback=callback,latency=(0.1,0.1)).latency does not return (0.1,0.1) Cheers!

mgeier commented 3 years ago

Thanks @wilsco for reporting this!

The builtin basestring abstract type was removed. Use str instead

How exactly did you get this error?

The exception should have been caught by this:

https://github.com/spatialaudio/python-sounddevice/blob/40e6380f93456e3843798eced3189da8780ba092/sounddevice.py#L97-L100

But anyway, we don't support Python 2 anymore, so we don't need this compatibility check and I'm suggesting to remove it in #276.

sd.Stream(channels=1, callback=callback,latency=(0.1,0.1)).latency does not return (0.1,0.1)

What does it return instead?

If this is a different problem than discussed here, can you please open a new issue?

wilsco commented 3 years ago

Ah, I was unclear:

The builtin basestring abstract type was removed. Use str instead

This was not an error I received, but rather a quote from the python docs on changes in Python 3.

For me sd.Stream(channels=1,latency=(0.1,0.1)).latency returns

(0.014512471655328799, 0.10158730158730159)

I only got as far debugging it as to find this basestring error, so it could well be a different problem that is causing my issue. I have opened #280 as you suggest.

Thanks!