autoeq 0.4.40

Automatic equalization for speakers, headphones and rooms!
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
<!-- markdownlint-disable-file MD013 MD033 -->

# RoomEQ — How it Works

`roomeq` is the multi-channel room-equalization engine in the `autoeq` crate.
Given one or more measurement curves of a loudspeaker system in a real room,
it produces a per-channel DSP chain (gain, delays, IIR biquads, optional FIR
convolution kernels, and crossovers) that compensates the system toward a
psychoacoustically motivated target.

This article walks through every stage of the pipeline, the data structures
flowing between them, and the algorithms that drive each stage. It is meant
both as a reader's guide and as an implementation map of the code under
`crates/autoeq/src/roomeq/`.

---

## 1. Big picture

```mermaid
flowchart TB
    A[RoomConfig JSON] --> B[Measurements<br/>CSV / WAV / API]
    A --> C[Optimizer settings<br/>target, algo, bounds]
    B --> P((RoomPipeline))
    C --> P
    P --> D[Per-channel DSP chains<br/>gain · delay · biquads · FIR]
    D --> E[DspChainOutput JSON]
    E --> F1[AudioEngine plugin runtime]
    E --> F2[CamillaDSP / APO / EasyEffects /<br/>Wavelet / PipeWire / Roon export]
```

A run is fully described by a single JSON configuration:

* **`speakers`** — a map from a logical name to a measurement source
  (CSV path, WAV recording, in-memory curve, or a `Group` /
  `MultiSubGroup` / `DBA` / `Cardioid` aggregate).
* **`system`** (optional) — explicit topology: `Stereo`, `Stereo 2.1`,
  `HomeCinema`, `Custom`. When present, it selects a specialised workflow
  rather than the generic per-channel path.
* **`crossovers`** — named filter blocks used by multi-driver speakers
  and by bass management (`LR24`, `LR48`, `BW12`, `BW24`).
* **`optimizer`** — target curve, optimizer algorithm, filter count,
  Q / gain / freq bounds, and feature toggles
  (Schroeder split, excursion protection, phase alignment, multi-seat,
  CEA-2034 pre-correction, mixed-phase, …).

The output is a `DspChainOutput`: per-channel `plugins` arrays plus
`global_plugins` (a sparse routing matrix when bass management is active)
and a `metadata` block holding pre/post scores, EPA per channel,
the bass-management report, and version metadata.

---

## 2. The high-level pipeline

`optimize_room()` is the public entry point in
`crates/autoeq/src/roomeq/optimize.rs`. It dispatches into
`RoomPipeline::run()` with an observer that translates internal
`PipelineEvent`s into user-visible `RoomOptimizationProgress` updates.
The pipeline emits the following stages in order:

```mermaid
flowchart TB
    s0([ConfigPreparation]) --> s1([Validation])
    s1 --> s2([TopologyRouteSelection])
    s2 -->|Stereo 2.0/2.1, HC| s3a([TopologyWorkflowExecution])
    s2 -->|Custom / has Group| s3b([GenericChannelOptimization])
    s3a --> s4
    s3b --> s4([FirGeneration<br/>if PhaseLinear/Hybrid])
    s4 --> s5([MixedPhaseFirGeneration<br/>if MixedPhase])
    s5 --> s6([PhaseCorrection<br/>if configured])
    s6 --> s7([ImpulseResponseComputation])
    s7 --> s8([ChannelMatching<br/>≥2 channels])
    s8 --> s9([MetadataRefresh])
    s9 --> s10([SanityCheck])
    s10 --> OUT[(RoomOptimizationResult)]
```

`PipelineStepId` is the canonical list of step names. Observers receive
`Started`/`Completed`/`Failed` events for each step and can `Stop` the
run from any callback (used by the GPUI/TUI front-ends to implement
cancellation).

### 2.1 ConfigPreparation

The incoming `RoomConfig` is cloned. If
`optimizer.cea2034_correction.enabled` is set, the spinorama API is
queried (or the local cache hit) for every speaker, and the resulting
CEA-2034 measurement bundle is stored on `config.cea2034_cache` so the
per-channel workflow can apply a 3-pass speaker pre-correction
(on-axis · listening-window · sound-power) before room correction.

