← Back to blog
March 20267 min read

What I Learned Building a Parametric EQ in Python

A practical breakdown of filter design, Q-factor tradeoffs, and audio processing pitfalls I ran into while building an EQ from scratch.

PythonDSPAudio EngineeringSciPy

Why I Built It

I wanted to understand how EQ behavior is actually created in code instead of treating plugins like a black box.

Building a parametric EQ manually forced me to think in terms of center frequency, bandwidth, gain, and how those parameters affect real audio data sample-by-sample.

Big Technical Lessons

  • Second-order-section filters are safer for audio work than direct-form coefficients because they are more numerically stable.
  • Q controls bandwidth in a non-intuitive way at first; visualizing low Q vs high Q behavior helped me tune filter ranges faster.
  • Splitting the signal into low-pass, band-pass, and high-pass regions made it easier to reason about what was being boosted or cut.
  • Sample rate awareness is critical: every cutoff and design parameter must be normalized correctly or results will sound wrong.
  • Stereo-safe processing means always filtering across axis 0 carefully and preserving channel structure.
High-pass filter helper
def high_pass_filter(data, cutoff_frequency, sample_rate, order=5):
    sos = butter(order, cutoff_frequency / (sample_rate / 2),
                 btype='high', output='sos')
    filtered_data = sosfilt(sos, data, axis=0)
    return filtered_data
Converting center frequency + Q into a usable bandwidth
def convert_to_q(crit_freq, q, range=[1,20000]):
    half_bandwidth = ((crit_freq/q)/2)
    a = crit_freq - half_bandwidth
    b = crit_freq + half_bandwidth

    # Make sure it does not go out of bounds of audio perceivable sound
    if a < range[0]:
        a = range[0]

    return [a, b]

The Data Type Problem

Most DSP operations run in floating point, but WAV files are often int16. If I wrote float output directly without conversion, playback broke or clipped unpredictably.

The reliable flow was: process in float, verify levels, then cast back to the original dtype before writing the file.

Parametric EQ core processing
def parametric_eq(data, center_frequency, Q, gain, sample_rate):
    # Define the bandwidth for boosting/cutting
    bandwidth = convert_to_q(center_frequency, Q)
    print(f"This is the bandwidth range: {bandwidth}")

    # Bandpass filter to be affected
    sos_bandpass = iirfilter(N=2, Wn=bandwidth, btype='band', ftype='butter', analog=False, fs=sample_rate, output='sos')
    processed_data_bandpass = sosfilt(sos_bandpass, data, axis=0)
    processed_data_bandpass *= 10**(gain / 20.0)

    # Design high-pass filter for frequencies below the specified range
    sos_highpass = iirfilter(N=1, Wn=bandwidth[1], btype='high', ftype='butter', analog=False, fs=sample_rate, output='sos')
    processed_data_highpass = sosfilt(sos_highpass, data, axis=0)

    # Design low-pass filter for frequencies above the specified range
    sos_lowpass = iirfilter(N=1, Wn=bandwidth[0], btype='low', ftype='butter', analog=False, fs=sample_rate, output='sos')
    processed_data_lowpass = sosfilt(sos_lowpass, data, axis=0)

    # Return the summation of all processed segments
    return processed_data_highpass + processed_data_lowpass + processed_data_bandpass
Preserving WAV compatibility with dtype casting
print(f"This is the old dtype of new_audio: {new_audio.dtype}")
print(f"This is the dtype of audio_data: {audio_data.dtype}")

# Cast the processed data to the original data type
new_audio = new_audio.astype(audio_data.dtype)

print(f"This is the new dtype of new_audio: {new_audio.dtype}")
wavfile.write('testing_output.wav', rate, new_audio)

What I Would Improve Next

  • Add output gain compensation and limiter-style safety to reduce clipping risk after boosts.
  • Graph the frequency response so tuning center frequency and Q is faster than trial-and-error listening.
  • Expand to multi-band EQ with preset support for voice cleanup, music shaping, and live playback contexts.
  • Package the code into a reusable module/CLI so it can process folders of files in one run.

Takeaway

The biggest win was confidence: after building it from scratch, I understand what EQ parameters are really doing and can make better engineering decisions in both software and live audio workflows.