0

I'm currently making a DSP project with Python, which is equalizing in 10 bands with knob UI made by tkinter. This is my Audio Processing Logic with using RFFT and IRFFT, and streaming with sounddevice and soundfile. When I adjust any knob, disturbing but not that big clicking or popping sound appears periodically. So, I increased buffer as much as I can to reduce these sounds, but it won't disappear. Any ideas for reducing these annoying sounds perfectly?

<Audio Processing Logic>

import sounddevice as sd
import soundfile as sf
import numpy as np
import sys

class AudioPlayer_sounddevice:
def __init__(self):
    self.current_frame = 0
    self.playback_active = False
    self.playback_finished = False
    self.stream = None
    self.fs = None
    self.volume = 1.0
    self.file = None
    self.bands = [(40, 80), (80, 160), (200, 300), (400, 600), (800, 1200),
                  (1600, 2400), (3000, 5000), (7000, 9000), (11000, 14000), (15000, 17000)]
    self.gain = [0] * len(self.bands)
    self.overlap_buffer = None
    self.frame_size = 8192*4  # I increased overlap frame size
    self.overlap_size = self.frame_size // 2  # as a half size of frame size
    
def adjust_volume(self, volume):
    self.volume = max(0.0, min(1.0, volume))

def adjust_gain(self, former, sliders):
    self.gain = sliders


def load(self, filepath):
    self.filepath = filepath
    self.file = sf.SoundFile(self.filepath, mode='r')
    self.fs = self.file.samplerate
    self.current_frame = 0
    self.playback_active = False
    self.playback_finished = False

def play(self):
    self.playback_finished = False
    if not self.playback_active:
        self.playback_active = True
        if self.playback_finished or self.stream is None:
            self.current_frame = 0
            self.playback_finished = False
            self._start_stream()
        else:
            self._start_stream()
    elif self.playback_finished:
        self.current_frame = 0
        self.playback_finished = False
        self._start_stream()

def _start_stream(self):
    if self.stream is not None:
        self.stream.stop()
        self.stream.close()
    self.stream = sd.OutputStream(callback=self.audio_callback, samplerate=self.fs, channels=self.file.channels, blocksize=8192*4) # I increased buffer size
    self.stream.start()
        
# This function is important, which has the main Audio Processing Logic
def audio_callback(self, outdata, frames, time, status):
    if status:
        print(status, file=sys.stderr)
    if not self.playback_active:
        outdata.fill(0)
        return

    data = self.file.read(frames, dtype='float32', always_2d=True)
    read_frames = data.shape[0]

    # Overlap Manipulation
    if self.overlap_buffer is not None:
        data[:self.overlap_size] += self.overlap_buffer

    # FFT Transformation
    freq_data = np.fft.rfft(data, axis=0)
    freq_bins = np.fft.rfftfreq(frames, d=1/self.fs)
    
    # Band Gain Filtering
    for i, band in enumerate(self.bands):
        start_idx = np.searchsorted(freq_bins, band[0])
        end_idx = np.searchsorted(freq_bins, band[1])
        gain_linear = 10 ** (self.gain[i] / 20)
        freq_data[start_idx:end_idx] *= np.float32(gain_linear)
        
    # IFFT Transformation
    processed_data = np.fft.irfft(freq_data, n=frames, axis=0)
    # processed_data = self.butter_lowpass_filter(processed_data, cutoff=30000, fs=self.fs)
    
    # processed_data's real length calculation
    processed_length = processed_data.shape[0] - 2 * self.overlap_size

    # Real data length + Padding
    required_length = frames - processed_length

    # Flattening outdata's size
    if required_length > 0:
        padding = np.zeros((required_length, data.shape[1]), dtype=processed_data.dtype)
        outdata[:] = np.concatenate((processed_data[self.overlap_size:-self.overlap_size], padding))
    else:
        # if padding isn't needed
        outdata[:] = processed_data[self.overlap_size:self.overlap_size+frames]
    
    if read_frames < frames:
        outdata[:read_frames] = processed_data[:read_frames] * np.float32(self.volume * 0.7)
        outdata[read_frames:] = 0
        self.playback_finished = True
        self.playback_active = False
        raise sd.CallbackStop
    else:
        outdata[:read_frames] = processed_data * np.float32(self.volume * 0.7)

    self.current_frame += read_frames

def stop(self):
    if self.stream is not None:
        self.stream.stop()
        self.stream.close()
    self.stream = None
    self.playback_active = False
    self.playback_finished = True
    self.current_frame = 0

def pause(self):
    if self.playback_active:
        self.playback_active = False
        if self.stream is not None:
            self.stream.stop()

def unpause(self):
    if not self.playback_active and not self.playback_finished:
        self.play()
2
  • 1
    Hi, welcome to StackOverflow. Please take the tour and learn How to Ask. In order to get help, you will need to provide a minimal reproducible example. In this case, I would really stress the concept of MINIMAL. Don't post your complete code; try making an easy example we can use to understand your problem. Commented Apr 3, 2024 at 10:05
  • 2
    it is not good to change volume in "leaps", you need some fade in/out between current and new volume to fix the clicking problem Commented Apr 3, 2024 at 10:11

1 Answer 1

3

As stated in comment, most likely clicking sound comes from rapidily changing volume. It's good to change volume smoothly, over some period of time.

Since you are using FFT filtering method, adjusting volume in Fourier domain, you can't apply fade in/out there (easily, without boundary problems and convolution, which I assume you wanted to avoid).

Fade in out in time domain

What you can do is first, check if filter parameters have changed. If not, use your current method. If yes do 3 more steps:

  1. calculate new filtered samples buffer with old filter parameters -> s_old
  2. calculate new filtered samples buffer with new filter parameters -> s_new
  3. make weighted average of both signals by applying fade in on s_new and fade out on s_old
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.