### 2.2 Validation

`validate_room_config(config)` runs a battery of structural checks:

* every `system.speakers` role points to an existing key in `speakers`;
* every named crossover referenced from a speaker is defined;
* every `MeasurementSource::Path` exists on disk and parses as a valid
  curve (frequency grid monotonic, no NaNs);
* phase-data presence is enforced for features that need it
  (multi-seat, phase alignment, mixed-phase);
* min/max frequency bounds lie within the measurement support of every
  channel (this is the common foot-gun: configuring a 20 Hz floor when
  the measurement starts at 50 Hz produces meaningless filters).

Failures abort the run before any optimization happens.

### 2.3 Topology route selection

The dispatcher inspects `config.system`:

```mermaid
flowchart TB
    T{system?} -->|None| Generic
    T -->|Custom| Generic
    T -->|Stereo, no subs| S20[optimize_stereo_2_0]
    T -->|Stereo, has subs| S21[optimize_stereo_2_1]
    T -->|HomeCinema| HC[optimize_home_cinema]
    T -->|any uses Group| Generic
    Generic[GenericChannelOptimization]
```

Specialised workflows run group-aware logic (bass management, LFE
routing, multi-sub crossovers); the generic path treats every speaker
in isolation through `process_single_speaker()`.

The dispatcher logs which workflow it selected so users can see at a
glance whether 2.1 / Home-Cinema features were engaged.

---

## 3. Per-channel optimization (`process_single_speaker`)

This is the heart of the system. Whether invoked from a topology
workflow or from the generic path, every channel goes through the same
sequence:

```mermaid
sequenceDiagram
    autonumber
    participant Cfg as RoomConfig
    participant LD as load_source
    participant TG as build_complete_target_curve
    participant EX as detect_f3 / generate_excursion_protection
    participant MS as optimize_multiseat (if subs · seats)
    participant PA as optimize_phase_alignment
    participant EQ as optimize_eq_with_optional_schroeder
    participant FIR as fir::generate_*
    participant OUT as build_channel_dsp_chain

    Cfg->>LD: MeasurementSource (CSV / WAV / API)
    LD-->>EQ: Curve { freq, spl, phase? }
    Cfg->>TG: TargetResponseConfig
    TG-->>EQ: target_db(f)
    Cfg->>EX: excursion_protection
    EX-->>OUT: HPF biquads (prepended)
    Cfg->>MS: multi-seat measurements
    MS-->>OUT: per-sub gain · delay · polarity · all-pass
    Cfg->>PA: phase_alignment band
    PA-->>OUT: delay · polarity for sub vs main
    Cfg->>EQ: optimizer config
    EQ-->>FIR: Vec<Biquad> + final curve
    FIR-->>OUT: optional FIR coeffs
    OUT-->>Cfg: ChannelDspChain
```

Each box maps directly to a submodule:

| Stage | Module | Key symbol |
|---|---|---|
| Load measurements | `read/` | `load_source(MeasurementSource)` |
| Build target | `roomeq/target_tilt.rs` | `build_complete_target_curve` |
| F3 + HPF | `roomeq/excursion.rs` | `detect_f3`, `generate_excursion_protection` |
| Multi-seat | `roomeq/multiseat.rs` | `optimize_multiseat` |
| Phase alignment | `roomeq/phase_alignment.rs` | `optimize_phase_alignment` |
| EQ search | `roomeq/speaker_eq/schroeder.rs` | `optimize_eq_with_optional_schroeder` |
| FIR | `roomeq/fir.rs` | `post_generate_fir` / `post_generate_mixed_phase_fir` |
| Build chain | `roomeq/output.rs` | `build_channel_dsp_chain*` |

### 3.1 Loading measurements

`load_source` accepts five kinds of `MeasurementSource`:

* `Path` to a 2-column CSV (`freq, spl`, optional `phase`);
* `WavFile` — runs an FFT-based deconvolution against the matched
  reference signal (MLS or Dirac, configured by `recording_config`)
  to recover the impulse response and convert it into a frequency
  response on a logarithmic grid;
