spatialaudio / python-sounddevice

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

Crackling/clicking at the end of signal when using playback without callback #482

Open kbasaran opened 10 months ago

kbasaran commented 10 months ago
import numpy as np
import sounddevice as sd
import matplotlib.pyplot as plt
plt.rcParams["figure.dpi"] = 150

class SoundEngine():
    def __init__(self):
        self.FS = sd.query_devices(
            device=sd.default.device,
            kind='output',
            )["default_samplerate"]
        self.start_stream()
        self.test_beep()

    def start_stream(self):
        self.stream = sd.OutputStream(samplerate=self.FS, channels=2, latency='high')
        self.stream.start()

    def beep(self, T, freq):
        t = np.arange(T * self.FS) / self.FS
        y = 0.1 * np.sin(t * 2 * np.pi * freq)

        # pad = np.zeros(100)
        # y = np.concatenate([pad, y, pad])

        y = np.tile(y, self.stream.channels)
        y = y.reshape((len(y) // self.stream.channels, self.stream.channels), order='F')
        y = np.ascontiguousarray(y, self.stream.dtype)
        plt.plot(y[-200:, :]); plt.grid()
        underflowed = self.stream.write(y)
        print("Underflowed: ", underflowed)

    def test_beep(self):
        self.beep(1, 200)

sound_engine = SoundEngine()

It seems like somehow the end of my signal is being clipped which causes an immediate jump to zero amplitude and a click sound.

What I have tested

My system

Thank you already for the help. Edit: Addition to "What I have tested"

mgeier commented 10 months ago

You should always check the return value of write(), see https://python-sounddevice.readthedocs.io/en/0.4.6/api/streams.html#sounddevice.Stream.write

kbasaran commented 10 months ago

I updated the code to print out the return value "underflowed (bool)". It returns False. Yet the audio click is clearly audible.

mgeier commented 10 months ago

OK, that was an important first step.

Now, listening to the output and looking at your code more closely, I have noticed that you don't do any fade in nor fade out, right?

It is expected that you hear a click if you abruptly start or stop a sine wave, which creates a discontinuity in the signal. In signal processing terms, you are applying a "rectangular window" to the sine signal.

You should hear the same click in the play_sine.py example (you may hear it more clearly at lower sine frequencies, as in your example).

To avoid the click, you should create a fade in/out, see e.g. https://nbviewer.org/github/spatialaudio/communication-acoustics-exercises/blob/master/intro.ipynb#Listening-to-the-Signal

A simple linear fade normally suffices.

I guess this should solve the problem, but if not, there can be another reason for audible clicks when starting and stopping a stream, see #455.

kbasaran commented 10 months ago

I do not do fade in/out but I was careful to make the sine wave end at a zero point. This does make a soft pop. But what I hear is a loud click, which seems to have another cause.

To verify this I added 100 samples of zeros to the end of my signal (updated code above with "pad"). I'd expect that I hear the same click if it were about the sine wave's ending. Yet this fixed my problem and the loud click is gone.

I also did the same experiment in Audacity. If I simply generate a sine wave that ends at a zero, I hear the exact same loud click. If I add silence at the end of it, the click is gone.

This I believe points to the issue not being about python-sounddevice.

mgeier commented 10 months ago

I do not do fade in/out but I was careful to make the sine wave end at a zero point.

Yeah, stopping at a zero crossing isn't enough to avoid audible artifacts, as you have witnessed:

This does make a soft pop.

With an appropriate fade, this should also go away.

But what I hear is a loud click, which seems to have another cause.

OK, that's good to know. Did you look at #455 then?

kbasaran commented 10 months ago

Yes I did. Similar conclusion there also, it seems to be an issue with host API. Suggestion is to separate playback endings from stream endings. So I'll add padding to my signals as in the example above. That is a good workaround. Thank you for the help.