#!/usr/bin/env python3 """ Generate pre-computed biquad coefficient tables for TAS5825M 15-band EQ. Frequencies and Q values match the mrtoy-me/esphome-tas58xx reference implementation. Coefficients are 5.27 fixed-point, big-endian byte order. Output: C header file with static const tables. """ import math import struct import sys # ─── Band definitions (matching reference) ─── BANDS = [ # (freq_hz, Q) ( 20.0, 2.0), # BQ1 ( 31.5, 2.0), # BQ2 ( 50.0, 1.5), # BQ3 ( 80.0, 1.5), # BQ4 ( 125.0, 1.0), # BQ5 ( 200.0, 1.0), # BQ6 ( 315.0, 0.9), # BQ7 ( 500.0, 0.9), # BQ8 ( 800.0, 0.8), # BQ9 ( 1250.0, 0.8), # BQ10 ( 2000.0, 0.7), # BQ11 ( 3150.0, 0.7), # BQ12 ( 5000.0, 0.6), # BQ13 ( 8000.0, 0.6), # BQ14 (16000.0, 0.5), # BQ15 ] NUM_BANDS = len(BANDS) FS = 48000.0 # Sample rate used by the reference implementation MIN_GAIN = -15 MAX_GAIN = 15 NUM_GAINS = MAX_GAIN - MIN_GAIN + 1 # 31 def calc_peaking_biquad(fc, gain_db, q, fs): """ Compute peaking-EQ biquad coefficients (Audio EQ Cookbook). TI convention: H(z) = (b0 + b1·z⁻¹ + b2·z⁻²) / (1 − a1·z⁻¹ − a2·z⁻²) so stored a1/a2 are the *negated* textbook values. Returns list of 5 int32 values in 5.27 fixed-point. """ if abs(gain_db) < 0.001: # At 0 dB gain, we still compute the proper biquad (not passthrough) # to match reference behavior. A=1 makes b0/a0=1 exactly but # the other coefficients encode the filter shape. pass w0 = 2.0 * math.pi * fc / fs A = 10.0 ** (gain_db / 40.0) sinw = math.sin(w0) cosw = math.cos(w0) alpha = sinw / (2.0 * q) b0 = 1.0 + alpha * A b1 = -2.0 * cosw b2 = 1.0 - alpha * A a0 = 1.0 + alpha / A a1_txt = -2.0 * cosw a2_txt = 1.0 - alpha / A inv_a0 = 1.0 / a0 b0 *= inv_a0 b1 *= inv_a0 b2 *= inv_a0 # TI format: negate textbook a1/a2 a1_ti = -a1_txt * inv_a0 a2_ti = -a2_txt * inv_a0 # Convert to 5.27 fixed-point scale = float(1 << 27) coeffs = [ int(round(b0 * scale)), int(round(b1 * scale)), int(round(b2 * scale)), int(round(a1_ti * scale)), int(round(a2_ti * scale)), ] return coeffs def int32_to_bytes(val): """Convert a signed 32-bit integer to 4 big-endian bytes.""" # Ensure value is in signed 32-bit range val = val & 0xFFFFFFFF return struct.pack(">I", val) def format_biquad_bytes(coeffs): """Format 5 int32 coefficients as a C byte array initializer.""" raw = b"" for c in coeffs: raw += int32_to_bytes(c) hex_bytes = ", ".join(f"0x{b:02X}" for b in raw) return hex_bytes def main(): out = sys.stdout out.write("""\ #pragma once /** * Pre-computed biquad coefficient tables for TAS5825M 15-band parametric EQ. * * Generated by scripts/gen_eq_tables.py — DO NOT EDIT BY HAND. * * Band definitions match the mrtoy-me/esphome-tas58xx reference: * Band Freq (Hz) Q * 0 20 2.0 * 1 31.5 2.0 * 2 50 1.5 * 3 80 1.5 * 4 125 1.0 * 5 200 1.0 * 6 315 0.9 * 7 500 0.9 * 8 800 0.8 * 9 1250 0.8 * 10 2000 0.7 * 11 3150 0.7 * 12 5000 0.6 * 13 8000 0.6 * 14 16000 0.5 * * Gain range: -15 dB to +15 dB in 1 dB steps (31 levels). * Sample rate: 48000 Hz. * Coefficient format: 5.27 signed fixed-point, big-endian byte order. * * Index mapping: eq_coeff_table[gain_dB + 15][band] * gain_dB = -15 → index 0 * gain_dB = 0 → index 15 * gain_dB = +15 → index 30 */ #include #define EQ_GAIN_OFFSET 15 /* add to gain_dB to get table index */ #define EQ_NUM_GAIN_STEPS 31 #define EQ_COEFF_BYTES 20 /* 5 coefficients × 4 bytes */ """) # ─── Biquad address table for TAS5825M (Book 0xAA) ─── out.write("""\ /* ─── TAS5825M EQ biquad address table (Book 0xAA) ─── * Each entry: { page, sub_address } * From mrtoy-me/esphome-tas58xx reference for TAS5825M. */ typedef struct { uint8_t page; uint8_t sub_addr; } eq_bq_addr_t; static const eq_bq_addr_t eq_left_addr[15] = { { 0x01, 0x30 }, // BQ1 Left - 20 Hz { 0x01, 0x44 }, // BQ2 Left - 31.5 Hz { 0x01, 0x58 }, // BQ3 Left - 50 Hz { 0x01, 0x6C }, // BQ4 Left - 80 Hz { 0x02, 0x08 }, // BQ5 Left - 125 Hz { 0x02, 0x1C }, // BQ6 Left - 200 Hz { 0x02, 0x30 }, // BQ7 Left - 315 Hz { 0x02, 0x44 }, // BQ8 Left - 500 Hz { 0x02, 0x58 }, // BQ9 Left - 800 Hz { 0x02, 0x6C }, // BQ10 Left - 1250 Hz { 0x03, 0x08 }, // BQ11 Left - 2000 Hz { 0x03, 0x1C }, // BQ12 Left - 3150 Hz { 0x03, 0x30 }, // BQ13 Left - 5000 Hz { 0x03, 0x44 }, // BQ14 Left - 8000 Hz { 0x03, 0x58 }, // BQ15 Left - 16000 Hz }; static const eq_bq_addr_t eq_right_addr[15] = { { 0x03, 0x6C }, // BQ1 Right - 20 Hz { 0x04, 0x08 }, // BQ2 Right - 31.5 Hz { 0x04, 0x1C }, // BQ3 Right - 50 Hz { 0x04, 0x30 }, // BQ4 Right - 80 Hz { 0x04, 0x44 }, // BQ5 Right - 125 Hz { 0x04, 0x58 }, // BQ6 Right - 200 Hz { 0x04, 0x6C }, // BQ7 Right - 315 Hz { 0x05, 0x08 }, // BQ8 Right - 500 Hz { 0x05, 0x1C }, // BQ9 Right - 800 Hz { 0x05, 0x30 }, // BQ10 Right - 1250 Hz { 0x05, 0x44 }, // BQ11 Right - 2000 Hz { 0x05, 0x58 }, // BQ12 Right - 3150 Hz { 0x05, 0x6C }, // BQ13 Right - 5000 Hz { 0x06, 0x08 }, // BQ14 Right - 8000 Hz { 0x06, 0x1C }, // BQ15 Right - 16000 Hz }; """) # ─── Pre-computed coefficient table ─── out.write("""\ /* ─── Pre-computed biquad coefficients ─── * eq_coeff_table[gain_index][band] = 20-byte biquad (b0 b1 b2 a1 a2). * gain_index = gain_dB + EQ_GAIN_OFFSET (0..30 for -15..+15 dB). */ typedef struct { uint8_t bytes[EQ_COEFF_BYTES]; } eq_biquad_t; """) out.write( f"static const eq_biquad_t eq_coeff_table" f"[EQ_NUM_GAIN_STEPS][{NUM_BANDS}] = {{\n" ) for gain_idx in range(NUM_GAINS): gain_db = gain_idx + MIN_GAIN out.write(f" /* gain = {gain_db:+d} dB (index {gain_idx}) */\n") out.write(" {\n") for band_idx, (freq, q) in enumerate(BANDS): coeffs = calc_peaking_biquad(freq, float(gain_db), q, FS) hex_str = format_biquad_bytes(coeffs) comment = f"BQ{band_idx+1:2d} {freq:7.1f} Hz Q={q}" out.write(f" {{ {{ {hex_str} }} }}, // {comment}\n") out.write(" },\n") out.write("};\n") out.write("\n/* End of generated data */\n") if __name__ == "__main__": main()