Skip to content

AutoEQ Benchmarks

modernGraphTool’s AutoEQ runs turboEQ, a WebAssembly port of AutoEq’s optimizer. This page compares it, as an algorithm, with the optimizers it replaces and with the one it was ported from. Every engine fits the same 176 real IEM measurements under the same constraints.

  • It is two orders of magnitude faster. With eight bands, turboEQ’s median run is 6.2 ms. CrinGraph’s optimizer takes 807 ms and modernGraphTool’s old TypeScript one 1.41 s.
  • It returns the number of bands you ask for, every time. Asked for eight, CrinGraph returned eight for 87% of the measurements and the old optimizer for 22%.
  • Exact match fits closest from eight bands up. At ten bands, its median error across 20 Hz–20 kHz is 0.48 dB, against 0.74 for the old optimizer and 1.24 for CrinGraph. At eight it leads by less (0.77 against 0.82), and its bad cases are less bad (1.59 against 1.90 at the 90th percentile). On headphones it leads at every band count.
  • With five bands on IEMs, the old optimizer scores better, mostly on level. It reaches 1.16 dB against exact match’s 1.27. Where turboEQ loses explains why.
  • Treble-safe stays close to AutoEq. Given the same constraints, its EQ lands 0.17 dB (median) from AutoEq’s when both run to convergence. For comparison, AutoEq’s own early stopping moves AutoEq’s answer by 0.07 dB.

Every engine got the same limits on each band:

LimitValue
Q0.1–10
Gain−20 to +20 dB
Where a band may sit20 Hz–20 kHz
Largest total boost20 dB

Some differences between the engines are part of the method and stay:

  • CrinGraph has no shelf filters, so all of its bands are peaking. The others use a low and a high shelf once there are four bands or more, and place them themselves. turboEQ and AutoEq keep shelf Q between 0.4 and 0.7.
  • Treble-safe places no band above 10 kHz, because above that it scores only the overall level, not the shape.
  • The limit on total boost only means something to the AutoEq family, which caps the whole EQ. CrinGraph and the TypeScript optimizer limit each band on its own.

Band counts below are totals, shelves included. Ten bands is the layout of AutoEq’s 8_PEAKING_WITH_SHELVES preset.

Median time for one fit, in milliseconds. The last column is the 90th percentile at ten bands.

Engine5 bands8 bands10 bands10 bands, p90
CrinGraph4828079931,080
mGT TypeScript5981,4102,1002,440
turboEQ · Exact match1.86.21224
turboEQ · Treble-safe2.18.21939
AutoEq (Python)3074124180

At eight bands, turboEQ is about 130× faster than CrinGraph and 230× faster than the old optimizer. On the same work as AutoEq itself, the curve processing plus the fit, treble-safe is about 9× faster than the Python original.

CrinGraph runs on the page’s main thread, so the page freezes for as long as the fit takes. modernGraphTool runs AutoEQ in a web worker, and the TypeScript fallback runs there too.

How close the equalized curve lands to the target, as RMS error in dB after removing the overall level offset. Lower is better. Without any EQ, the median error is 3.03 dB across 20 Hz–20 kHz and 2.23 dB below 10 kHz.

Across 20 Hz–20 kHz, median with the 90th percentile in parentheses:

Engine5 bands8 bands10 bands
CrinGraph2.01 (3.04)1.47 (2.84)1.24 (2.60)
mGT TypeScript1.16 (2.28)0.82 (1.90)0.74 (1.85)
turboEQ · Exact match1.27 (2.26)0.77 (1.59)0.48 (1.13)
turboEQ · Treble-safe1.72 (2.54)1.63 (2.43)1.63 (2.38)
AutoEq (Python)1.74 (2.52)1.69 (2.47)1.70 (2.45)

Below 10 kHz, median. This leaves out the region AutoEq deliberately does not shape:

Engine5 bands8 bands10 bands
CrinGraph0.810.370.27
mGT TypeScript0.670.490.44
turboEQ · Exact match0.840.450.27
turboEQ · Treble-safe0.880.790.78
AutoEq (Python)0.920.810.80

How often the requested band count came back:

Engine5 bands8 bands10 bands
CrinGraph94%87%64%
mGT TypeScript60%22%6%
turboEQ (both modes) and AutoEq100%100%100%

CrinGraph’s search stops once it runs out of candidate peaks and dips. The TypeScript optimizer also drops bands it judges ineffective, so its numbers above often come from fewer bands than asked.

Treble-safe scores worse on purpose. It smooths the treble, scores only the level above 10 kHz and places no band there. That gives up matching the graph in exchange for a correction that holds up when the treble measurement can’t be trusted.

Exact match, like the TypeScript optimizer, will use narrow, deep bands when they fit. Whether those are an improvement you can hear is a separate question. The error here is measured against the same measurement the EQ was fitted to, and narrow features in the treble move with fit and seating, so a band aimed exactly at one may miss it on your head.

With five bands on IEMs, the old TypeScript optimizer scores 1.16 dB against exact match’s 1.27. Almost all of that is level. The scores on this page remove the overall level offset, the most generous way to line two curves up, and the TypeScript optimizer’s answers lean on it. Line the curves up the way AutoEq does instead, by the mean error between 100 Hz and 10 kHz before any EQ, and it scores 1.74 while exact match stays at 1.27. turboEQ fits at a fixed level, so its score does not move.

What is left is the optimizer itself. With so few bands, turboEQ sometimes settles in a local minimum from the layout AutoEq’s init() starts it in. Its own objective rates the TypeScript answer better on 48 of the 176 curves at five bands, 16 at eight and one at ten.

How different each engine’s EQ is from what upstream AutoEq by jaakkopasanen produces for the same measurement, under the same constraints: the RMS difference between the two EQ curves over 20 Hz–20 kHz, level offset removed. Median, with the 90th percentile in parentheses.

Engine8 bands10 bands
CrinGraph1.32 (2.13)1.33 (2.13)
mGT TypeScript1.39 (1.97)1.42 (1.96)
turboEQ · Exact match1.39 (2.03)1.57 (2.10)
turboEQ · Treble-safe0.22 (0.52)0.24 (0.53)
AutoEq, run to convergence0.07 (0.24)0.08 (0.21)

The last row compares AutoEq with itself, with and without the preset’s early stopping (min_std). turboEQ always runs to convergence. Measured against converged AutoEq instead, treble-safe is 0.17 dB away at eight bands and 0.19 at ten. With the shelves pinned where AutoEq’s presets put them, the same comparison gives 0.10: letting the shelves move gives two optimizers more places to settle apart.

Exact match is as far from AutoEq as the other engines are, and that is intended. It fits a different target: the curve as the graph shows it.

The headphone set is small: nine curves from two headphones, most of them one headphone in different EQ modes. Treat it as a consistency check rather than a second result. At eight bands:

EngineMedian timeBand count20 Hz–20 kHzBelow 10 kHz
CrinGraph947 ms100%3.220.87
mGT TypeScript1.48 s22%1.380.96
turboEQ · Exact match5.8 ms100%1.260.82
turboEQ · Treble-safe6.8 ms100%1.670.90
AutoEq (Python)99 ms100%1.881.03

Exact match leads at five and ten bands too, across the full band (1.60 against 1.81, and 1.14 against 1.31). The treble of these headphones is a row of narrow peaks and notches, which exact match can now follow: the penalty on steep bands and the extra smoothing AutoEq applies to its target no longer apply in exact mode.

CrinGraphmGT TypeScript (fallback)turboEQ in modernGraphToolAutoEq (Python)
OptimizerGreedy candidate search, then grid refinementCrinGraph lineage, plus shelves and pruningAll parameters at once, with analytic gradientsAll parameters at once, SciPy SLSQP
Filter typesPeakingPeaking, low and high shelfPeaking, low and high shelf placed by the fitPeaking, shelves (its presets pin them)
Returns the band countOften fewerOften fewerAlwaysAlways
RangesClamped after the fitLimit the searchBounds inside the optimizerBounds inside the optimizer
Sharp filtersAllowedAllowedExact match: allowed. Treble-safe: penalizedPenalized above ~18 dB/octave
Level alignmentWhatever the graph’s normalization givesPinned at 1 kHzMinimum mean error, 100 Hz–10 kHzMinimum mean error, 100 Hz–10 kHz
TrebleFirst pass skips above 7 kHzFitted like everything elseExact match: full shape. Treble-safe: AutoEq’s handlingSmoothed, level only above 10 kHz
Boost limitPer bandPer bandWhole EQ, set by the gain range’s maximumWhole EQ, 6 dB by default
Graphic EQ presetsNoNoYes, gain only on the preset’s bandsYes, fixed-band EQ
RunsPage’s main threadWeb workerWeb worker, WebAssembly (about 37 KB gzipped)Python
  • Data. The measurements of silicagel.squig.link database. One measurement per device, the first file of each phone_book.json entry, left and right averaged: 176 IEM curves. All nine headphone curves were used, because there were so few. Targets were Harman IE 2019v2 for IEMs and Harman 2018 for headphones.
  • Inputs. Each engine got its curves the way its own app prepares them. CrinGraph: its 1/48-octave grid and 60-phon loudness normalization. modernGraphTool: 1/48-octave smoothing and 500 Hz normalization. AutoEq: the raw average, as its command line takes it.
  • Scoring. Every engine’s filters went through the same biquads at 48 kHz, were added to the raw measurement, and were compared with the raw target on a 1/48-octave grid.
  • Timing. The engine call alone, one fit after another, after a warm-up fit. The JavaScript engines ran under Node 24.18 on V8, which is the engine Chrome runs. In the app, a run also pays a message round trip to the worker. AutoEq ran under Python 3.14.7, NumPy 2.5.3 and SciPy 1.18.1.
  • Versions. turboEQ 0.2.0, CrinGraph from squiglink/lab main at 9ff842c, AutoEq v4.1.2.
  • Machine. Mac mini, Apple M4, 16 GB, macOS 27.2. Measured 2026-09-25.

Timings don’t carry over to other machines. The ratios between rows should.

The harness lives in scripts/bench-autoeq/. It needs a checkout of squiglink/lab, a checkout of AutoEq, and a measurement folder with a phone_book.json. The constraints default to the ones above and can be changed with --q, --gain and --fc:

Terminal window
node scripts/bench-autoeq/run.mjs --data DATA_DIR --target TARGET.txt \
--lab LAB_REPO --out iem.jsonl
python scripts/bench-autoeq/upstream.py --autoeq AUTOEQ_REPO \
--curves iem.curves.json --out iem-upstream.jsonl
node scripts/bench-autoeq/score.mjs --curves iem.curves.json iem.jsonl iem-upstream.jsonl

upstream.py reads the constraints from iem.curves.json, so AutoEq runs under the same ones. It needs NumPy, SciPy, Matplotlib, tabulate and PyYAML, since AutoEq imports all of them.