* `InMemory` — a pre-loaded `Curve` (used by the GPUI/TUI when
  measurements are taken interactively);
* `Spinorama` — fetched from `api.spinorama.org`;
* `Aggregate` — average / complex-sum of multiple sub-sources
  (multi-mic, multi-position, …).

After loading, the curve is resampled onto the optimizer's frequency
grid (`config.optimizer.min_freq` … `max_freq`), 1/n-octave smoothed
where requested, and clamped to the measurement support so optimizer
weights never reach into noise.

### 3.2 Building the target curve

```mermaid
flowchart TB
    S[shape: flat / harman / custom / file / from_measurement] --> BASE[base curve]
    SLOPE[slope_db_per_octave<br/>reference_freq] --> BASE
    BASE --> ADD[+ bass shelf<br/>+ treble shelf]
    PREF[preference.bass_shelf_db / freq<br/>preference.treble_shelf_db / freq] --> ADD
    ADD --> T[target_db<br/>f_i for all i]
```

The base shape is one of:

* **`flat`**`target_db(f) = 0`;
* **`harman`** — Olive / Toole's recommended -0.8 dB/oct in-room slope
  through a configurable reference frequency (default 1 kHz);
* **`custom`** — user-set slope and reference;
* **`file`** — interpolated from a CSV;
* **`from_measurement`** — an optionally smoothed copy of the
  measured curve, useful for "preserve the speaker, fix the room".

User-preference shelves are layered on top with smooth 2nd-order
transitions. When `broadband_precorrection` is enabled, the optimizer
runs a preliminary low-Q broadband fit (broadband shelf + gain) before
the fine-grained PEQ pass, which tames pre-shelf level errors that
otherwise mislead the local optimiser.

### 3.3 Excursion protection (Scenario B)

Bookshelf and small-cabinet drivers cannot produce flat bass below
their cabinet/driver -3 dB point (`F3`). EQ that boosts below `F3`
drives excursion past `Xmax` and produces audible harmonic distortion
or, in the limit, mechanical damage.

```mermaid
flowchart LR
    M[Smoothed measurement<br/>1/3 oct] --> RL[Reference level<br/>100..200 Hz]
    RL --> SD[Search downward<br/>for −3 dB point]
    SD --> F3((F3))
    F3 --> HPF[HPF at F3 · 2^(−margin_octaves)<br/>LR4 / BW2]
```

The detected `F3` is used to place a **highpass filter** that becomes
the first plugin in the channel's chain, before the EQ section.
This bound flows back into the optimiser's frequency range so it
never tries to fight the rolloff with a boost.

### 3.4 Phase alignment (Scenario A — sub + mains)

For systems with subwoofers, time and polarity mismatch around the
crossover region creates a deep cancellation dip that no amount of
gain EQ can fix. Phase alignment optimises **delay** and **polarity**
of the sub relative to the main channel by maximising the energy of
the complex sum in the crossover band:

$$ J(\tau, p) = \int_{f_\text{lo}}^{f_\text{hi}} \left| H_\text{sub}(f) + p \cdot H_\text{main}(f) \cdot e^{-j 2\pi f \tau} \right|^2 \, df $$

where `p ∈ {+1, -1}`. The search is a coarse 0.5 ms grid over
`[-max_delay, +max_delay]`, refined to 0.1 ms around the best point.
Phase-aware: requires phase data on both measurements (REW exports
phase, the WAV-based loader recovers it from the IR).

### 3.5 Multi-seat (MSO) optimization

In rooms with multiple listening positions the modal field varies
significantly. Optimising for a single seat usually degrades others.
RoomEQ ports the MSO logic into per-sub gain, delay, polarity, and
optional all-pass filters, optimising the cross-seat objective:

```mermaid
flowchart TB
    M[Per-sub × per-seat<br/>complex transfer functions H_ij] --> P[Parameters x:<br/>gains, delays, polarities,<br/>allpass freq · Q]
    P --> S[Combined response per seat<br/>S_j = Σ_i x_i · H_ij]
    S --> OBJ[Objective:<br/>variance / primary+constraints / average]
    OBJ -->|gradient-free DE| P
```

