Created
February 16, 2025 11:14
-
-
Save FoxeiZ/e08c1eefd2d7efd7652d6f3cf0a541bd to your computer and use it in GitHub Desktop.
optimize parametric eq
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| from scipy import signal | |
| def create_filter_coeffs(filter_type, fc, gain_db, q, fs=48000): | |
| w0 = 2 * np.pi * fc / fs | |
| A = 10 ** (gain_db / 40) | |
| alpha = np.sin(w0) / (2 * q) | |
| if filter_type in ["PK"]: # Peaking EQ | |
| b0 = 1 + alpha * A | |
| b1 = -2 * np.cos(w0) | |
| b2 = 1 - alpha * A | |
| a0 = 1 + alpha / A | |
| a1 = -2 * np.cos(w0) | |
| a2 = 1 - alpha / A | |
| elif filter_type in ["LSC", "LS"]: # Low Shelf | |
| b0 = A * ((A + 1) - (A - 1) * np.cos(w0) + 2 * np.sqrt(A) * alpha) | |
| b1 = 2 * A * ((A - 1) - (A + 1) * np.cos(w0)) | |
| b2 = A * ((A + 1) - (A - 1) * np.cos(w0) - 2 * np.sqrt(A) * alpha) | |
| a0 = (A + 1) + (A - 1) * np.cos(w0) + 2 * np.sqrt(A) * alpha | |
| a1 = -2 * ((A - 1) + (A + 1) * np.cos(w0)) | |
| a2 = (A + 1) + (A - 1) * np.cos(w0) - 2 * np.sqrt(A) * alpha | |
| elif filter_type in ["HSC", "HS"]: # High Shelf | |
| b0 = A * ((A + 1) + (A - 1) * np.cos(w0) + 2 * np.sqrt(A) * alpha) | |
| b1 = -2 * A * ((A - 1) + (A + 1) * np.cos(w0)) | |
| b2 = A * ((A + 1) + (A - 1) * np.cos(w0) - 2 * np.sqrt(A) * alpha) | |
| a0 = (A + 1) - (A - 1) * np.cos(w0) + 2 * np.sqrt(A) * alpha | |
| a1 = 2 * ((A - 1) - (A + 1) * np.cos(w0)) | |
| a2 = (A + 1) - (A - 1) * np.cos(w0) - 2 * np.sqrt(A) * alpha | |
| else: | |
| raise ValueError(f"Unknown filter type: {filter_type}") | |
| return [b0 / a0, b1 / a0, b2 / a0], [1.0, a1 / a0, a2 / a0] | |
| def parse_filter_config(config_text): | |
| filters = [] | |
| for line in config_text.strip().split("\n"): | |
| if line.startswith("Filter: ON"): | |
| parts = line.split() | |
| ftype = parts[2] | |
| fc = float(parts[4]) | |
| gain = float(parts[7]) | |
| q = float(parts[10]) if len(parts) > 10 else 0.707 | |
| filters.append((ftype, fc, gain, q)) | |
| return filters | |
| def plot_eq_response(config_text): | |
| filters = parse_filter_config(config_text) | |
| fs = 48000 | |
| freq = np.logspace(np.log10(20), np.log10(20000), 1000) | |
| w = 2 * np.pi * freq / fs | |
| total_response = np.ones_like(freq) | |
| plt.figure(figsize=(15, 8)) | |
| for ftype, fc, gain, q in filters: | |
| b, a = create_filter_coeffs(ftype, fc, gain, q, fs) | |
| w, h = signal.freqz(b, a, worN=freq, fs=fs) # type: ignore | |
| response_db = 20 * np.log10(np.abs(h)) | |
| total_response += response_db | |
| plt.semilogx(freq, response_db, alpha=0.1, color="blue") | |
| plt.semilogx(freq, total_response, "r-", linewidth=2, label="Combined Response") | |
| plt.grid(True) | |
| plt.xlabel("Frequency [Hz]") | |
| plt.ylabel("Amplitude [dB]") | |
| plt.title("Parametric EQ Frequency Response") | |
| plt.ylim(-30, 20) | |
| plt.axhline(y=0, color="k", linestyle="-", alpha=0.3) | |
| plt.legend() | |
| plt.show() | |
| def merge_filters(filters): | |
| """Merge multiple filters into single equivalent filter""" | |
| # Average frequency weighted by gain magnitude | |
| weights = [abs(f[2]) for f in filters] | |
| fc = np.average([f[1] for f in filters], weights=weights) | |
| # Sum gains | |
| gain = sum(f[2] for f in filters) | |
| # Average Q weighted by gain | |
| q = np.average([f[3] for f in filters], weights=weights) | |
| # Use most common filter type | |
| ftype = max(set(f[0] for f in filters), key=lambda x: filters.count((x))) | |
| return (ftype, fc, gain, q) | |
| def calculate_response(filters, freq, fs): | |
| """Calculate combined frequency response for set of filters""" | |
| total_response = np.zeros_like(freq) | |
| for ftype, fc, gain, q in filters: | |
| b, a = create_filter_coeffs(ftype, fc, gain, q, fs) | |
| w, h = signal.freqz(b, a, worN=freq, fs=fs) # type: ignore | |
| response_db = 20 * np.log10(np.abs(h)) | |
| total_response += response_db | |
| return total_response | |
| def optimize_eq_config(config_text): | |
| # Parse original filters | |
| original_filters = parse_filter_config(config_text) | |
| fs = 48000 | |
| freq = np.logspace(np.log10(20), np.log10(20000), 1000) | |
| # Get original response | |
| orig_response = calculate_response(original_filters, freq, fs) | |
| # Initial filter grouping | |
| grouped_filters = [] | |
| current_group = [] | |
| for i, (ftype, fc, gain, q) in enumerate(original_filters): | |
| if not current_group: | |
| current_group.append((ftype, fc, gain, q)) | |
| continue | |
| prev_fc = current_group[-1][1] | |
| if fc / prev_fc < 1.2: # Group filters within 20% frequency range | |
| current_group.append((ftype, fc, gain, q)) | |
| else: | |
| # Merge group into single filter | |
| if len(current_group) > 1: | |
| merged = merge_filters(current_group) | |
| grouped_filters.append(merged) | |
| else: | |
| grouped_filters.extend(current_group) | |
| current_group = [(ftype, fc, gain, q)] | |
| # Add remaining group | |
| if current_group: | |
| if len(current_group) > 1: | |
| grouped_filters.append(merge_filters(current_group)) | |
| else: | |
| grouped_filters.extend(current_group) | |
| # Generate new config text | |
| optimized_config = "" | |
| for ftype, fc, gain, q in grouped_filters: | |
| optimized_config += ( | |
| f"Filter: ON {ftype} Fc {fc:.0f} Hz Gain {gain:.1f} dB Q {q:.1f}\n" | |
| ) | |
| # Plot comparison | |
| plt.figure(figsize=(15, 8)) | |
| plt.semilogx(freq, orig_response, "b-", label="Original", alpha=0.5) | |
| opt_response = calculate_response(grouped_filters, freq, fs) | |
| plt.semilogx(freq, opt_response, "r--", label="Optimized") | |
| plt.grid(True) | |
| plt.legend() | |
| plt.xlabel("Frequency [Hz]") | |
| plt.ylabel("Amplitude [dB]") | |
| plt.title( | |
| f"Original ({len(original_filters)} filters) vs Optimized ({len(grouped_filters)} filters)" | |
| ) | |
| plt.show() | |
| return optimized_config | |
| def optimize_eq_config_v2(config_text, max_filters=15, tolerance_db=0.5): | |
| original_filters = parse_filter_config(config_text) | |
| fs = 48000 | |
| freq = np.logspace(np.log10(20), np.log10(20000), 1000) | |
| orig_response = calculate_response(original_filters, freq, fs) | |
| grouped_filters = [] | |
| current_group = [] | |
| # Sort filters by frequency for better grouping | |
| sorted_filters = sorted(original_filters, key=lambda x: x[1]) | |
| for ftype, fc, gain, q in sorted_filters: | |
| if not current_group: | |
| current_group.append((ftype, fc, gain, q)) | |
| continue | |
| prev_fc = current_group[-1][1] | |
| if fc / prev_fc < 1.2 and len(grouped_filters) < max_filters: | |
| current_group.append((ftype, fc, gain, q)) | |
| else: | |
| if len(current_group) > 1: | |
| merged = merge_filters(current_group) | |
| grouped_filters.append(merged) | |
| else: | |
| grouped_filters.extend(current_group) | |
| current_group = [(ftype, fc, gain, q)] | |
| # Add remaining group | |
| if current_group: | |
| if len(current_group) > 1: | |
| grouped_filters.append(merge_filters(current_group)) | |
| else: | |
| grouped_filters.extend(current_group) | |
| # Iteratively remove least significant filters until error exceeds tolerance | |
| while len(grouped_filters) > max_filters: | |
| min_error = float("inf") | |
| best_filters = None | |
| # Try removing each filter | |
| for i in range(len(grouped_filters)): | |
| test_filters = grouped_filters[:i] + grouped_filters[i + 1 :] | |
| test_response = calculate_response(test_filters, freq, fs) | |
| error = np.max(np.abs(test_response - orig_response)) | |
| if error < min_error: | |
| min_error = error | |
| best_filters = test_filters | |
| # Stop if error exceeds tolerance | |
| if min_error > tolerance_db: | |
| break | |
| grouped_filters = best_filters | |
| # Generate optimized config | |
| optimized_config = "" | |
| for ftype, fc, gain, q in grouped_filters: | |
| optimized_config += ( | |
| f"Filter: ON {ftype} Fc {fc:.0f} Hz Gain {gain:.1f} dB Q {q:.1f}\n" | |
| ) | |
| # Plot comparison | |
| plt.figure(figsize=(15, 8)) | |
| plt.semilogx( | |
| freq, | |
| orig_response, | |
| "b-", | |
| label=f"Original ({len(original_filters)} filters)", | |
| alpha=0.5, | |
| ) | |
| opt_response = calculate_response(grouped_filters, freq, fs) | |
| plt.semilogx( | |
| freq, opt_response, "r--", label=f"Optimized ({len(grouped_filters)} filters)" | |
| ) | |
| plt.grid(True) | |
| plt.legend() | |
| plt.xlabel("Frequency [Hz]") | |
| plt.ylabel("Amplitude [dB]") | |
| plt.title(f"Max Error: {np.max(np.abs(opt_response - orig_response)):.1f} dB") | |
| plt.show() | |
| return optimized_config | |
| # Call the function with your config text | |
| config_eq = """ | |
| Filter: ON LSC Fc 25 Hz Gain -6 dB Q 0.8 | |
| Filter: ON PK Fc 42 Hz Gain -1.1 dB Q 2 | |
| Filter: ON PK Fc 57 Hz Gain 1.2 dB Q 8 | |
| # ^ literally bassboost controller ^ | |
| Filter: ON PK Fc 69 Hz Gain 1.6 dB Q 7 | |
| Filter: ON PK Fc 75 Hz Gain 1.3 dB Q 2.7 | |
| Filter: ON PK Fc 84 Hz Gain -2 dB Q 3.4 | |
| Filter: ON PK Fc 115 Hz Gain -4 dB Q 10 | |
| Filter: ON LSC Fc 150 Hz Gain 1.5 dB Q 1.1 | |
| Filter: ON PK Fc 200 Hz Gain -7 dB Q 8 | |
| Filter: ON PK Fc 225 Hz Gain 0.5 dB Q 8 | |
| Filter: ON PK Fc 350 Hz Gain -2.5 dB Q 1.2 | |
| Filter: ON PK Fc 240 Hz Gain -2 dB Q 0.8 | |
| Filter: ON PK Fc 450 Hz Gain -4.5 dB Q 0.82 | |
| Filter: ON PK Fc 720 Hz Gain -2.8 dB Q 0.91 | |
| Filter: ON PK Fc 950 Hz Gain -1.2 dB Q 7 | |
| Filter: ON PK Fc 1900 Hz Gain -2 dB Q 9 | |
| Filter: ON PK Fc 1000 Hz Gain -3 dB Q 7 | |
| Filter: ON PK Fc 1500 Hz Gain 1.3 dB Q 0.8 | |
| Filter: ON PK Fc 2600 Hz Gain -1 dB Q 1.2 | |
| Filter: ON PK Fc 4300 Hz Gain -1.2 dB Q 7 | |
| Filter: ON PK Fc 4800 Hz Gain -4.2 dB Q 10 | |
| Filter: ON PK Fc 3200 Hz Gain 0.1 dB Q 2 | |
| Filter: ON PK Fc 5000 Hz Gain -2.3 dB Q 2 | |
| Filter: ON PK Fc 8200 Hz Gain -5 dB Q 1 | |
| Filter: ON HSC Fc 9600 Hz Gain -1.8 dB Q 1.6 | |
| Filter: ON PK Fc 9750 Hz Gain 1.4 dB Q 2.4 | |
| Filter: ON PK Fc 12000 Hz Gain 1.2 dB Q 1 | |
| Filter: ON PK Fc 13500 Hz Gain 0.3 dB Q 1.2498 | |
| Filter: ON PK Fc 14250 Hz Gain 0.2 dB Q 2.6 | |
| Filter: ON PK Fc 15000 Hz Gain -1.4 dB Q 1 | |
| Filter: ON HSC Fc 20000 Hz Gain -4.2 dB Q 0.8 | |
| Filter: ON LSC Fc 90 Hz Gain 2.5 dB Q 0.8 | |
| # Filter: ON HSC Fc 9500 Hz Gain -3 dB Q 0.5 | |
| Filter: ON PK Fc 136 Hz Gain -10 dB Q 12 | |
| Filter: ON PK Fc 180 Hz Gain -15 dB Q 15 | |
| Filter: ON PK Fc 1096 Hz Gain -4 dB Q 0.5 | |
| Filter: ON PK Fc 1500 Hz Gain -4.2 dB Q 2.8 | |
| Filter: ON LS Fc 120 Hz Gain -6.4 dB | |
| Filter: ON HSC Fc 11600 Hz Gain -0.6 dB Q 4""" | |
| # plot_eq_response(config_eq) | |
| # optimized_config = optimize_eq_config(config_eq) | |
| # print(optimized_config) | |
| optimized_config_v2 = optimize_eq_config_v2(config_eq, max_filters=18) | |
| print(optimized_config_v2) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment