Crackling/clicking at the end of signal when using playback without callback
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
- Different sampling rates: 44100/48000
- Different audio devices (USB wireless headset, onboard digital out, onboard analog out)
- using play() method instead of Stream.write()
- Changing amplitude of the beep (0.1 == -20dB above)
- Loudness of the click at the end changes proportionally
My system
- Ubuntu 22.04 LTS with Pulseaudio
- Everything stock and ~~I do not have such issues in any other app~~ - see update
- libportaudio2/jammy,now 19.6.0-1.1 amd64 [installed]
- Python 3.11
Thank you already for the help. Edit: Addition to "What I have tested"
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
I updated the code to print out the return value "underflowed (bool)". It returns False. Yet the audio click is clearly audible.
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.
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.
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?
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.