Three strategies are exposed:

* **`minimize_variance`** — minimise the standard deviation of SPL
  across seats in the optimisation band.
* **`primary_with_constraints`** — optimise the primary seat while
  constraining secondary seats within `max_deviation_db`.
* **`average`** — flatten the cross-seat magnitude average.

Two extra penalties prevent pathological solutions:

* `MSO_MAX_MEAN_OUTPUT_LOSS_DB = 1.5` — gives up bass headroom only
  up to 1.5 dB on average;
* `MSO_NULL_DEFICIT_ALLOWANCE_DB = 3.0` — does not chase deeper nulls
  by 3 dB more than necessary.

### 3.6 Time alignment (`time_align.rs`)

When measurements come from synchronised WAV recordings of an MLS or
Dirac probe, the time of arrival of each channel can be measured
directly. `find_arrival_time(wav)` performs threshold detection
relative to peak, and `detect_delay_with_probe()` cross-correlates the
recovered IR with the reference signal envelope. The shortest arrival
becomes the reference, and per-channel **delay plugins** equalise the
others to within one sample.

When per-channel probe-arrival overrides are passed in (the GPUI
"Detect delay" UI step), `optimize_room_with_probe_arrivals()` skips
WAV-onset detection and uses the measured ms values directly.

### 3.7 EQ optimization with Schroeder split

The Schroeder frequency `f_S` is the boundary between **modal** room
behaviour (sparse, isolated resonances — narrow, high-Q corrections
work) and **statistical** behaviour (overlapping modes — broad
corrections work):

$$ f_S \approx 2000 \sqrt{\frac{T_{60}}{V}} $$

(or the simplified `11885 / √V` from room volume alone).

```mermaid
flowchart TB
    SS{schroeder_split<br/>enabled?}
    SS -->|no| STD[Standard single-pass<br/>EQ search<br/>min_freq..max_freq]
    SS -->|yes| LO[Low-band pass<br/>min_freq..f_S<br/>high Q, no boost]
    LO --> HI[High-band pass<br/>f_S..max_freq<br/>low Q, optionally shelves only]
    HI --> COMBINE[Combine biquads]
    STD --> RESULT
    COMBINE --> RESULT[Final filter set]
```

Each pass runs `autoeq::optim::optimize_filters` over the configured
algorithm:

* **DE** (Differential Evolution) — global search seeded with a Sobol
  quasi-random population. Robust to multi-modal loss landscapes,
  parameter strategies (`currenttobest1bin`, `rand1bin`, …).
* **NLOPT COBYLA** — local trust-region constrained search; very fast
  when the initial guess is good.
* **NLOPT ISRES** — global stochastic ranking ES.
* **MetaHeuristics PSO** etc.

The **loss function** is selected by `loss_type`:

* **`flat`** — ERB-weighted MSE between the corrected curve and the
  target.
* **`score`** — Olive / Toole speaker-preference score
  (NBD + LFX + SM·PIR with bass-boost shaping).
* **`epa`** — Zwicker/Fastl psychoacoustic composite of flatness +
  sharpness + roughness + loudness-balance + cubic-distortion-tone
  level. Tunable via `OptimizerConfig.epa_config`. Pre/post EPA scores
  are always emitted in the JSON metadata.

### 3.8 FIR / mixed-phase post-generation

When `processing_mode` is `PhaseLinear` or `Hybrid`, the IIR result
is converted to a phase-linear FIR by inverse-FFT of the magnitude
response (windowed, length = `fir_taps`). The chain then gets a
`convolution` plugin that loads `<channel>_fir.wav`.

`MixedPhase` instead decomposes the impulse response into
**minimum-phase** and **excess-phase** parts (cepstral folding) and
generates a short excess-phase corrector FIR. This compensates the
group-delay character of the room without the latency cost of full
phase linearisation.

`PhaseCorrection` adds a standalone rePhase-style allpass-only
correction on top.

### 3.9 Channel matching (≥ 2 channels)

