Skip to content

v0.7.0 - Harmonic character is now genuinely frequency-selective

Choose a tag to compare

@stoatworks-labs stoatworks-labs released this 24 Jul 23:33

The fix

The Harmonic band character was running its waveshaper across the entire broadband signal passing through the band. A Harmonic bell at 5 kHz saturated your bass, your mids, everything — it was a full-range saturator sitting behind an EQ filter, not a harmonic EQ.

A harmonic EQ's defining behaviour is frequency-selectivity: isolate the region the band targets, generate harmonics from only that, mix them back. That's now what it does.

  • The band's region is split off through a region-isolation filter (band-pass at freq/Q for Bell/Tilt, high-pass at the corner for High Shelf, low-pass for Low Shelf — the same shape the dynamic detector uses).
  • Only that isolated region is shaped.
  • Only the generated harmonics — shaped signal minus isolated region — are summed back in parallel with the dry path. Because the difference is what's summed, the band's own level is unchanged and drive=0 is exactly transparent: it adds harmonics without adding gain.

Why this wasn't caught in v0.3.0

The original verification FFT'd a single tone at the band's own frequency. That test is structurally incapable of distinguishing "saturate only this region" from "saturate everything" — with one tone present, both produce identical output. It confirmed the waveshaper maths (which was, and still is, correct) and nothing whatsoever about the routing.

The new test is two-tone, which separates them immediately.

Verified

Two-tone harness: ~100 Hz well outside the band + ~5 kHz inside it; Harmonic bell at 5 kHz, Q=1, +12 dB.

Measurement Result
In-band 2nd harmonic vs out-of-band 2nd harmonic 91 dB selectivity
Out-of-band fundamental level change within 0.01 dB of input
Pure-odd blend, 2nd harmonic 121 dB down (tanh is an odd function)
Harmonic band at 0 dB gain transparent to -180 dB
Dynamic EQ after shared-filter refactor still ducks correctly

The old implementation could not have produced the first two numbers.

pluginval strictness 5: VST3 clean. AU passes with two benign, pre-existing warnings unrelated to this DSP-only change (sidechain bus-disable quirk, and the AU wrapper reporting program -1) — both now documented accurately in the README, which previously undercounted them.

Also in this release

The "which filter isolates this band's region" logic existed only inside DynamicEQDetector. It's now a single shared EQBand::designRegionIsolationFilter used by both the harmonic stage and the dynamic detector, so the two can't drift apart. Isolation coefficients are cached and redesigned only when the region actually moves, since coefficient construction allocates and this runs on the audio thread.

Known limitations

  • Not yet tested against real (non-silent) audio hardware in a live signal chain
  • No linear-phase mode — intentionally out of scope (zero latency is the point)

Full Changelog: v0.6.0...v0.7.0