Anatomy of an LLM-Generated Biquad EQ

I’ve been building a live-coding glicol VST in Rust for shits and giggles and wanted a “native” EQ in the GUI because it’s convenient vs going back to the DAW and adding a seperate EQ effect, and I couldn’t just farm it out to the Glicol engine because that’s not what it’s for.

I found myself staring at a filter implementation that I first wrote by hand, and then had an LLM rewrite because mine clearly sucked and I hated it. I’m looking over the resulting LLM generated code, fairly impressed tbh, and noted Claude hadn’t used a “WebSearch” tool at all to create it, it just blasted it out “one shot”. Which had me wondering where it came from. Not in the existential sense — I know I asked an LLM to “make a 3-band parametric EQ that works withy my Mod abstraction, use best practices the way an experienced audio engineer would” — but in the archaeological sense. These coefficients. This structure. This specific hardcoded 0.9 buried in an alpha calculation. Where did this thing learn to do this? And is it any good?

Queue the rabbit hole that’s been the last 24 hours trying to figure out how LLMs synthesize “canonical” algorithms — and what that tells us about both the limits and the weirdly impressive capabilities of statistical pattern matching.

Turns out, the answer to “where did the LLM learn this” is:

Everywhere. And nowhere. It’s complicated.

The Starting Point: Naive Me

Let’s be honest about where I started. I knew I wanted a 3-band EQ — low shelf, mid peak, high shelf. I knew these were “biquad filters” from reading about audio DSP over the years. And I knew enough to be dangerous, which meant my first attempt looked like this:

pub fn process_sample(
    input: f32,
    freq: f32,
    gain_db: f32,
    sample_rate: f32,
    x1: &mut f32, x2: &mut f32,
    y1: &mut f32, y2: &mut f32,
) -> f32 {
    // Computing trig functions PER SAMPLE (44100 times per second!)
    let a = 10.0_f32.powf(gain_db / 40.0);
    let w0 = 2.0 * PI * freq / sample_rate;
    let cos_w0 = w0.cos();
    let sin_w0 = w0.sin();

    // Giant wall of coefficient math adapted from code I found on the internet...
    let alpha = sin_w0 / 2.0 * ((a + 1.0/a) * (1.0/0.9 - 1.0) + 2.0).sqrt();
    // ... dozens more lines ...

    let output = b0 * input + b1 * *x1 + b2 * *x2 - a1 * *y1 - a2 * *y2;
    output
}

It worked! In the sense that audio came out and it sounded vaguely filtered. But I knew it was bad. I just didn’t know how bad, or what “good” would even look like but I assumed examples on the internet I looked at probably weren’t it. So I asked Claude to improve it. And what came back was… different. Structured. Professional-looking. A separation between coefficients and state. A dirty flag for lazy recomputation. Proper stereo handling. All 300 lines of it.

But here’s the thing that nagged at me: I recognized patterns in that code. Not because I wrote them, but because they felt familiar in the way that all biquad implementations feel familiar if you’ve looked at a few. Which made me wonder — did the LLM actually understand what it was doing, or did it just really convincingly remix the collected wisdom of the internet?

Robert Bristow-Johnson Posted a Thing to Usenet in 1998

To understand what the LLM produced, you have to understand the source material. And for biquad filters, there is essentially one source: Robert Bristow-Johnson’s Audio EQ Cookbook.

Nobody remembers the original usenet post, but everybody uses those formulas. They’re in the W3C Web Audio spec now. They’re in every Rust audio crate I looked at. For example: (biquad-rs, fundsp). They’re in Python scipy discussions. Every tutorial on EarLevel Engineering. Every forum thread on KVR. The same damned formulas, copy-pasted across every programming language humans have invented. And when you ask an LLM for a biquad filter, you get… those formulas. Because that’s what “biquad filter” means at this point. The training data isn’t biased toward RBJ — it is RBJ, wearing different syntactic costumes.

Compare the cookbook’s lowShelf formula:

b0 =    A*[ (A+1) - (A-1)*cos(w0) + 2*sqrt(A)*alpha ]
b1 =  2*A*[ (A-1) - (A+1)*cos(w0)                   ]
a0 =        (A+1) + (A-1)*cos(w0) + 2*sqrt(A)*alpha