After every channel is independently optimised, the optimiser still
needs to make the channels match each other. Three corrections run:

```mermaid
flowchart LR
    ICD[Inter-channel deviation<br/>per-band SPL Δ] --> SA[Spectral alignment<br/>shelf filters + gain]
    SA --> VOG[Voice of God<br/>timbre matching]
    VOG --> RESULT[Final per-channel<br/>plugins]
```

* **`compute_inter_channel_deviation`** — a band-by-band SPL
  difference report (the GPUI front-end shows this as a heatmap).
* **`compute_spectral_alignment`** — fits low/high shelves + gain to
  bring each channel's broadband balance toward the cross-channel
  average.
* **`compute_voice_of_god`** — narrower-band timbre matching for
  detailed transient consistency between L / R / centre.

### 3.10 Sanity check & IR computation

Pre/post impulse responses are computed for visualisation. The final
`RoomOptimizationResult` is run through `sanity_check_result()`:

* every channel's `freq`/`spl` lengths match;
* no NaN / non-finite SPL values (which would signal optimiser
  divergence).

Debug builds `debug_assert!` so test runs surface the exact violated
invariant; release builds return a clean `Err` so fuzz/QA jobs report
divergence rather than ship a corrupted DSP chain.

---

## 4. Topology workflows

### 4.1 Stereo 2.0 / 2.1

`optimize_stereo_2_0` and `optimize_stereo_2_1` reuse
`process_single_speaker` for L and R, but layer:

* **2.1**: the `LFE` / sub channel gets its own optimisation against
  the bass-management crossover (LR24 by default).
* **Phase alignment**: between each main and the sub at the crossover
  frequency.
* **Bass management routing**: sub gain is linked across mains so
  bass content sums coherently rather than per-channel-independent.

### 4.2 Home Cinema

```mermaid
flowchart TB
    M[Mains roles:<br/>L, R, C, LSurround, RSurround,<br/>LBack, RBack, …] --> A[Per-channel optimization]
    SUB[LFE / Sub roles] --> A
    A --> AL[Time-align via probe]
    AL --> BMG[Group bass-management<br/>by role group]
    BMG --> XO[Crossover frequency search<br/>per group]
    XO --> SUM[Predicted bass sum]
    SUM --> ADV[Advisories + headroom report]
    ADV --> OUT[Per-channel chains +<br/>global routing matrix]
```

Highlights:

* Roles are grouped (front, surround, surround-back, height) so a
  single crossover decision covers each group rather than fighting
  across symmetric channels.
* `optimize_home_cinema_group_crossovers` searches the crossover
  frequency for each group jointly with the sub gain, evaluating the
  predicted bass sum against the target curve.
* A **bass-bus headroom simulation** estimates how much summed bass
  energy hits the LFE bus at typical levels, surfacing clipping risk
  as advisories rather than silent saturation.
* The result includes a `BassManagementRoutingGraph` that the
  AudioEngine renders as a sparse matrix plugin sitting before any
  per-channel plugins.

### 4.3 Specialised aggregates

* **`MultiSubGroup`** — multiple subs treated as a single virtual
  channel; multi-seat MSO runs over the group.
* **`DBA` (Double Bass Array)** — front-wall sources + delayed
  back-wall sources to cancel the modal field below a target
  frequency.
* **`Cardioid`** — phase-and-delay-aligned pair of drivers producing a
  cardioid bass radiation pattern for sidewall cancellation.

Each aggregate has its own `build_*_dsp_chain` in `roomeq/output.rs`.

---

## 5. Optional advanced corrections

These run after per-channel EQ when configured:

```mermaid
flowchart TB
    R0[Per-channel EQ result] --> R1{decomposed_correction?}
    R1 -->|yes| R1Y[analyze_decomposed_correction<br/>modes vs reflections vs steady]
    R1 -->|no| R2
    R1Y --> R2{reflection_cancel?}
    R2 -->|yes| R2Y[Johnston IIR<br/>y_n = x_n - g · LP_x_n_minus_d_]
    R2 -->|no| R3
    R2Y --> R3{spatial_robustness?}
    R3 -->|yes| R3Y[Brännmark / Sternad<br/>multi-position robustness]
    R3 -->|no| R4
    R3Y --> R4{mixed_phase / phase_correction?}
    R4 -->|yes| R4Y[FIR convolution<br/>mixed-phase or rePhase]
    R4 -->|no| OUT
    R4Y --> OUT[(DspChainOutput)]
```

