spatialaudio / python-sounddevice

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

sounddevice.play vs manual OutputStream - samplerate confusion #370

Closed ethancrawford closed 3 years ago

ethancrawford commented 3 years ago

When I use the convenience function play to play a wav file at a particular samplerate, it seems to play back at the correct rate. However, when I attempt to play the same wav file by manually creating an OutputStream with the same samplerate, the sample seems to play back at far too high a rate.

To the best of my knowledge, I set things up correctly, but I'm wondering if I'm missing something obvious. I'd appreciate some advice if so. Below are some details - pardon me if I've left anything important out, I can update this (such as with a link to the sound file itself) if needed.

Platform/API: macOS/Core Audio Audio device: Scarlett 4i4 USB (6 in, 4 out) (device 3 according to query_devices()) Python module versions:

% python3 -m pip show sounddevice
Name: sounddevice
Version: 0.4.2 ...

% python3 -m pip show soundfile
Name: SoundFile
Version: 0.10.3.post1 ...

The minimal(ish) script that reproduces this on my system:

import sounddevice
import soundfile
data_type = "int16"
file = soundfile.read("samples/misc_crow.wav", dtype=data_type)
data, sample_rate = file # sample_rate is 44100
sounddevice.play(data, sample_rate, device=3) # sounds fine
sounddevice.wait()

output = sounddevice.OutputStream(device=3, dtype=data_type, samplerate=sample_rate)
output.start()
output.write(data) # sounds much too fast/high pitched
output.close()
HaHeho commented 3 years ago

A minimal example (not too much, if you neglect all the input parameter parsing) is shown in play_file.py.

There you see that writing to the stream actually happens in the callback() function which is automatically triggered in the appropriate pace (depending on chunk size and sampling frequency). The code becomes much more complicated obviously, but the examples provide a good assistance to learn how to. And in this way, you may have full control over each individual chunk being delivered in time to the output.

It gets even more funky with an additional queues being filled, if you want to playback a very long file (one that may not fit into memory all at once) with this advanced method in play_long_file.py.

ethancrawford commented 3 years ago

Ah, yes. Thanks for pointing all that out - good to know 🙂

mgeier commented 3 years ago

I'm wondering if I'm missing something obvious.

No, you might have found a bug.

Is your file a mono file?

It looks like when data is one-dimensional, the Stream.write() function doesn't check whether the stream has one channel.

In your case, the stream has 4 channels, so the audio data is distributed over 4 channels, each one being four times faster (i.e. two octaves higher) than intended (and probably having some aliasing artifacts).

So I think this is a bug.

As a work-around, you can use channels=1 when creating the OutputStream.

ethancrawford commented 3 years ago

Oh! It was indeed a mono sample. Thanks for the clarification and workaround 😄

mgeier commented 3 years ago

I've just created #372 which hopefully fixes this issue.

@ethancrawford Can you please check if that works for your example?

And by "work" I mean it should raise a meaningful error instead of playing back the wrong thing.

ethancrawford commented 3 years ago

🙂

As requested: Running the code snippet in my original comment plays a sound for the call to .play, and for the manual OutputStream, produces the following:

Traceback (most recent call last):
  File "/Users/ethan/Desktop/test_sound.py", line 13, in <module>
    output.write(data)
  File "/Users/ethan/.pyenv/versions/3.9.0/lib/python3.9/site-packages/sounddevice.py", line 1532, in write
    raise ValueError('number of channels must match')
ethancrawford commented 3 years ago

(After running the following commands to bring in your fix of course):

python3 -m pip uninstall sounddevice
python3 -m pip install git+https://github.com/mgeier/python-sounddevice.git@write-one-dimensional
mgeier commented 3 years ago

Thanks for testing! I've just merged #372. If there's anything else not working, please let me know!