Skip to main content

secs_engine/
ba.rs

1//! Bass alignment, ported from `correction_engine.py:624–1095,1371–1428`.
2//! D1 is deliberately not applied: bus delay still penalizes coherent addition.
3
4use crate::chain::{BaRow, PreparedBaTerm, gather, sub_sum, sub_sum_prepared};
5use crate::decay::{BaLateGrid, ba_late_db};
6use crate::fit::{Fitter, xo_target_peq};
7use crate::grid::{BINS, SR_F, design_freqs};
8use crate::input::{BandType, BundleGain, DbaRow, LayoutKind, Peq, SubAllpass, SubMode};
9use crate::measurement::{Prepared, Spectra, onset_index, ramp_phase};
10use crate::notes::Note;
11use crate::setup::{Setup, SubFilter};
12use crate::{C, EngineConfig, EngineError, MeasurementSet, Tuning};
13use secs_compute::CancelToken;
14use secs_dsp::optimize::{DifferentialEvolutionOptions, differential_evolution};
15use secs_dsp::{
16    fft, pairwise_sum, response,
17    smooth::{self, Fraction},
18};
19use std::{f64::consts::PI, sync::Arc};
20
21pub struct BaInput<'a> {
22    pub measurements: &'a MeasurementSet,
23    pub prepared: &'a Prepared,
24    pub setup: &'a Setup,
25    pub config: &'a EngineConfig,
26    pub tuning: &'a Tuning,
27    pub xo_of: &'a [(String, f64)],
28    pub ref_level: f64,
29    pub cancel: &'a CancelToken,
30}
31
32#[derive(Debug, Clone, PartialEq)]
33pub struct ManualPlan {
34    pub fixed: Vec<String>,
35    pub bundles: Vec<(String, Vec<String>)>,
36    pub auto: Vec<String>,
37    pub gain_anchored: bool,
38}
39
40#[derive(Debug, Clone, PartialEq)]
41pub struct LayoutInfo {
42    pub kind: LayoutKind,
43    pub members: Vec<String>,
44    pub gains: Vec<(String, f64)>,
45    pub front: Vec<String>,
46    pub rear: Vec<String>,
47    pub rear_ms: f64,
48    pub auto_ms: Option<f64>,
49    pub trim_ms: f64,
50    pub min_ms: Option<f64>,
51    pub peq: Vec<Peq>,
52    #[cfg(test)]
53    candidates: Vec<(f64, f64)>,
54}
55
56pub struct SubAlignment {
57    pub rows: Vec<BaRow>,
58    pub sub_filters: Vec<(String, SubFilter)>,
59    pub h_subs: Vec<(u32, String, Arc<[C]>)>,
60    pub layout_rel: Vec<(String, Vec<(String, BaRow)>)>,
61    pub layout_info: Vec<(String, LayoutInfo)>,
62    pub sub_level_db: Option<f64>,
63    pub bus_gain: f64,
64    pub bus_peq: Vec<Peq>,
65    pub sub_sum_by_pos: Vec<(u32, Arc<[C]>)>,
66    pub plan: Option<ManualPlan>,
67    pub notes: Vec<Note>,
68}
69
70fn check(input: &BaInput<'_>) -> Result<(), EngineError> {
71    if input.cancel.is_cancelled() {
72        Err(EngineError::Cancelled)
73    } else {
74        Ok(())
75    }
76}
77fn row(gain_db: f64, delay_ms: f64, polarity: i8, allpass: Vec<(f64, f64)>) -> BaRow {
78    BaRow {
79        gain_db,
80        delay_ms,
81        polarity,
82        allpass,
83    }
84}
85fn mean(x: &[f64]) -> f64 {
86    pairwise_sum(x) / x.len() as f64
87}
88fn median(mut x: Vec<f64>) -> f64 {
89    x.sort_by(f64::total_cmp);
90    let m = x.len() / 2;
91    if x.len().is_multiple_of(2) {
92        (x[m - 1] + x[m]) / 2.0
93    } else {
94        x[m]
95    }
96}
97fn mag(x: C) -> f64 {
98    x.re.hypot(x.im)
99}
100fn power(x: C) -> f64 {
101    let a = mag(x);
102    a * a
103}
104fn weight(pos: u32, main: u32) -> f64 {
105    if pos == main { 5.0 } else { 1.0 }
106}
107fn base(input: &BaInput<'_>, role: &str) -> f64 {
108    input
109        .prepared
110        .align_delay_ms
111        .iter()
112        .find(|(r, _)| r == role)
113        .unwrap()
114        .1
115}
116fn complete(h: &Spectra, roles: &[String]) -> Vec<u32> {
117    h.positions()
118        .iter()
119        .copied()
120        .filter(|&p| roles.iter().all(|r| h.get(p, r).is_some()))
121        .collect()
122}
123fn sums(
124    h: &Spectra,
125    roles: &[String],
126    rows: &[BaRow],
127    idx: &[usize],
128    positions: &[u32],
129) -> Vec<(u32, Arc<[C]>)> {
130    let f = gather(design_freqs(), idx);
131    positions
132        .iter()
133        .map(|&p| {
134            let cells: Vec<_> = roles
135                .iter()
136                .map(|r| gather(h.get(p, r).unwrap(), idx))
137                .collect();
138            let terms: Vec<_> = cells
139                .iter()
140                .zip(rows)
141                .map(|(h, r)| (h.as_slice(), r))
142                .collect();
143            (p, sub_sum(&terms, &f).into())
144        })
145        .collect()
146}
147fn grid(hi: f64) -> Vec<usize> {
148    let f = design_freqs();
149    let lo = f.partition_point(|f| *f < 20.0);
150    let hi = f.partition_point(|f| *f <= hi) - 1;
151    let start = (lo as f64).log10();
152    let stop = (hi as f64).log10();
153    let mut out: Vec<_> = (0..160)
154        .map(|i| {
155            let v = if i == 159 {
156                stop
157            } else {
158                start + i as f64 * ((stop - start) / 159.0)
159            };
160            10f64.powf(v).round_ties_even() as usize
161        })
162        .collect();
163    out.dedup();
164    out
165}
166
167/// NumPy polyfit: descending Vandermonde, strided column norm, scaled SVD,
168/// rcond = n * eps. A three-column one-sided Jacobi SVD avoids normal equations.
169/// LAPACK's divide-and-conquer SVD has different final bits; the oracle measures
170/// their effect on the objective, rather than claiming bit-identical coefficients.
171struct Quadratic {
172    x: Vec<f64>,
173    columns: [Vec<f64>; 3],
174    right: [[f64; 3]; 3],
175    scale: [f64; 3],
176    norm: [f64; 3],
177}
178impl Quadratic {
179    fn new(x: Vec<f64>) -> Self {
180        let mut columns = [
181            x.iter().map(|v| v * v).collect::<Vec<_>>(),
182            x.clone(),
183            vec![1.0; x.len()],
184        ];
185        // NumPy axis=0 on its C-order Vandermonde is a sequential reduction.
186        let scale = std::array::from_fn(|j| columns[j].iter().fold(0.0, |s, v| s + v * v).sqrt());
187        for j in 0..3 {
188            for v in &mut columns[j] {
189                *v /= scale[j];
190            }
191        }
192        let mut right = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
193        for _ in 0..30 {
194            let mut changed = false;
195            for (p, q) in [(0, 1), (0, 2), (1, 2)] {
196                let a = columns[p].iter().map(|v| v * v).sum::<f64>();
197                let b = columns[q].iter().map(|v| v * v).sum::<f64>();
198                let c = columns[p]
199                    .iter()
200                    .zip(&columns[q])
201                    .map(|(v, w)| v * w)
202                    .sum::<f64>();
203                if c.abs() <= f64::EPSILON * (a * b).sqrt() {
204                    continue;
205                }
206                changed = true;
207                let z = (b - a) / (2.0 * c);
208                let t = z.signum() / (z.abs() + (1.0 + z * z).sqrt());
209                let co = 1.0 / (1.0 + t * t).sqrt();
210                let si = co * t;
211                let (left, right_column) = columns.split_at_mut(q);
212                for (a, b) in left[p].iter_mut().zip(&mut right_column[0]) {
213                    let old_a = *a;
214                    let old_b = *b;
215                    *a = co * old_a - si * old_b;
216                    *b = si * old_a + co * old_b;
217                }
218                for r in &mut right {
219                    let a = r[p];
220                    let b = r[q];
221                    r[p] = co * a - si * b;
222                    r[q] = si * a + co * b;
223                }
224            }
225            if !changed {
226                break;
227            }
228        }
229        let norm = std::array::from_fn(|j| {
230            pairwise_sum(&columns[j].iter().map(|v| v * v).collect::<Vec<_>>())
231        });
232        Self {
233            x,
234            columns,
235            right,
236            scale,
237            norm,
238        }
239    }
240    fn rough(&self, y: &[f64]) -> f64 {
241        let max = self.norm.iter().copied().fold(0.0, f64::max).sqrt();
242        let mut coef = [0.0; 3];
243        for j in 0..3 {
244            if self.norm[j].sqrt() <= self.x.len() as f64 * f64::EPSILON * max {
245                continue;
246            }
247            let dot = pairwise_sum(
248                &self.columns[j]
249                    .iter()
250                    .zip(y)
251                    .map(|(a, b)| a * b)
252                    .collect::<Vec<_>>(),
253            ) / self.norm[j];
254            for (i, c) in coef.iter_mut().enumerate() {
255                *c += self.right[i][j] * dot;
256            }
257        }
258        for (c, s) in coef.iter_mut().zip(self.scale) {
259            *c /= s;
260        }
261        mean(
262            &self
263                .x
264                .iter()
265                .zip(y)
266                .map(|(x, y)| {
267                    let d = y - ((coef[0] * x + coef[1]) * x + coef[2]);
268                    d * d
269                })
270                .collect::<Vec<_>>(),
271        )
272    }
273}
274fn variance(levels: &[Vec<f64>]) -> f64 {
275    if levels.len() <= 1 {
276        return 0.0;
277    }
278    let mut out = vec![0.0; levels[0].len()];
279    for i in 0..out.len() {
280        let m = levels.iter().fold(0.0, |s, l| s + l[i]) / levels.len() as f64;
281        out[i] = levels.iter().fold(0.0, |s, l| s + (l[i] - m).powi(2)) / levels.len() as f64;
282    }
283    mean(&out)
284}
285fn gd_variance(h: &[C], f: &[f64]) -> f64 {
286    let mut phase: Vec<_> = h.iter().map(|h| h.im.atan2(h.re)).collect();
287    let original = phase.clone();
288    let mut correction = 0.0;
289    for i in 1..phase.len() {
290        let d = original[i] - original[i - 1];
291        let mut dm = (d + PI).rem_euclid(2.0 * PI) - PI;
292        if dm == -PI && d > 0.0 {
293            dm = PI;
294        }
295        if d.abs() >= PI {
296            correction += dm - d;
297        }
298        phase[i] += correction;
299    }
300    let x: Vec<_> = f.iter().map(|f| 2.0 * PI * f).collect();
301    let n = x.len();
302    let mut gd = vec![0.0; n];
303    gd[0] = (phase[1] - phase[0]) / (x[1] - x[0]);
304    gd[n - 1] = (phase[n - 1] - phase[n - 2]) / (x[n - 1] - x[n - 2]);
305    for i in 1..n - 1 {
306        let a = x[i] - x[i - 1];
307        let b = x[i + 1] - x[i];
308        gd[i] = (-b / (a * (a + b))) * phase[i - 1]
309            + ((b - a) / (a * b)) * phase[i]
310            + (a / (b * (a + b))) * phase[i + 1];
311    }
312    for v in &mut gd {
313        *v = -*v * 1000.0;
314    }
315    let m = median(gd.clone());
316    mean(&gd.iter().map(|v| (v - m).powi(2)).collect::<Vec<_>>())
317}
318
319fn individual_db(power: &[Vec<f64>]) -> Vec<Vec<f64>> {
320    power
321        .iter()
322        .map(|row| row.iter().map(|p| 10.0 * (p + 1e-30).log10()).collect())
323        .collect()
324}
325
326struct BaSetup {
327    grid: Vec<usize>,
328    positions: Vec<u32>,
329    late: BaLateGrid,
330    raw_late: Vec<f64>,
331    h_grid: Vec<Vec<Vec<C>>>,
332    h_lin: Vec<Vec<Vec<C>>>,
333    f: Vec<f64>,
334    quadratic: Quadratic,
335}
336impl BaSetup {
337    fn new(input: &BaInput<'_>, h: &Spectra, hi: f64) -> Self {
338        let grid = grid(hi);
339        let positions = complete(h, &input.config.subs);
340        let f = gather(design_freqs(), &grid);
341        let late = BaLateGrid::new(hi);
342        let h_grid = positions
343            .iter()
344            .map(|&p| {
345                input
346                    .config
347                    .subs
348                    .iter()
349                    .map(|r| gather(h.get(p, r).unwrap(), &grid))
350                    .collect()
351            })
352            .collect();
353        let h_lin: Vec<Vec<Vec<C>>> = positions
354            .iter()
355            .map(|&p| {
356                input
357                    .config
358                    .subs
359                    .iter()
360                    .map(|r| h.get(p, r).unwrap()[..late.lin].to_vec())
361                    .collect()
362            })
363            .collect();
364        let rows: Vec<_> = input
365            .config
366            .subs
367            .iter()
368            .map(|r| row(0.0, base(input, r), 1, vec![]))
369            .collect();
370        let raw_late = h_lin
371            .iter()
372            .map(|hs| {
373                let terms: Vec<_> = hs
374                    .iter()
375                    .zip(&rows)
376                    .map(|(h, r)| (h.as_slice(), r))
377                    .collect();
378                ba_late_db(&late, &sub_sum(&terms, &design_freqs()[..late.lin]))
379            })
380            .collect();
381        let quadratic = Quadratic::new(f.iter().map(|f| f.ln()).collect());
382        Self {
383            grid,
384            positions,
385            late,
386            raw_late,
387            h_grid,
388            h_lin,
389            f,
390            quadratic,
391        }
392    }
393    fn indiv(&self, rows: Option<&[BaRow]>) -> Vec<Vec<f64>> {
394        self.h_grid
395            .iter()
396            .map(|hs| {
397                let mut total = vec![0.0; self.grid.len()];
398                for (j, h) in hs.iter().enumerate() {
399                    let g = rows.map_or(1.0, |r| 10f64.powf(r[j].gain_db / 10.0));
400                    for (s, &v) in total.iter_mut().zip(h) {
401                        *s += power(v) * g;
402                    }
403                }
404                total
405            })
406            .collect()
407    }
408    // Existing cost-oracle callers supply powers. Production precomputes the
409    // fixed reference in dB once before entering differential evolution.
410    #[cfg(test)]
411    fn cost(
412        &self,
413        input: &BaInput<'_>,
414        rows: &[BaRow],
415        fixed_indiv: Option<&[Vec<f64>]>,
416        bus: Option<&[C]>,
417    ) -> f64 {
418        let fixed_db = fixed_indiv.map(individual_db);
419        self.cost_with_db(input, rows, fixed_db.as_deref(), bus)
420    }
421
422    fn cost_with_db(
423        &self,
424        input: &BaInput<'_>,
425        rows: &[BaRow],
426        fixed_indiv: Option<&[Vec<f64>]>,
427        bus: Option<&[C]>,
428    ) -> f64 {
429        if input.cancel.is_cancelled() {
430            return 1e30;
431        }
432        let owned;
433        let indiv = if let Some(i) = fixed_indiv {
434            i
435        } else {
436            owned = individual_db(&self.indiv(Some(rows)));
437            &owned
438        };
439        let grid_terms: Vec<_> = rows
440            .iter()
441            .map(|r| PreparedBaTerm::new(&self.f, r))
442            .collect();
443        let lin_terms: Vec<_> = rows
444            .iter()
445            .map(|r| PreparedBaTerm::new(&design_freqs()[..self.late.lin], r))
446            .collect();
447        let mut levels = Vec::new();
448        let mut total = 0.0;
449        let mut weights = 0.0;
450        for (k, &pos) in self.positions.iter().enumerate() {
451            let sum = sub_sum_prepared(&self.h_grid[k], &grid_terms, self.f.len());
452            let db: Vec<_> = sum
453                .iter()
454                .map(|&h| 20.0 * (mag(h) + 1e-20).log10())
455                .collect();
456            let ind = &indiv[k];
457            let rough = self.quadratic.rough(&db);
458            let loss = mean(
459                &ind.iter()
460                    .zip(&db)
461                    .map(|(i, d)| (i - d - 3.0).max(0.0).powi(2))
462                    .collect::<Vec<_>>(),
463            );
464            let eff = mean(&db.iter().zip(ind).map(|(d, i)| d - i).collect::<Vec<_>>());
465            let with_bus: Vec<_> = sum
466                .iter()
467                .zip(&self.grid)
468                .map(|(&s, &i)| bus.map_or(s, |b| s * b[i]))
469                .collect();
470            let gd = gd_variance(&with_bus, &self.f);
471            let ls = sub_sum_prepared(&self.h_lin[k], &lin_terms, self.late.lin);
472            let worse = (ba_late_db(&self.late, &ls) - self.raw_late[k]).max(0.0);
473            let w = weight(pos, input.config.main_position);
474            weights += w;
475            total += w
476                * (rough + 0.5 * loss + 0.02 * gd + 0.5 * worse * worse
477                    - input.tuning.ba_eff_weight * eff);
478            levels.push(db);
479        }
480        total / weights + 0.5 * variance(&levels)
481    }
482}
483
484fn manual_value(setup: &Setup, role: &str) -> BaRow {
485    setup
486        .sub_manual
487        .iter()
488        .find(|(r, _)| r == role)
489        .map_or_else(
490            || row(0.0, 0.0, 1, vec![]),
491            |(_, m)| {
492                row(
493                    m.gain_db,
494                    m.delay_ms,
495                    if m.polarity < 0 { -1 } else { 1 },
496                    m.allpass.iter().take(2).copied().collect(),
497                )
498            },
499        )
500}
501fn manual_plan(input: &BaInput<'_>) -> Option<ManualPlan> {
502    let mut p = ManualPlan {
503        fixed: vec![],
504        bundles: vec![],
505        auto: vec![],
506        gain_anchored: false,
507    };
508    for r in &input.config.subs {
509        match input
510            .setup
511            .sub_manual
512            .iter()
513            .find(|(s, _)| s == r)
514            .map(|(_, m)| m)
515        {
516            Some(m) if m.mode == SubMode::Fixed => p.fixed.push(r.clone()),
517            Some(m) if m.mode == SubMode::Bundle => {
518                let name = if m.bundle.is_empty() { "A" } else { &m.bundle };
519                if let Some((_, rs)) = p.bundles.iter_mut().find(|(b, _)| b == name) {
520                    rs.push(r.clone());
521                } else {
522                    p.bundles.push((name.to_owned(), vec![r.clone()]));
523                }
524            }
525            _ => p.auto.push(r.clone()),
526        }
527    }
528    if p.fixed.is_empty() && p.bundles.is_empty() {
529        return None;
530    }
531    p.gain_anchored = !p.fixed.is_empty()
532        || (!p.bundles.is_empty() && input.tuning.bundle_gain == BundleGain::Fixed);
533    Some(p)
534}
535
536struct Unit {
537    roles: Vec<(String, BaRow)>,
538    center: f64,
539    gain_var: bool,
540    ap_var: bool,
541    auto: bool,
542}
543struct Search {
544    bounds: Vec<(f64, f64)>,
545    units: Vec<Unit>,
546    constants: Vec<(String, BaRow)>,
547    fixed_indiv: bool,
548    skip: bool,
549}
550impl Search {
551    fn new(input: &BaInput<'_>, alignment: &SubAlignment) -> Self {
552        let qmax = if input.tuning.sub_allpass == SubAllpass::LowQ {
553            1.0
554        } else {
555            3.0
556        };
557        let aps = [(20.0, 150.0), (0.3, qmax), (20.0, 150.0), (0.3, qmax)];
558        let mut out = Self {
559            bounds: vec![],
560            units: vec![],
561            constants: vec![],
562            fixed_indiv: true,
563            skip: false,
564        };
565        let Some(plan) = &alignment.plan else {
566            for r in &input.config.subs {
567                out.bounds.extend([
568                    (-8.0, 0.0),
569                    (-input.tuning.ba_delay_ms, input.tuning.ba_delay_ms),
570                    (0.0, 1.0),
571                ]);
572                out.bounds.extend(aps);
573                out.units.push(Unit {
574                    roles: vec![(r.clone(), row(0.0, 0.0, 1, vec![]))],
575                    center: base(input, r),
576                    gain_var: true,
577                    ap_var: true,
578                    auto: true,
579                });
580            }
581            out.skip = input.config.subs.len() == 1;
582            return out;
583        };
584        let extra = if plan.fixed.is_empty() {
585            0.0
586        } else {
587            input.config.sub_main_delay_ms
588        };
589        out.constants = plan
590            .fixed
591            .iter()
592            .map(|r| {
593                let mut v = manual_value(input.setup, r);
594                v.delay_ms -= extra;
595                (r.clone(), v)
596            })
597            .collect();
598        let off = if plan.fixed.is_empty() {
599            0.0
600        } else {
601            median(
602                out.constants
603                    .iter()
604                    .map(|(r, v)| v.delay_ms - base(input, r))
605                    .collect(),
606            )
607        };
608        let mut anchored = plan.fixed.clone();
609        if input.tuning.bundle_gain == BundleGain::Fixed {
610            anchored.extend(plan.bundles.iter().flat_map(|(_, rs)| rs).cloned());
611        }
612        let reference = (!anchored.is_empty()).then(|| {
613            mean(
614                &anchored
615                    .iter()
616                    .map(|r| manual_value(input.setup, r).gain_db)
617                    .collect::<Vec<_>>(),
618            )
619        });
620        out.fixed_indiv = reference.is_none();
621        let gain_bounds =
622            |rel: f64| reference.map_or((-rel - 8.0, -rel), |r| (r - rel - 8.0, r - rel + 8.0));
623        let delay_bounds = |center: f64| {
624            let lo = if plan.fixed.is_empty() {
625                -input.tuning.ba_delay_ms
626            } else {
627                (-input.tuning.ba_delay_ms).max(-extra - center)
628            };
629            (lo, input.tuning.ba_delay_ms.max(lo + 1.0))
630        };
631        for r in &plan.auto {
632            let center = base(input, r) + off;
633            out.bounds
634                .extend([gain_bounds(0.0), delay_bounds(center), (0.0, 1.0)]);
635            out.bounds.extend(aps);
636            out.units.push(Unit {
637                roles: vec![(r.clone(), row(0.0, 0.0, 1, vec![]))],
638                center,
639                gain_var: true,
640                ap_var: true,
641                auto: true,
642            });
643        }
644        for (name, rs) in &plan.bundles {
645            let layout = alignment.layout_rel.iter().find(|(b, _)| b == name);
646            let mut vals = layout.map_or_else(
647                || {
648                    rs.iter()
649                        .map(|r| (r.clone(), manual_value(input.setup, r)))
650                        .collect()
651                },
652                |(_, v)| v.clone(),
653            );
654            let dmin = vals
655                .iter()
656                .map(|(_, v)| v.delay_ms)
657                .fold(f64::INFINITY, f64::min);
658            let center = vals
659                .iter()
660                .map(|(r, v)| base(input, r) - (v.delay_ms - dmin))
661                .fold(f64::NEG_INFINITY, f64::max)
662                + off;
663            let gain_var = input.tuning.bundle_gain == BundleGain::Auto || layout.is_some();
664            let ap_var = layout.is_some() && input.tuning.sub_allpass != SubAllpass::None;
665            if gain_var {
666                out.bounds.push(gain_bounds(mean(
667                    &vals.iter().map(|(_, v)| v.gain_db).collect::<Vec<_>>(),
668                )));
669            }
670            out.bounds.extend([delay_bounds(center), (0.0, 1.0)]);
671            if ap_var {
672                out.bounds.extend(aps);
673            }
674            for (_, v) in &mut vals {
675                v.delay_ms -= dmin;
676            }
677            out.units.push(Unit {
678                roles: vals,
679                center,
680                gain_var,
681                ap_var,
682                auto: false,
683            });
684        }
685        out.skip = out.units.is_empty() || (out.units.len() == 1 && plan.fixed.is_empty());
686        out
687    }
688    fn unpack(&self, input: &BaInput<'_>, x: &[f64]) -> Vec<BaRow> {
689        let mut out = self.constants.clone();
690        let mut k = 0;
691        for u in &self.units {
692            let gain = if u.gain_var {
693                let v = x[k];
694                k += 1;
695                v
696            } else {
697                0.0
698            };
699            let d = x[k];
700            let pol = if x[k + 1] < 0.5 { 1 } else { -1 };
701            k += 2;
702            let mut aps = vec![];
703            if u.ap_var {
704                if input.tuning.sub_allpass != SubAllpass::None
705                    && (u.auto || (x[k] > 0.0 && x[k + 2] > 0.0))
706                {
707                    aps.extend([(x[k], x[k + 1]), (x[k + 2], x[k + 3])]);
708                }
709                k += 4;
710            }
711            for (r, v) in &u.roles {
712                let mut a = v.allpass.clone();
713                a.extend_from_slice(&aps);
714                out.push((
715                    r.clone(),
716                    row(
717                        v.gain_db + gain,
718                        u.center + v.delay_ms + d,
719                        v.polarity * pol,
720                        a,
721                    ),
722                ));
723            }
724        }
725        input
726            .config
727            .subs
728            .iter()
729            .map(|r| out.iter().find(|(s, _)| s == r).unwrap().1.clone())
730            .collect()
731    }
732}
733
734fn level(input: &BaInput<'_>, by_pos: &[(u32, Arc<[C]>)]) -> f64 {
735    let h = &by_pos
736        .iter()
737        .find(|(p, _)| *p == input.config.main_position)
738        .unwrap_or(&by_pos[0])
739        .1;
740    let indices: Vec<_> = design_freqs()
741        .iter()
742        .enumerate()
743        .filter(|(_, f)| **f >= 30.0 && **f <= 80.0)
744        .map(|(i, _)| i)
745        .collect();
746    let target = input
747        .config
748        .target
749        .sample(&gather(design_freqs(), &indices));
750    median(
751        indices
752            .iter()
753            .zip(target)
754            .map(|(&i, t)| 20.0 * (mag(h[i]) + 1e-20).log10() - t)
755            .collect(),
756    )
757}
758#[allow(clippy::too_many_arguments)]
759fn target_peq(
760    input: &BaInput<'_>,
761    by_pos: &[(u32, Arc<[C]>)],
762    lvl: f64,
763    hi: f64,
764    bands: usize,
765    boost: f64,
766    fmax: Option<f64>,
767    fitter: &mut Fitter<'_>,
768) -> Vec<Peq> {
769    let by_pos: Vec<_> = by_pos
770        .iter()
771        .map(|(p, h)| (weight(*p, input.config.main_position), h.as_ref()))
772        .collect();
773    xo_target_peq(
774        &by_pos,
775        &input.config.target,
776        lvl,
777        20.0,
778        hi,
779        bands,
780        boost,
781        fmax,
782        fitter,
783    )
784}
785fn bus_fit(input: &BaInput<'_>, by_pos: &[(u32, Arc<[C]>)], fitter: &mut Fitter<'_>) -> Vec<Peq> {
786    if !input.tuning.xo_stage || input.xo_of.is_empty() {
787        return vec![];
788    }
789    let xo: Vec<_> = input.xo_of.iter().map(|(_, v)| *v).collect();
790    let hi = if input.tuning.sub_bus_fix {
791        1.25 * xo.iter().copied().fold(f64::NEG_INFINITY, f64::max)
792    } else {
793        2.0 * mean(&xo)
794    };
795    target_peq(
796        input,
797        by_pos,
798        input.ref_level,
799        hi,
800        10,
801        6.0_f64.min(input.config.max_boost_db),
802        input.tuning.sub_bus_fix.then_some(hi),
803        fitter,
804    )
805}
806fn bus_response(
807    input: &BaInput<'_>,
808    h: &Spectra,
809    alignment: &SubAlignment,
810    rows: &[BaRow],
811    fitter: &mut Fitter<'_>,
812) -> Result<Option<Vec<C>>, EngineError> {
813    check(input)?;
814    if !input.tuning.xo_stage || input.xo_of.is_empty() {
815        return Ok(None);
816    }
817    let mut by_pos = sums(
818        h,
819        &input.config.subs,
820        rows,
821        &(0..BINS).collect::<Vec<_>>(),
822        &complete(h, &input.config.subs),
823    );
824    if !by_pos.iter().any(|(p, _)| *p == input.config.main_position) {
825        return Ok(None);
826    }
827    if !alignment.plan.as_ref().is_some_and(|p| p.gain_anchored) {
828        let gain = 10f64.powf((input.ref_level - level(input, &by_pos)) / 20.0);
829        for (_, h) in &mut by_pos {
830            *h = h.iter().map(|v| *v * C::new(gain, 0.0)).collect();
831        }
832    }
833    let peq = bus_fit(input, &by_pos, fitter);
834    check(input)?;
835    Ok((!peq.is_empty()).then(|| response::peq_response(design_freqs(), &peq)))
836}
837
838// The layout closure is kept separate from BaLateGrid: it is its own upstream
839// definition, even though its current window endpoints happen to be identical.
840fn layout_late(spec: &[C], band: &[f64], fs: f64) -> f64 {
841    let y = fft::irfft(
842        &spec
843            .iter()
844            .zip(band)
845            .map(|(s, b)| s * b)
846            .collect::<Vec<_>>(),
847        2 * (spec.len() - 1),
848    );
849    let top = y.iter().map(|v| v.abs()).fold(0.0, f64::max);
850    let k = y.iter().position(|v| v.abs() > 0.1 * top).unwrap_or(0);
851    let energy = |a: f64, b: f64| {
852        pairwise_sum(
853            &y[(k + (a * fs) as usize).min(y.len())..(k + (b * fs) as usize).min(y.len())]
854                .iter()
855                .map(|v| v * v)
856                .collect::<Vec<_>>(),
857        )
858    };
859    10.0 * (energy(0.1, 0.38) / (energy(0.0, 0.05) + 1e-30) + 1e-30).log10()
860}
861fn layout_stage(
862    input: &BaInput<'_>,
863    h: &mut Spectra,
864    out: &mut SubAlignment,
865    hi: f64,
866    fitter: &mut Fitter<'_>,
867) -> Result<(), EngineError> {
868    let f = design_freqs();
869    let grid = grid(hi);
870    let fg = gather(f, &grid);
871    let quadratic = Quadratic::new(fg.iter().map(|f| f.ln()).collect());
872    let lin = f.partition_point(|f| *f < 200.0);
873    let li: Vec<_> = (0..lin).collect();
874    let band: Vec<_> = f[..lin]
875        .iter()
876        .map(|f| ((f - 15.0) / 5.0).clamp(0.0, 1.0) * ((hi * 1.3 - f) / (hi * 0.3)).clamp(0.0, 1.0))
877        .collect();
878    let mask: Vec<_> = f
879        .iter()
880        .enumerate()
881        .filter(|(_, f)| **f >= 20.0 && **f <= hi)
882        .map(|(i, _)| i)
883        .collect();
884    for (name, lay) in &input.setup.layouts {
885        check(input)?;
886        let positions = complete(h, &lay.members);
887        if positions.is_empty() {
888            return Err(EngineError::Input(format!(
889                "bundle {name}: no complete position"
890            )));
891        }
892        let levels: Vec<_> = lay
893            .members
894            .iter()
895            .map(|r| {
896                let mut pw = vec![0.0; mask.len()];
897                let mut total = 0.0;
898                for &p in h.positions() {
899                    if let Some(h) = h.get(p, r) {
900                        let w = weight(p, input.config.main_position);
901                        total += w;
902                        for (v, &i) in pw.iter_mut().zip(&mask) {
903                            *v += w * power(h[i]);
904                        }
905                    }
906                }
907                median(
908                    pw.iter()
909                        .map(|v| 10.0 * (v / total + 1e-30).log10())
910                        .collect(),
911                )
912            })
913            .collect();
914        let low = levels.iter().copied().fold(f64::INFINITY, f64::min);
915        let gains: Vec<_> = lay
916            .members
917            .iter()
918            .cloned()
919            .zip(levels.iter().map(|v| low - v))
920            .collect();
921        let rear: Vec<_> = lay
922            .members
923            .iter()
924            .filter(|r| {
925                input
926                    .setup
927                    .sub_manual
928                    .iter()
929                    .any(|(s, m)| s == *r && m.dba_row == DbaRow::Rear)
930            })
931            .cloned()
932            .collect();
933        let front: Vec<_> = lay
934            .members
935            .iter()
936            .filter(|r| !rear.contains(r))
937            .cloned()
938            .collect();
939        let mut info = LayoutInfo {
940            kind: lay.kind,
941            members: lay.members.clone(),
942            gains: gains.clone(),
943            front: front.clone(),
944            rear: vec![],
945            rear_ms: 0.0,
946            auto_ms: None,
947            trim_ms: 0.0,
948            min_ms: None,
949            peq: vec![],
950            #[cfg(test)]
951            candidates: vec![],
952        };
953        let mut rows: Vec<_> = gains.iter().map(|(_, g)| row(*g, 0.0, 1, vec![])).collect();
954        if lay.kind == LayoutKind::Dba {
955            if front.is_empty() || rear.is_empty() {
956                info.kind = LayoutKind::Multi;
957                out.notes.push(Note::LayoutFallback(name.clone()));
958            } else {
959                let arrival = |p, r: &str| -> Result<f64, EngineError> {
960                    let m = input
961                        .measurements
962                        .get(p, r)
963                        .ok_or_else(|| EngineError::Input(format!("missing DBA IR {p}/{r}")))?;
964                    Ok(1000.0 * (m.t0 + onset_index(&m.samples) as f64 / SR_F))
965                };
966                let mut gap = 0.0_f64;
967                for &p in &positions {
968                    let r = rear
969                        .iter()
970                        .map(|r| arrival(p, r))
971                        .collect::<Result<Vec<_>, _>>()?;
972                    let fr = front
973                        .iter()
974                        .map(|r| arrival(p, r))
975                        .collect::<Result<Vec<_>, _>>()?;
976                    gap = gap.max((mean(&r) - mean(&fr)).abs());
977                }
978                let lo = (gap.min(59.0) * 4.0).ceil() / 4.0;
979                let mut best = (f64::INFINITY, 0.0);
980                for k in 0..=((60.0 - lo) * 4.0) as usize {
981                    check(input)?;
982                    let delay = lo + k as f64 * 0.25;
983                    let trial: Vec<_> = gains
984                        .iter()
985                        .map(|(r, g)| {
986                            row(
987                                *g,
988                                if rear.contains(r) { delay } else { 0.0 },
989                                if rear.contains(r) { -1 } else { 1 },
990                                vec![],
991                            )
992                        })
993                        .collect();
994                    let gs = sums(h, &lay.members, &trial, &grid, &positions);
995                    let ls = sums(h, &lay.members, &trial, &li, &positions);
996                    let mut cost = 0.0;
997                    let mut wsum = 0.0;
998                    let mut dbs = vec![];
999                    for ((p, g), (_, l)) in gs.iter().zip(&ls) {
1000                        let w = weight(*p, input.config.main_position);
1001                        wsum += w;
1002                        let db: Vec<_> =
1003                            g.iter().map(|v| 20.0 * (mag(*v) + 1e-20).log10()).collect();
1004                        cost += w * quadratic.rough(&db);
1005                        cost += w * 0.2 * layout_late(l, &band, 2.0 * f[lin - 1]);
1006                        dbs.push(db);
1007                    }
1008                    cost = cost / wsum + 0.5 * variance(&dbs);
1009                    #[cfg(test)]
1010                    info.candidates.push((delay, cost));
1011                    if cost < best.0 {
1012                        best = (cost, delay);
1013                    }
1014                }
1015                let delay = (best.1 + lay.rear_trim_ms).max(0.0);
1016                for ((r, _), v) in gains.iter().zip(&mut rows) {
1017                    if rear.contains(r) {
1018                        v.delay_ms = delay;
1019                        v.polarity = -1;
1020                    }
1021                }
1022                info.rear = rear;
1023                info.rear_ms = delay;
1024                info.auto_ms = Some(best.1);
1025                info.trim_ms = lay.rear_trim_ms;
1026                info.min_ms = Some(gap);
1027            }
1028        }
1029        let by_pos = sums(
1030            h,
1031            &lay.members,
1032            &rows,
1033            &(0..BINS).collect::<Vec<_>>(),
1034            &positions,
1035        );
1036        let peq = target_peq(
1037            input,
1038            &by_pos,
1039            level(input, &by_pos),
1040            hi,
1041            6,
1042            6.0,
1043            None,
1044            fitter,
1045        );
1046        check(input)?;
1047        if !peq.is_empty() {
1048            let response = response::peq_response(f, &peq);
1049            for r in &lay.members {
1050                if !out.sub_filters.iter().any(|(s, _)| s == r) {
1051                    out.sub_filters.push((
1052                        r.clone(),
1053                        SubFilter {
1054                            hp_hz: 0.0,
1055                            hp_type: BandType::Lr24,
1056                            lp_hz: 0.0,
1057                            lp_type: BandType::Lr24,
1058                            peq: vec![],
1059                        },
1060                    ));
1061                }
1062                out.sub_filters
1063                    .iter_mut()
1064                    .find(|(s, _)| s == r)
1065                    .unwrap()
1066                    .1
1067                    .peq
1068                    .extend_from_slice(&peq);
1069                for p in h.positions().to_vec() {
1070                    if let Some(old) = h.get(p, r) {
1071                        let new: Arc<[C]> = old.iter().zip(&response).map(|(a, b)| a * b).collect();
1072                        h.set(p, r, new.clone());
1073                        out.h_subs.push((p, r.clone(), new));
1074                    }
1075                }
1076            }
1077        }
1078        info.peq = peq;
1079        out.layout_rel.push((
1080            name.clone(),
1081            lay.members.iter().cloned().zip(rows).collect(),
1082        ));
1083        out.layout_info.push((name.clone(), info));
1084        out.notes.push(Note::Layout(name.clone()));
1085    }
1086    Ok(())
1087}
1088
1089fn two_way(input: &BaInput<'_>, h: &Spectra, out: &mut SubAlignment) -> Result<usize, EngineError> {
1090    let filters = &out.sub_filters;
1091    let low: Vec<_> = input
1092        .config
1093        .subs
1094        .iter()
1095        .filter(|r| {
1096            filters
1097                .iter()
1098                .any(|(s, f)| s == *r && f.lp_hz > 0.0 && f.hp_hz == 0.0)
1099        })
1100        .cloned()
1101        .collect();
1102    let high: Vec<_> = input
1103        .config
1104        .subs
1105        .iter()
1106        .filter(|r| !low.contains(r))
1107        .collect();
1108    if low.is_empty() || high.is_empty() {
1109        return Ok(0);
1110    }
1111    let xo = filters.iter().find(|(r, _)| r == &low[0]).unwrap().1.lp_hz;
1112    if high.iter().any(|r| {
1113        (filters
1114            .iter()
1115            .find(|(s, _)| s == *r)
1116            .map_or(0.0, |(_, f)| f.hp_hz)
1117            - xo)
1118            .abs()
1119            > 0.5
1120    }) {
1121        return Ok(0);
1122    }
1123    let movable: Vec<_> = low
1124        .into_iter()
1125        .filter(|r| !out.plan.as_ref().is_some_and(|p| p.fixed.contains(r)))
1126        .collect();
1127    if movable.is_empty() {
1128        return Ok(0);
1129    }
1130    let f = design_freqs();
1131    let lo: Vec<_> = f
1132        .iter()
1133        .enumerate()
1134        .filter(|(_, f)| **f >= 12.0_f64.max(xo * 0.55) && **f <= xo * 0.85)
1135        .map(|(i, _)| i)
1136        .collect();
1137    let hi: Vec<_> = f
1138        .iter()
1139        .enumerate()
1140        .filter(|(_, f)| **f >= xo * 1.3 && **f <= 80.0_f64.min(xo * 3.2))
1141        .map(|(i, _)| i)
1142        .collect();
1143    if lo.is_empty() || hi.is_empty() {
1144        return Ok(0);
1145    }
1146    let target = input
1147        .config
1148        .target
1149        .sample(&f.iter().map(|f| f.max(1.0)).collect::<Vec<_>>());
1150    let mut total = 0.0;
1151    let mut iterations = 0;
1152    for _ in 0..4 {
1153        iterations += 1;
1154        check(input)?;
1155        let sum = sums(
1156            h,
1157            &input.config.subs,
1158            &out.rows,
1159            &(0..BINS).collect::<Vec<_>>(),
1160            &[input.config.main_position],
1161        );
1162        let p: Vec<_> = sum[0].1.iter().map(|h| power(*h)).collect();
1163        let sm = smooth::smooth_power(f, &p, Fraction::Scalar(3.0))?;
1164        let db: Vec<_> = sm
1165            .iter()
1166            .zip(&target)
1167            .map(|(p, t)| 10.0 * (p + 1e-30).log10() - t)
1168            .collect();
1169        let diff = median(gather(&db, &hi)) - median(gather(&db, &lo));
1170        let step = diff.clamp(-24.0 - total, input.config.infra_boost_max_db - total);
1171        if step.abs() < 0.1 {
1172            break;
1173        }
1174        for (r, v) in input.config.subs.iter().zip(&mut out.rows) {
1175            if movable.contains(r) {
1176                v.gain_db += step;
1177            }
1178        }
1179        total += step;
1180    }
1181    if total.abs() >= 0.1 {
1182        out.notes.push(Note::TwoWay {
1183            roles: movable,
1184            gain_db: total,
1185            xo_hz: xo,
1186        });
1187    }
1188    Ok(iterations)
1189}
1190
1191type DeSolver<'a> = dyn FnMut(
1192        &mut dyn FnMut(&[f64]) -> f64,
1193        &[(f64, f64)],
1194        Option<&[f64]>,
1195        DifferentialEvolutionOptions,
1196    ) -> Result<secs_dsp::optimize::DifferentialEvolutionResult, secs_dsp::DspError>
1197    + 'a;
1198
1199pub fn align(input: &BaInput<'_>, fitter: &mut Fitter<'_>) -> Result<SubAlignment, EngineError> {
1200    align_with(input, fitter, &mut |cost, bounds, x0, options| {
1201        differential_evolution(cost, bounds, x0, options)
1202    })
1203}
1204
1205fn align_with(
1206    input: &BaInput<'_>,
1207    fitter: &mut Fitter<'_>,
1208    solver: &mut DeSolver<'_>,
1209) -> Result<SubAlignment, EngineError> {
1210    check(input)?;
1211    let mut out = SubAlignment {
1212        rows: vec![],
1213        sub_filters: input.setup.sub_filters.clone(),
1214        h_subs: vec![],
1215        layout_rel: vec![],
1216        layout_info: vec![],
1217        sub_level_db: None,
1218        bus_gain: 1.0,
1219        bus_peq: vec![],
1220        sub_sum_by_pos: vec![],
1221        plan: manual_plan(input),
1222        notes: vec![],
1223    };
1224    if input.config.subs.is_empty() {
1225        return Ok(out);
1226    }
1227    let hi = input
1228        .xo_of
1229        .iter()
1230        .map(|(_, x)| *x)
1231        .reduce(f64::max)
1232        .unwrap_or(100.0)
1233        * 1.5;
1234    let mut h = input.prepared.h.clone();
1235    layout_stage(input, &mut h, &mut out, hi, fitter)?;
1236    let search = Search::new(input, &out);
1237    if out.plan.is_some() {
1238        out.notes.push(Note::BaManual);
1239    }
1240    if out.plan.is_none() && input.config.subs.len() == 1 {
1241        out.rows = vec![row(0.0, base(input, &input.config.subs[0]), 1, vec![])];
1242    } else if search.skip {
1243        out.rows = search.unpack(input, &vec![0.0; search.bounds.len()]);
1244    } else {
1245        let s = BaSetup::new(input, &h, hi);
1246        if s.positions.is_empty() {
1247            return Err(EngineError::Input("BA has no complete position".into()));
1248        }
1249        let indiv = search.fixed_indiv.then(|| individual_db(&s.indiv(None)));
1250        let res = solver(
1251            &mut |x| s.cost_with_db(input, &search.unpack(input, x), indiv.as_deref(), None),
1252            &search.bounds,
1253            None,
1254            DifferentialEvolutionOptions::default(),
1255        )?;
1256        check(input)?;
1257        out.notes.push(Note::BaCost(res.fun));
1258        let mut x = res.x;
1259        if input.tuning.sub_bus_fix
1260            && let Some(p1) = bus_response(input, &h, &out, &search.unpack(input, &x), fitter)?
1261        {
1262            let x0: Vec<_> = x
1263                .iter()
1264                .zip(&search.bounds)
1265                .map(|(x, (a, b))| x.clamp(*a, *b))
1266                .collect();
1267            let res = solver(
1268                &mut |x| {
1269                    s.cost_with_db(input, &search.unpack(input, x), indiv.as_deref(), Some(&p1))
1270                },
1271                &search.bounds,
1272                Some(&x0),
1273                DifferentialEvolutionOptions {
1274                    maxiter: 40,
1275                    seed: 2,
1276                    ..Default::default()
1277                },
1278            )?;
1279            check(input)?;
1280            let b1 = bus_response(input, &h, &out, &search.unpack(input, &x), fitter)?;
1281            let c1 = s.cost_with_db(
1282                input,
1283                &search.unpack(input, &x),
1284                indiv.as_deref(),
1285                b1.as_deref(),
1286            );
1287            let b2 = bus_response(input, &h, &out, &search.unpack(input, &res.x), fitter)?;
1288            let c2 = s.cost_with_db(
1289                input,
1290                &search.unpack(input, &res.x),
1291                indiv.as_deref(),
1292                b2.as_deref(),
1293            );
1294            out.notes.push(Note::BaBusPass {
1295                first: c1,
1296                second: c2,
1297                kept_first: c2 >= c1,
1298            });
1299            if c2 < c1 {
1300                x = res.x;
1301            }
1302        }
1303        out.rows = search.unpack(input, &x);
1304    }
1305    two_way(input, &h, &mut out)?;
1306    check(input)?;
1307    out.sub_sum_by_pos = sums(
1308        &h,
1309        &input.config.subs,
1310        &out.rows,
1311        &(0..BINS).collect::<Vec<_>>(),
1312        &complete(&h, &input.config.subs),
1313    );
1314    if !out
1315        .sub_sum_by_pos
1316        .iter()
1317        .any(|(p, _)| *p == input.config.main_position)
1318    {
1319        return Err(EngineError::Input("missing BA reference position".into()));
1320    }
1321    let lvl = level(input, &out.sub_sum_by_pos);
1322    out.sub_level_db = Some(lvl - input.ref_level);
1323    if out.plan.as_ref().is_some_and(|p| p.gain_anchored) {
1324        out.notes.push(Note::BusGainAnchored(lvl - input.ref_level));
1325    } else {
1326        out.bus_gain = 10f64.powf((input.ref_level - lvl) / 20.0);
1327    }
1328    for (_, h) in &mut out.sub_sum_by_pos {
1329        *h = h.iter().map(|v| *v * C::new(out.bus_gain, 0.0)).collect();
1330    }
1331    out.bus_peq = bus_fit(input, &out.sub_sum_by_pos, fitter);
1332    check(input)?;
1333    if input.tuning.xo_stage && !input.xo_of.is_empty() {
1334        let p = response::peq_response(design_freqs(), &out.bus_peq);
1335        for (_, h) in &mut out.sub_sum_by_pos {
1336            *h = h.iter().zip(&p).map(|(v, p)| v * p).collect();
1337        }
1338    }
1339    Ok(out)
1340}
1341
1342/// Main spectra include acoustic-XO PEQ, but not their electrical HP or delay.
1343/// Returns `Err(Cancelled)` once `cancelled()` reads true; it is polled once
1344/// per main position while the bands are gathered and once per candidate
1345/// delay, which leaves the search's arithmetic and order unchanged (TD-49).
1346#[allow(clippy::type_complexity)]
1347pub fn bus_delay(
1348    alignment: &SubAlignment,
1349    mains: &[(String, Vec<(u32, Arc<[C]>)>)],
1350    align_delay_ms: &[(String, f64)],
1351    xo_of: &[(String, f64)],
1352    lo_ms: f64,
1353    cancelled: &dyn Fn() -> bool,
1354) -> Result<(f64, i8), EngineError> {
1355    bus_delay_observe(
1356        alignment,
1357        mains,
1358        align_delay_ms,
1359        xo_of,
1360        lo_ms,
1361        cancelled,
1362        &mut |_, _, _| {},
1363    )
1364}
1365
1366#[allow(clippy::type_complexity)]
1367fn bus_delay_observe(
1368    alignment: &SubAlignment,
1369    mains: &[(String, Vec<(u32, Arc<[C]>)>)],
1370    align_delay_ms: &[(String, f64)],
1371    xo_of: &[(String, f64)],
1372    lo_ms: f64,
1373    cancelled: &dyn Fn() -> bool,
1374    observe: &mut dyn FnMut(i8, f64, f64),
1375) -> Result<(f64, i8), EngineError> {
1376    let check = || {
1377        if cancelled() {
1378            Err(EngineError::Cancelled)
1379        } else {
1380            Ok(())
1381        }
1382    };
1383    if alignment.plan.as_ref().is_some_and(|p| !p.fixed.is_empty()) {
1384        return Ok((0.0, 1));
1385    }
1386    let mut prepared = vec![];
1387    for (r, positions) in mains {
1388        let Some(&(_, xo)) = xo_of.iter().find(|(s, x)| s == r && *x != 0.0) else {
1389            continue;
1390        };
1391        let idx: Vec<_> = design_freqs()
1392            .iter()
1393            .enumerate()
1394            .filter(|(_, f)| **f >= xo / 2.0 && **f <= xo * 2.0)
1395            .map(|(i, _)| i)
1396            .collect();
1397        let f = gather(design_freqs(), &idx);
1398        let d = align_delay_ms.iter().find(|(s, _)| s == r).unwrap().1;
1399        let hp = response::lr4(&f, xo, "HP");
1400        let lp = response::lr4(&f, xo, "LP");
1401        for (pos, h) in positions {
1402            check()?;
1403            let sub = &alignment
1404                .sub_sum_by_pos
1405                .iter()
1406                .find(|(p, _)| p == pos)
1407                .expect("complete sub position")
1408                .1;
1409            let main: Vec<_> = idx
1410                .iter()
1411                .enumerate()
1412                .map(|(k, &i)| h[i] * C::from_polar(1.0, ramp_phase(f[k], d) * 0.001) * hp[k])
1413                .collect();
1414            let sub: Vec<_> = idx
1415                .iter()
1416                .enumerate()
1417                .map(|(k, &i)| sub[i] * lp[k])
1418                .collect();
1419            prepared.push((f.clone(), main, sub));
1420        }
1421    }
1422    if prepared.is_empty() {
1423        return Ok((0.0, 1));
1424    }
1425    let mut best = (1e18, 0.0, 1);
1426    for pol in [1, -1] {
1427        let mut k = 0;
1428        while lo_ms + k as f64 * 0.25 < 15.01 {
1429            check()?;
1430            let d = lo_ms + k as f64 * 0.25;
1431            k += 1;
1432            let mut cost = 0.0;
1433            for (f, main, sub) in &prepared {
1434                let errors: Vec<_> = f
1435                    .iter()
1436                    .zip(main)
1437                    .zip(sub)
1438                    .map(|((&f, &m), &s)| {
1439                        let s = s
1440                            * C::new(f64::from(pol), 0.0)
1441                            * C::from_polar(1.0, ramp_phase(f, d) * 0.001);
1442                        let total = power(m + s);
1443                        let reference = power(m) + power(s);
1444                        (10.0 * (reference + 1e-30).log10() - 10.0 * (total + 1e-30).log10())
1445                            .powi(2)
1446                    })
1447                    .collect();
1448                cost += mean(&errors);
1449            }
1450            observe(pol, d, cost);
1451            if cost < best.0 {
1452                best = (cost, d, pol);
1453            }
1454        }
1455    }
1456    Ok((best.1, best.2))
1457}
1458pub fn fold(rows: &[BaRow], bus_gain: f64, bus_delay_ms: f64, bus_polarity: i8) -> Vec<BaRow> {
1459    rows.iter()
1460        .map(|r| {
1461            row(
1462                r.gain_db + 20.0 * bus_gain.log10(),
1463                r.delay_ms + bus_delay_ms,
1464                r.polarity * bus_polarity,
1465                r.allpass.clone(),
1466            )
1467        })
1468        .collect()
1469}
1470pub fn sub_mode_label(role: &str, setup: &Setup, layout_info: &[(String, LayoutInfo)]) -> String {
1471    let Some((_, m)) = setup.sub_manual.iter().find(|(r, _)| r == role) else {
1472        return "auto".into();
1473    };
1474    match m.mode {
1475        SubMode::Auto => "auto".into(),
1476        SubMode::Fixed => "fixed".into(),
1477        SubMode::Bundle => {
1478            let name = if m.bundle.is_empty() { "A" } else { &m.bundle };
1479            if let Some((_, info)) = layout_info.iter().find(|(n, _)| n == name) {
1480                let kind = match info.kind {
1481                    LayoutKind::Multi => "multi",
1482                    LayoutKind::Dba => {
1483                        if info.rear.iter().any(|r| r == role) {
1484                            "dba-rear"
1485                        } else {
1486                            "dba-front"
1487                        }
1488                    }
1489                };
1490                format!("bundle:{name}:{kind}")
1491            } else {
1492                format!("bundle:{name}")
1493            }
1494        }
1495    }
1496}
1497
1498#[cfg(test)]
1499#[path = "tests/ba.rs"]
1500mod tests;