* **Decomposed correction** (Laborie/Bruno/Montoya, 2003) splits the
  IR into modal, early-reflection, and steady-state regions and
  applies different correction strategies to each.
* **First-reflection cancellation** (Johnston, AES 2008) subtracts a
  delayed low-passed copy of the input below ~500 Hz to cancel the
  measured first floor/ceiling reflection.
* **Spatial robustness** (Brännmark & Sternad, AES 2008 + EP2104374B1)
  trades single-point flatness for cross-position robustness with a
  spatial zero-clustering pre-ringing constraint.
* **Mixed-phase / phase correction** post-generates a short FIR that
  corrects excess phase without doubling latency.

---

## 6. Output and export

```mermaid
flowchart TB
    R[RoomOptimizationResult] --> O[create_dsp_chain_output]
    O --> J[(DspChainOutput JSON)]
    J --> AE[AudioEngine plugin runtime]
    J --> CD[CamillaDSP YAML]
    J --> APO[Equalizer APO / Peace text]
    J --> EE[EasyEffects JSON preset]
    J --> WL[Wavelet GraphicEQ text]
    J --> PW[PipeWire SPA-JSON .conf]
    J --> RN[Roon DSP preset JSON]
```

`DspChainOutput` carries:

* `channels` — per-channel `plugins` arrays
  (`gain``delay``eq` (biquads) → `convolution` (FIR) → …),
* `global_plugins` — a sparse matrix `crossover` plugin for bass
  management when the topology requires routing,
* `metadata` — pre/post combined scores, EPA per channel, optimizer
  algorithm, iteration counts, version stamp, and a full
  `BassManagementReport` (routing graph, group crossovers, headroom
  simulation, advisories).

`export_dsp_chain` (`roomeq/export.rs`) serialises the result into
external formats. Some targets (e.g. EasyEffects, Wavelet GraphicEQ)
do not support routing matrices or convolution; the exporter rejects
those configurations explicitly rather than silently dropping plugins.

---

## 7. Code map

```text
crates/autoeq/src/roomeq/
├── mod.rs                    # public re-exports and module wiring
├── pipeline.rs               # RoomPipeline / Event / Observer
├── optimize.rs               # optimize_room, optimize_speaker, RoomOptimizationResult
├── workflows.rs              # topology dispatch (stereo, home cinema, custom)
│   └── bass_management.rs    # group crossover optimisation
├── speaker_eq.rs             # process_single_speaker, multi-meas strategies
│   └── schroeder.rs          # optimize_eq_with_optional_schroeder
├── group_processing.rs       # multi-driver crossover, multisub, DBA, cardioid, mixed-mode
├── eq.rs                     # core EQ search adapter (autoeq::optim wrapper)
├── crossover.rs              # crossover filter design (LR / BW)
├── time_align.rs             # WAV onset + probe-burst delay detection
├── multiseat.rs              # MSO variance / constrained / average optimisation
├── phase_alignment.rs        # sub/main delay+polarity grid+refine
├── target_tilt.rs            # build_complete_target_curve
├── excursion.rs              # F3 detection + protection HPF
├── multisub.rs               # multi-sub flat optimisation
├── dba.rs                    # double bass array
├── cea2034_correction.rs     # 3-pass speaker pre-correction
├── mixed_phase.rs            # IIR + excess-phase FIR
├── impulse_analysis.rs       # decomposed correction (modes/refl/steady)
├── reflection_cancel.rs      # Johnston first-reflection canceller
├── spatial_robustness.rs     # Brännmark / Sternad multi-position
├── spectral_align.rs         # inter-channel shelf+gain alignment
├── voice_of_god.rs           # narrow-band timbre matching across channels
├── fir.rs                    # FIR generation from biquad set
├── ir_waveform.rs            # pre/post IR computation
├── output.rs                 # DSP-chain construction and serialisation
├── export.rs                 # CamillaDSP / APO / EasyEffects / … exporters
├── home_cinema.rs            # bass management report, headroom, signal-flow advisories
├── bass_phase_confidence.rs  # bass-phase coherence gate for GD-Opt v2
├── gd_opt.rs                 # group-delay optimisation v2 (LowLatency IIR)
├── frequency_grid.rs         # grid validation, common-range computation
├── slope.rs                  # broadband slope estimation
├── temporal_targets.rs       # perceptual decay thresholds
├── synthetic.rs              # synthetic measurements for QA
├── progress.rs               # multi-stage progress reporter
└── types/                    # config / output data structures
```

