1use 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
167struct 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 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 #[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
838fn 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#[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;