To what the LLM produced in my eq.rs:

let a_plus_1 = a + 1.0;
let a_minus_1 = a - 1.0;
let two_sqrt_a_alpha = 2.0 * a.sqrt() * alpha;

BiquadCoeffs {
    b0: (a * (a_plus_1 - a_minus_1 * cos_w0 + two_sqrt_a_alpha)) / a0,
    // ...
}

It’s the same formula. Factored slightly differently, with more verbose variable names, but structurally identical. The LLM didn’t invent anything here. It transcribed.

Convergent Citation Creates Canonical Knowledge

Here’s where it gets interesting from an LLM-learning perspective. The RBJ cookbook doesn’t appear once in the training data — it appears everywhere, in every language, with every syntax. This creates what I’m calling convergent citation: a pattern so universally adopted that it becomes statistically inevitable in any generated output.

Think about what the LLM has seen:

  • The W3C Web Audio specification (which cites RBJ verbatim)
  • The biquad-rs Rust crate (which implements RBJ formulas)
  • Python scipy discussions (RBJ formulas in numpy)
  • CCRMA academic materials (RBJ with derivations)
  • Hundreds of Stack Overflow answers (RBJ, badly formatted)
  • Thousands of personal blog posts (RBJ, with varying degrees of understanding)

The formulas aren’t just common — they’re canonical. And the LLM, being a statistical creature, produces what is statistically dominant. This isn’t a bug. For well-established algorithms, this is exactly what you want. The cookbook is battle-tested across millions of implementations. Transcribing it faithfully is the correct choice. Innovation would introduce bugs.

The Fingerprints of Multi-Source Synthesis

But the LLM didn’t just transcribe the cookbook. It synthesized from multiple sources, and you can see the fingerprints if you look closely.

From RBJ Cookbook: The exact coefficient formulas. The A = 10^(dBgain/40) calculation (40, not 20, because we’re dealing with amplitude, not power). The hardcoded S = 0.9 shelf slope buried in that alpha calculation as 1.0 / 0.9 - 1.0.

From Rust audio crates: The separation of BiquadCoeffs (static coefficients) from BiquadState (per-sample state). The coeffs_dirty flag for lazy recomputation. The #[derive(Clone, Copy)] idiom.

From tutorials: The Direct Form 1 implementation with explicit x1, x2, y1, y2 state variables. The verbose naming (a_plus_1 rather than ap1).

From Rust best practices: The impl Default pattern. The StereoSample abstraction. The separation of concerns that makes the code testable.

What a direct C-to-Rust port might look like (and, frankly, where my brain went for my own first draft):

typedef struct { float b[3], a[3]; } Coeffs;
typedef struct { float x[2], y[2]; } State;

What the LLM produced:

struct BiquadCoeffs { b0: f32, b1: f32, b2: f32, a1: f32, a2: f32 }
struct BiquadState { x1_l: f32, x2_l: f32, y1_l: f32, y2_l: f32, ... }

The flattened struct with named fields (not arrays) is idiomatic Rust, I think, not a direct port from any single source. The LLM synthesized a “greatest hits” compilation: formulas from the cookbook, architecture from professional crates, idioms from Rust best practices, naming from tutorial culture. I’m already taking this technology for granted, but when I just look at one specific examples like this it’s hard for me to not be impressed. I certainly would struggle to do as well, as my shit implementation clearly demonstrates.

When Naive is Fine (and the LLM Doesn’t Know the Difference)

Here’s the thing about naive implementations: sometimes they’re fine. How much does it really matter if you’re computing sin() and cos() 44,100 times per second instead of caching the coefficients? For a guitar pedal where you maybe tweak the EQ once a song? Probably doesn’t matter at all.

But the LLM doesn’t know that. It’s seen enough “optimized” implementations that it knows the pattern: separate your coefficients from your state, add a dirty flag, recompute only when parameters change. It’s not thinking about your use case. It’s reproducing what people generally do. And that’s usually fine! But it’s worth knowing the difference between “best practice” and “necessary for my context.”

The current implementation sits at what I’d call Level 1 on an optimization spectrum:

