#!/usr/bin/env python3
"""
Apply a biquad IIR filter to a WAV file (e.g. for sweep-test equalization).
Biquad coefficients follow the standard form:
b0, b1, b2, a0, a1, a2
as used in SoX-style "biquad" definitions.
Spyder usage:
Just set INPUT_WAV and OUTPUT_WAV below, then hit Run (F5).
"""
import numpy as np
from scipy.io import wavfile
from scipy.signal import lfilter
# --- Your biquad coefficients ---
# biquad-m track b0 b1 b2 a0 a1 a2
B0 = 0.73137993061956
B1 = -0.39063797071820
B2 = -0.27569578143734
A0 = 1.0
A1 = -0.59106473412185
A2 = -0.39977762693709
b = [B0, B1, B2]
a = [A0, A1, A2]
# --- Set your file names here (must be in the same folder as this script) ---
INPUT_WAV = "sweep.wav"
OUTPUT_WAV = "sweep_eq.wav"
def apply_biquad(in_path: str, out_path: str):
rate, data = wavfile.read(in_path)
orig_dtype = data.dtype
is_int = np.issubdtype(orig_dtype, np.integer)
# Work in float64 for filtering precision
if is_int:
max_val = np.iinfo(orig_dtype).max
x = data.astype(np.float64) / max_val
else:
x = data.astype(np.float64)
max_val = 1.0
# Handle mono vs multi-channel
if x.ndim == 1:
y = lfilter(b, a, x)
else:
y = np.zeros_like(x)
for ch in range(x.shape[1]):
y[:, ch] = lfilter(b, a, x[:, ch])
# Guard against clipping after filtering
peak = np.max(np.abs(y))
if peak > 1.0:
print(f"Warning: filtered signal peak = {peak:.3f}, normalizing to avoid clipping.")
y = y / peak
if is_int:
y_out = np.clip(y * max_val, np.iinfo(orig_dtype).min, np.iinfo(orig_dtype).max)
y_out = y_out.astype(orig_dtype)
else:
y_out = y.astype(orig_dtype)
wavfile.write(out_path, rate, y_out)
print(f"Wrote filtered file: {out_path} ({rate} Hz, {y_out.shape})")
if __name__ == "__main__":
apply_biquad(INPUT_WAV, OUTPUT_WAV)