---

## 8. Where to look next

* `crates/autoeq/bin/roomeq/README.md` — CLI usage, configuration
  examples, scenario A/B walkthroughs.
* `crates/autoeq/bin/roomeq/INPUT_FORMAT.md` — full input schema, every
  optional block documented.
* `crates/autoeq/bin/roomeq/OUTPUT_FORMAT.md` — full output schema for
  the `DspChainOutput`.
* `crates/autoeq/REFERENCES.md` — bibtex references for every cited
  algorithm.
* `crates/math-audio/math-iir-fir/REFERENCES.md` — RBJ cookbook,
  Linkwitz-Riley, Orfanidis, Vicanek, Zavalishin TPT/SVF, Kirkeby,
  filtfilt.
* `crates/math-audio/math-optimisation/REFERENCES.md` — DE, JADE,
  L-SHADE, COBYLA, ISRES, Levenberg-Marquardt.

---

## References

This section consolidates the citations directly relevant to the
RoomEQ pipeline. The full bibliography lives in
[`REFERENCES.md`](../REFERENCES.md).

### CEA / CTA-2034 spinorama standard

Consumer Technology Association. *ANSI/CTA-2034-A — Standard Method of
Measurement for In-Home Loudspeakers.* 2015. (Originally CEA-2034.)
Used by `cea2034.rs` to derive the listening-window / sound-power /
PIR curves and the NBD / LFX / SM-PIR sub-metrics that feed the
preference score.

### Olive speaker preference score

Olive, Sean E. *A Multiple Regression Model for Predicting Loudspeaker
Preference Using Objective Measurements: Parts I & II.* AES Conv. 116
& 117, 2004.
<https://www.aes.org/e-lib/browse.cfm?elib=12794>

Toole, Floyd E. *Sound Reproduction: The Acoustics and Psychoacoustics
of Loudspeakers and Rooms.* 3rd ed., Routledge, 2017. ISBN 978-1138921368.

### Harman in-room target / -0.8 dB/oct slope

Olive, Welti, McMullin. *The Influence of Listeners' Experience, Age,
and Culture on Headphone Sound Quality Preferences.* AES Conv. 137,
2014.

Toole, Floyd E. *The Measurement and Calibration of Sound Reproducing
Systems.* J. Audio Eng. Soc., 63(7/8):512–541, 2015.
DOI 10.17743/jaes.2015.0064.

### Schroeder frequency

Schroeder, Manfred R. *Frequency-correlation functions of frequency
responses in rooms.* JASA 34(12):1819–1823, 1962.
DOI 10.1121/1.1909136.

Schroeder, Manfred R. *The "Schroeder Frequency" Revisited.* JASA
99(5):3240–3241, 1996. DOI 10.1121/1.414868.

Used by `roomeq/eq.rs::estimate_schroeder_frequency` to split modal
from statistical room behaviour.

### Phase-coherent EQ

Klein, Werner, Brandenburg. *Phase-Coherent Equalization of
Loudspeakers.* AES Conv. 142, 2017.

Zacharov, Bech, Meares. *The Use of Trained Listeners in Multichannel
Sound Evaluation: Effect of Phase on Loudspeaker Sound Quality.* AES
Conv. 105, 1998.

### Brännmark & Sternad — robust room correction

Brännmark, Ahlén. *Spatially Robust Audio Compensation Based on SIMO
Feedforward Equalization.* AES Conv. 124, 2008.
<https://www.aes.org/e-lib/browse.cfm?elib=14529>