Level 0 (Naive): Recompute everything per sample. 44,100 trig calls per second. Probably fine for a hobby project. raises hand

Level 1 (My eq.rs): Cached coefficients with dirty flag. Clean, readable, maintainable. Good for most plugin scenarios.

Level 2 (DF2T): Direct Form 2 Transposed. Uses 2 state variables instead of 4. Better for static filters but introduces artifacts on parameter changes. Tutorials teach DF1T but mention DF2T.

Level 3 (SIMD): Process 4-8 channels simultaneously with AVX. Overkill for a mono-in/stereo-out guitar pedal. SIMD logic notoriously hard for humans to write bug free.

Level 4+ (Advanced): IIRNet neural filter design, nonlinear biquads with waveshaping, fixed-point with noise shaping for embedded / no FP hardware.

Level 5 (Custom): Tailored to specific use cases, such as guitar pedals that mimic classic analog circuits. Requires deep understanding of the hardware and software environment.

Someone who’s an expert could certainly list 10 more levels, I’d wager. I came up with these after a 4hr research extravaganza last night. As someone without much direct knowledge in this particular domain, but 30 years of generalalized experience in “why is this running so slow?” was enough for me to intuit that SIMD approaches would of course by applicable. I’ve used paid plugins by companies that emulate circuit designs so I know a custom algorithm that mimics old (incredibly limited!) hardware, analog or digital, is a thing. DFT2 is talked about in tutorials that implement DFT1 (but is left as an exercise for the reader), and there’s bespoke, clever implementations often designed to run on low end hardware done by smart folks who get a thrill out of it. Like Airwindow’s SmoothEQ3. Which, as an aside, is beautiful both in its cleverness and simplicity, but also in how “natural” it sounds. Where natural simply means it’s what my ears are used to hearing and therefore expect. That it both sounds better, and is significantly faster, is really something.

So there’s a bunch of possible approaches the LLM could have suggested. But when I asked it to build me a better, optimized version it went for a very specific approach (Level 1) and when I questioned it about futher optimizations it literally said:

"this is a textbook example of a biquad filter implementation, it is well optimized for performance and accuracy."

No fixed point optimizations for you! So.. the LLM wants to do it this way, which is… correct? It’d be hard for me to argue that it’s not the “smart default” that works for most cases. But it didn’t explain why, or help me understand when I might want to go higher or lower on that spectrum.

Where This Breaks Down

Convergent citation is great when there’s a canonical answer. For biquad EQs, there is. But what about things that aren’t statistically dominant in the training data?

SIMD optimizations? Sure, there’s some example code out there. But it’s specialized, inconsistent, poorly documented, and varies wildly between platforms. The LLM is much more likely to produce something subtly wrong because there are fewer examples and they’re more diverse.

Same for fixed-point implementations with noise shaping (embedded DSP without FPU). Same for cache-aware optimizations. Same for anything that requires understanding your specific hardware or use case. My project is very clearly aimed at guitar effects in particular, but the LLM was essentially unwilling to move past “textbook RBJ”.

The cookbook wins because everybody uses it. The edge cases lose because almost nobody documents them.

Can we all just appreciate for a moment the impact of that 1998 RBJ usenet post that by now has been battle-tested across more implementations than any of us can count.

It’s discussed to death that an LLM is fundamentally limited by what’s already been written. Obvious, I know, but worth restating and this little exploration certainly reinforced this fact for me. It’s excellent at convergent synthesis — combining canonical patterns from multiple sources into idiomatic target code. It’s going to be much weaker at innovation, non-obvious trade-offs, and anything that requires understanding your specific context.

I got a solid, professional-looking implementation that I can now understand and maintain. The LLM gave me a better starting point than my naive attempt, and the forensic exercise of understanding where it came from taught me more than I expected. So I’m pretty happy.

Whether that’s enough depends on what you’re building. For most of us, most of the time, I suspect it is.

More to come. I want to see if I can work with it to bang out more “interesting” EQ designs. I suspect it will hinder more than help, but I’m constantly surprised by how much it can do. Regardless, I want to try baning out my own SmoothEQ implementation because that airwindows code is so cool it makes me want to get better at these DSP algos.


Sources:

← Back to posts

© 2026 Mark Mayo · RSS