Brännmark, Sternad. *Method and apparatus for designing low-pre-ringing
inverse filters.* European Patent EP2104374B1, 2009.

Used by `roomeq/spatial_robustness.rs` and the pre-ringing constraint
in `roomeq/mixed_phase.rs`.

### Decomposed room correction

Laborie, Bruno, Montoya. *A New Comprehensive Approach of Surround
Sound Recording.* AES Conv. 114, 2003.
<https://www.aes.org/e-lib/browse.cfm?elib=12565>

Used by `roomeq/impulse_analysis.rs` to split the IR into modal,
early-reflection, and steady-state regions for differentiated
correction.

### Johnston — first-reflection cancellation

Johnston, James D. *Loudspeaker / Room Equalization in the Time and
Frequency Domains.* AES Conv. 125, 2008.

Used by `roomeq/reflection_cancel.rs::cancel_first_reflection`.

### ERB / cochlear bandwidth

Glasberg, Moore. *Derivation of auditory filter shapes from
notched-noise data.* Hearing Research 47(1-2):103–138, 1990.
DOI 10.1016/0378-5955(90)90170-T.

`24.7 · (1 + 4.37 · f/1000)` is used by `loss/enhanced_weights.rs` for
ERB-weighted flatness loss.

### Zwicker / Fastl psychoacoustics (EPA)

Zwicker, Fastl. *Psychoacoustics: Facts and Models.* 3rd ed., Springer,
2007. ISBN 978-3540231592. DOI 10.1007/978-3-540-68888-4.

Zwicker. *Ein Verfahren zur Berechnung der Lautstärke.* Acustica 10:
304–308, 1960.

### ISO 226 equal-loudness contours

International Organization for Standardization. *ISO 226:2003 —
Acoustics — Normal equal-loudness-level contours.* 2003.

### DIN 45692 — sharpness

Deutsches Institut für Normung. *DIN 45692:2009-08 — Measurement
technique for the simulation of the auditory sensation of sharpness.*
2009.

Aures, Wolfgang. *Berechnungsverfahren für den sensorischen Wohlklang
beliebiger Schallsignale.* Acustica 59:130–141, 1985.

### Sobol quasi-random sequences

Sobol', Ilya M. *On the distribution of points in a cube and the
approximate evaluation of integrals.* USSR Comp. Math. & Math. Phys.
7(4):86–112, 1967. DOI 10.1016/0041-5553(67)90144-9.

Joe, Kuo. *Constructing Sobol sequences with better two-dimensional
projections.* SIAM J. Sci. Comput. 30(5):2635–2654, 2008.
DOI 10.1137/070709359.

Used by `optim/de.rs::init_sobol` to seed the Differential Evolution
population.

### Multi-objective genetic algorithms

Deb, Pratap, Agarwal, Meyarivan. *A fast and elitist multiobjective
genetic algorithm: NSGA-II.* IEEE TEVC 6(2):182–197, 2002.
DOI 10.1109/4235.996017.

Ramos, López. *Multiobjective Genetic Algorithm Optimization of
Linkwitz-Riley Crossovers Using Group Delay and Magnitude Response
Criteria.* AES Conv. 121, 2006.

### Optimization algorithms

See [`crates/math-audio/math-optimisation/REFERENCES.md`](../../math-audio/math-optimisation/REFERENCES.md)
for COBYLA (Powell, 1994), ISRES (Runarsson & Yao, 2005),
Differential Evolution (Storn & Price, 1997 — and JADE / L-SHADE
variants), and Levenberg-Marquardt.

### Filter design

See [`crates/math-audio/math-iir-fir/REFERENCES.md`](../../math-audio/math-iir-fir/REFERENCES.md)
for the RBJ cookbook (Bristow-Johnson), Linkwitz-Riley crossovers,
Butterworth filters, Orfanidis high-shelf design, Vicanek matched
biquads, Zavalishin TPT/SVF state-variable filters, the Kirkeby
inverse-filter regularisation, and `filtfilt` zero-phase filtering.