1use std::sync::Arc;
6
7use secs_compute::{CancelToken, Context};
8use secs_dsp::{
9 interp, response,
10 smooth::{self, Fraction},
11};
12use secs_types::{
13 calibration::RcHiBasis,
14 chain::{BandType, MainChannel, SignalChain, SubChannel},
15 target::TargetCurve,
16};
17
18use crate::{
19 C, EngineConfig, EngineError, MeasurementSet, Tuning,
20 ba::{self, BaInput, LayoutInfo, ManualPlan},
21 chain::BaRow,
22 fit::{self, Fitter, PeqDerivatives},
23 grid::{N_FFT, SR, design_freqs},
24 levels,
25 measurement::{self, Spectra},
26 notes::Note,
27 rc::{self, RcInput},
28 scorecard::{self, Metrics},
29 setup::Setup,
30};
31
32#[derive(Debug)]
33pub enum FitStage<'a> {
34 Ba,
35 Top(&'a str),
36 Rc(&'a str),
37}
38pub type FitterFactory<'f> = dyn Fn(FitStage<'_>) -> Box<Fitter<'f>> + Sync + 'f;
39pub fn production_fitters(cancel: &CancelToken) -> Box<FitterFactory<'_>> {
40 production_fitters_with(cancel, PeqDerivatives::TwoPoint)
41}
42
43pub fn production_fitters_with(
44 cancel: &CancelToken,
45 derivatives: PeqDerivatives,
46) -> Box<FitterFactory<'_>> {
47 Box::new(move |_| {
48 Box::new(move |request| {
49 if cancel.is_cancelled() {
50 Vec::new()
51 } else {
52 fit::fit_peq_with(request, &|| cancel.is_cancelled(), derivatives)
53 }
54 })
55 })
56}
57
58#[derive(Debug)]
59pub struct DesignRequest<'a> {
60 pub measurements: &'a MeasurementSet,
61 pub config: &'a EngineConfig,
62 pub tuning: &'a Tuning,
63 pub subs_only: bool,
64 pub adapter_notes: &'a [String],
65}
66#[derive(Debug)]
67pub struct Design {
68 pub replay: Arc<crate::DesignReplay>,
69 pub chain: SignalChain,
70 pub mains: Vec<MainDesign>,
71 pub subs: Option<SubDesign>,
72 pub ref_level: f64,
73 pub rc_hi: f64,
74 pub rc_bands: Vec<(f64, f64)>,
76 pub rc_auto: bool,
77 pub schroeder_hz: Option<f64>,
78 pub target_db: Arc<[f64]>,
79 pub target_display: Vec<f64>,
80 pub spectra: DesignSpectra,
81 pub notes: Vec<Note>,
82 pub adapter_notes: Vec<String>,
83 pub scores: Vec<RoleScores>,
84 pub groups: Vec<(String, Vec<String>)>,
86 pub target: TargetCurve,
88 pub xo: Vec<(String, f64)>,
90 #[cfg(test)]
91 pub timings: Vec<(String, std::time::Duration)>,
92}
93#[derive(Debug)]
94pub struct DesignSpectra {
95 pub raw: Arc<Spectra>,
96 pub used: Arc<Spectra>,
97 pub main_position: u32,
98 pub positions: Vec<u32>,
99}
100#[derive(Debug)]
101pub struct MainDesign {
102 pub role: String,
103 pub gain_db: f64,
104 pub xo_hz: Option<f64>,
105 pub align_delay_ms: f64,
106 pub top_response: Option<Arc<[C]>>,
107 pub response: Arc<[C]>,
108 pub phase_strength: f64,
109 pub phase_band: Option<f64>,
110 pub phase_frac_used: Option<f64>,
111 pub before: Arc<[C]>,
112 pub combined: Arc<[C]>,
113 pub after: Arc<[C]>,
114}
115#[derive(Debug)]
116pub struct SubDesign {
117 pub rows: Vec<(String, BaRow)>,
118 pub bus_gain: f64,
119 pub bus_delay_ms: f64,
120 pub bus_polarity: i8,
121 pub bus_response: Option<Arc<[C]>>,
122 pub sum_at_reference: Arc<[C]>,
123 pub sub_level_db: Option<f64>,
124 pub layout_info: Vec<(String, LayoutInfo)>,
125 pub plan: Option<ManualPlan>,
126}
127#[derive(Debug)]
128pub struct RoleScores {
129 pub role: String,
130 pub before: Metrics,
131 pub after: Metrics,
132 pub spl_before: Vec<f64>,
133 pub spl_after: Vec<f64>,
134 pub gd_before: Vec<f64>,
135 pub gd_after: Vec<f64>,
136}
137
138#[derive(Debug, Clone, PartialEq)]
139pub struct RoleLateEnergy {
140 pub role: String,
141 pub position: u32,
142 pub before_db: f64,
143 pub after_db: f64,
144}
145
146pub fn late_energy(design: &Design) -> Result<Vec<RoleLateEnergy>, EngineError> {
152 let view = design.replay.export_view(design)?;
153 let mut rows = Vec::new();
154 for &position in &design.spectra.positions {
155 for channel in &design.chain.mains {
156 let (before_db, after_db) = if position == design.spectra.main_position {
157 let Some(main) = design.mains.iter().find(|main| main.role == channel.role) else {
158 continue;
159 };
160 (
161 crate::decay::late_db_20_150(&main.before),
162 crate::decay::late_db_20_150(&main.after),
163 )
164 } else {
165 let Some(before) = view.spectra.get(position, &channel.role) else {
166 continue;
167 };
168 let Some(after) =
169 crate::stage_responses(design, &channel.role, &view.spectra, position)
170 .map(|stages| stages.output)
171 else {
172 continue;
173 };
174 (
175 crate::decay::late_db_20_150(before),
176 crate::decay::late_db_20_150(&after),
177 )
178 };
179 rows.push(RoleLateEnergy {
180 role: channel.role.clone(),
181 position,
182 before_db,
183 after_db,
184 });
185 }
186 }
187 Ok(rows)
188}
189
190fn check(ctx: &Context) -> Result<(), EngineError> {
191 ctx.cancel().check().map_err(|_| EngineError::Cancelled)
192}
193
194pub(crate) fn check_taps(config: &EngineConfig) -> Result<(), EngineError> {
198 if config.taps > N_FFT {
199 return Err(EngineError::Input(format!(
200 "FIR taps {} outside 0..={N_FFT} (0 is PEQ only)",
201 config.taps
202 )));
203 }
204 Ok(())
205}
206
207const RC_SCRATCH_SPECTRA: u64 = 24;
216const RC_ADR0025_SPECTRA: u64 = 6;
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub(crate) struct DesignBytes {
227 pub retained: u64,
228 pub channel: u64,
229}
230
231impl DesignBytes {
232 pub(crate) fn new(
246 set: &MeasurementSet,
247 config: &EngineConfig,
248 setup: &Setup,
249 subs_only: bool,
250 ) -> Self {
251 let positions = set.positions.len() as u64;
252 let subs = config.subs.len() as u64;
253 let mains = if subs_only
254 && !(config.rc_range.1.is_none() && config.rc_hi_basis == RcHiBasis::Agreement)
255 {
256 0
257 } else {
258 config.mains.len() as u64
259 };
260 let ba = if config.subs.is_empty() {
261 0
262 } else {
263 measurement::cells(set, Some(&config.subs))
264 .saturating_mul(setup.layouts.len() as u64)
265 .saturating_add(measurement::cells(set, Some(&config.mains)))
266 .saturating_add(2 * positions)
267 .saturating_add(subs + 2)
268 .saturating_add(config.mains.len() as u64)
269 };
270 let spectra = |n: u64| n.saturating_mul(measurement::SPECTRUM_BYTES);
271 Self {
272 retained: spectra(ba.saturating_add(3 * mains))
273 .saturating_add(crate::DesignReplay::bytes(set, config, setup)),
274 channel: if mains == 0 {
275 0
276 } else {
277 let extra = if setup.lf_align || setup.mode_decay {
278 RC_ADR0025_SPECTRA
279 } else {
280 0
281 };
282 spectra(positions + 4 + RC_SCRATCH_SPECTRA + extra)
283 },
284 }
285 }
286}
287pub(crate) fn geomspace(lo: f64, hi: f64, n: usize) -> Vec<f64> {
288 let start = lo.log10();
289 let step = (hi.log10() - start) / (n - 1) as f64;
290 let mut out: Vec<_> = (0..n)
291 .map(|i| 10f64.powf(start + i as f64 * step))
292 .collect();
293 out[0] = lo;
294 out[n - 1] = hi;
295 out
296}
297pub(crate) fn spl_db(h: &[C], grid: &[f64]) -> Vec<f64> {
298 let p: Vec<_> = h
299 .iter()
300 .map(|v| {
301 let a = v.re.hypot(v.im);
302 a * a
303 })
304 .collect();
305 let db: Vec<_> = smooth::smooth_power(design_freqs(), &p, Fraction::Scalar(24.0))
306 .expect("design grid")
307 .iter()
308 .map(|p| 10.0 * (p + 1e-30).log10())
309 .collect();
310 grid.iter()
311 .map(|&g| interp(g, design_freqs(), &db))
312 .collect()
313}
314
315pub fn design(
316 req: &DesignRequest<'_>,
317 ctx: &Context,
318 fitters: &FitterFactory<'_>,
319) -> Result<Design, EngineError> {
320 design_observed(req, ctx, fitters, &|_, _| {})
321}
322
323pub(crate) fn design_observed(
325 req: &DesignRequest<'_>,
326 ctx: &Context,
327 fitters: &FitterFactory<'_>,
328 observed: &(dyn Fn(&str, std::time::Duration) + Sync),
329) -> Result<Design, EngineError> {
330 check(ctx)?;
331 check_taps(req.config)?;
332 let prep_started = std::time::Instant::now();
333 let config = req.config;
334 let setup = Setup::new(config);
335 let bytes = DesignBytes::new(req.measurements, config, &setup, req.subs_only);
339 let held =
340 measurement::preparation_bytes(req.measurements, &setup).saturating_add(bytes.retained);
341 let _permit = crate::memory::reserve(ctx, held, bytes.channel)?;
342 let prepared = measurement::prepare_until(req.measurements, config, &setup, &|| {
343 ctx.cancel().is_cancelled()
344 })?;
345 observed("prepare", prep_started.elapsed());
346 design_prepared_scored(
347 req,
348 ctx,
349 fitters,
350 &prepared,
351 observed,
352 true,
353 ChannelBytes {
354 held,
355 channel: bytes.channel,
356 },
357 )
358}
359
360pub(crate) fn design_prepared(
366 req: &DesignRequest<'_>,
367 ctx: &Context,
368 fitters: &FitterFactory<'_>,
369 prepared: &measurement::Prepared,
370 observed: &(dyn Fn(&str, std::time::Duration) + Sync),
371) -> Result<Design, EngineError> {
372 let bytes = DesignBytes::new(
373 req.measurements,
374 req.config,
375 &Setup::new(req.config),
376 req.subs_only,
377 );
378 let _permit = crate::memory::reserve(ctx, bytes.retained, bytes.channel)?;
379 design_prepared_scored(
380 req,
381 ctx,
382 fitters,
383 prepared,
384 observed,
385 true,
386 ChannelBytes {
387 held: bytes.retained,
388 channel: bytes.channel,
389 },
390 )
391}
392
393pub(crate) fn design_prepared_for_xo(
398 req: &DesignRequest<'_>,
399 ctx: &Context,
400 fitters: &FitterFactory<'_>,
401 prepared: &measurement::Prepared,
402) -> Result<Design, EngineError> {
403 design_prepared_scored(
404 req,
405 ctx,
406 fitters,
407 prepared,
408 &|_, _| {},
409 false,
410 ChannelBytes::RESERVED,
411 )
412}
413
414#[derive(Debug, Clone, Copy)]
417struct ChannelBytes {
418 held: u64,
419 channel: u64,
420}
421
422impl ChannelBytes {
423 const RESERVED: Self = Self {
425 held: 0,
426 channel: 0,
427 };
428}
429
430fn design_prepared_scored(
431 req: &DesignRequest<'_>,
432 ctx: &Context,
433 fitters: &FitterFactory<'_>,
434 prepared: &measurement::Prepared,
435 observed: &(dyn Fn(&str, std::time::Duration) + Sync),
436 include_scores: bool,
437 reserve: ChannelBytes,
438) -> Result<Design, EngineError> {
439 if req.config.rc_range.1.is_none() && req.config.rc_hi_basis == RcHiBasis::Agreement {
440 let count = agreement_positions(req.measurements, req.config);
441 if count < 3 {
442 return Err(EngineError::Input(format!(
443 "RC agreement requires at least 3 measured positions; found {count}"
444 )));
445 }
446 let (probe, limits) = design_pass(
447 req,
448 ctx,
449 fitters,
450 prepared,
451 observed,
452 false,
453 reserve,
454 RcPass::Probe,
455 )?;
456 let limit = levels::median(limits).clamp(100.0, 2000.0);
457 drop(probe);
458 check(ctx)?;
459 return design_pass(
460 req,
461 ctx,
462 fitters,
463 prepared,
464 observed,
465 include_scores,
466 reserve,
467 RcPass::Final(limit),
468 )
469 .map(|(design, _)| design);
470 }
471 design_pass(
472 req,
473 ctx,
474 fitters,
475 prepared,
476 observed,
477 include_scores,
478 reserve,
479 RcPass::Normal,
480 )
481 .map(|(design, _)| design)
482}
483
484pub(crate) fn agreement_positions(set: &MeasurementSet, config: &EngineConfig) -> usize {
485 set.positions
486 .iter()
487 .filter(|(_, roles)| {
488 roles
489 .iter()
490 .any(|(role, m)| config.mains.contains(role) && !m.samples.is_empty())
491 })
492 .count()
493}
494
495#[derive(Clone, Copy)]
496enum RcPass {
497 Normal,
498 Probe,
499 Final(f64),
500}
501
502#[allow(clippy::too_many_arguments)]
503fn design_pass(
504 req: &DesignRequest<'_>,
505 ctx: &Context,
506 fitters: &FitterFactory<'_>,
507 prepared: &measurement::Prepared,
508 observed: &(dyn Fn(&str, std::time::Duration) + Sync),
509 include_scores: bool,
510 reserve: ChannelBytes,
511 pass: RcPass,
512) -> Result<(Design, Vec<f64>), EngineError> {
513 check(ctx)?;
514 check_taps(req.config)?;
515 let config = req.config;
516 let mut setup = Setup::new(config);
517 if matches!(pass, RcPass::Probe) {
518 setup.rc_bands.clear();
519 setup.lf_align = false;
520 setup.mode_decay = false;
521 }
522 let positions = prepared.h.positions().to_vec();
523 let progress = ctx.stage("engine.design", 1 + config.mains.len() as u64);
524 let mut notes = Vec::new();
525 if let Some(from_ms) = setup.lift_capped_from_ms {
526 notes.push(Note::LiftCapped {
527 from_ms,
528 taps: config.taps,
529 lift_ms: setup.lift_ms,
530 });
531 }
532 if !setup.sub_filters.is_empty() {
533 notes.push(Note::SubFilters(setup.sub_filters.clone()));
534 }
535 let rc_auto = setup.rc_hi_manual.is_none();
536 let schroeder_hz = rc_auto
537 .then(|| levels::schroeder_hz(req.measurements, config))
538 .flatten();
539 let lenient = (rc_auto && config.rc_hi_basis == RcHiBasis::Lenient)
540 .then(|| levels::lenient_schroeder_hz(req.measurements, config))
541 .flatten();
542 if rc_auto {
543 if !matches!(config.rc_hi_transition, 1 | 2 | 4) {
544 return Err(EngineError::Input(
545 "RC transition factor must be 1, 2 or 4".into(),
546 ));
547 }
548 setup.rc_hi = match pass {
549 RcPass::Probe => 2000.0,
550 RcPass::Final(hz) => hz,
551 RcPass::Normal => {
552 levels::schroeder_limit(schroeder_hz, lenient.map(|v| v.0), config.rc_hi_transition)
553 }
554 };
555 }
556 notes.push(Note::RcLimit {
557 hz: setup.rc_hi,
558 auto: rc_auto,
559 schroeder_hz,
560 basis: config.rc_hi_basis,
561 factor: config.rc_hi_transition,
562 band_hz: lenient.map(|v| v.0),
563 band_t60_s: lenient.map(|v| v.1),
564 positions: (rc_auto && config.rc_hi_basis == RcHiBasis::Agreement)
565 .then(|| agreement_positions(req.measurements, config)),
566 });
567 let rc_bands = if setup.rc_bands.is_empty() {
568 Vec::new()
569 } else {
570 let bands = rc::band_set(setup.rc_lo, setup.rc_hi, &setup.rc_bands);
571 notes.push(Note::RcBands {
572 bands: bands.clone(),
573 });
574 bands
575 };
576 let xo_of: Vec<_> = config
577 .groups
578 .iter()
579 .filter(|(g, _)| g != "sub")
580 .flat_map(|(g, roles)| {
581 roles.iter().filter_map(move |r| {
582 config
583 .xo
584 .iter()
585 .find(|(name, x)| name == g && *x != 0.0 && !config.subs.is_empty())
586 .map(|(_, x)| (r.clone(), *x))
587 })
588 })
589 .collect();
590 let weights = levels::weights(&positions, config.main_position);
591 let levels: Vec<_> = config
592 .mains
593 .iter()
594 .filter(|r| prepared.h.get(config.main_position, r).is_some())
595 .map(|r| {
596 (
597 r.clone(),
598 levels::band_level(&prepared.h, r, &weights, &config.target, 300.0, 3000.0),
599 )
600 })
601 .collect();
602 let ref_level = levels
603 .iter()
604 .map(|(_, v)| *v)
605 .reduce(f64::min)
606 .ok_or_else(|| EngineError::Input("no measured main".into()))?;
607 let input = BaInput {
608 measurements: req.measurements,
609 prepared,
610 setup: &setup,
611 config,
612 tuning: req.tuning,
613 xo_of: &xo_of,
614 ref_level,
615 cancel: ctx.cancel(),
616 };
617 let ba_started = std::time::Instant::now();
618 let mut ba_fitter = fitters(FitStage::Ba);
619 let alignment = ba::align(&input, &mut *ba_fitter)?;
620 check(ctx)?;
621 #[cfg(test)]
622 let mut timings = vec![("BA".into(), ba_started.elapsed())];
623 notes.extend(alignment.notes.clone());
624 progress.advance(1);
625 let ba_elapsed = ba_started.elapsed();
626 let mut used = prepared.h.clone();
627 for (p, r, h) in &alignment.h_subs {
628 used.set(*p, r, h.clone());
629 }
630 let top_started = std::time::Instant::now();
631 let mut top_peq = Vec::new();
632 let mut top_responses = Vec::new();
633 if !config.subs.is_empty() && req.tuning.xo_stage {
634 for (role, xo) in &xo_of {
635 check(ctx)?;
636 let lo = if config.xo_peq_lo_hz == 0.0 {
637 xo / 2.0
638 } else {
639 config.xo_peq_lo_hz
640 };
641 let hi = if config.xo_peq_hi_hz == 0.0 {
642 xo * 4.0
643 } else {
644 config.xo_peq_hi_hz
645 };
646 if !config.xo_peq_on || hi <= lo {
647 continue;
648 }
649 let level = levels
650 .iter()
651 .find(|(r, _)| r == role)
652 .ok_or_else(|| EngineError::Input(format!("XO role {role} missing at reference")))?
653 .1;
654 let gain = C::new(10f64.powf((ref_level - level) / 20.0), 0.0);
655 let by_pos: Vec<_> = positions
656 .iter()
657 .zip(&weights)
658 .filter_map(|(&p, &w)| {
659 used.get(p, role)
660 .map(|h| (w, h.iter().map(|v| v * gain).collect::<Vec<_>>()))
661 })
662 .collect();
663 let weighted: Vec<_> = by_pos.iter().map(|(w, h)| (*w, h.as_slice())).collect();
664 let peq = fit::xo_target_peq(
665 &weighted,
666 &config.target,
667 ref_level,
668 lo,
669 hi,
670 6,
671 config.max_boost_db.min(6.0),
672 Some(hi),
673 &mut *fitters(FitStage::Top(role)),
674 );
675 check(ctx)?;
676 let response: Arc<[C]> = response::peq_response(design_freqs(), &peq).into();
677 for &p in &positions {
678 if let Some(h) = used.get(p, role) {
679 used.set(
680 p,
681 role,
682 h.iter().zip(response.iter()).map(|(h, p)| h * p).collect(),
683 );
684 }
685 }
686 top_responses.push((role.clone(), response));
687 top_peq.push((role.clone(), peq));
688 }
689 }
690 #[cfg(test)]
691 timings.push(("top PEQ".into(), top_started.elapsed()));
692 observed("top", top_started.elapsed());
693 let bus_started = std::time::Instant::now();
694 let (bus_delay_ms, bus_polarity) = if config.subs.is_empty() {
695 (0.0, 1)
696 } else {
697 let earliest = alignment
698 .rows
699 .iter()
700 .map(|r| r.delay_ms)
701 .fold(f64::INFINITY, f64::min);
702 let lo = (-(setup.max_latency_ms - setup.lift_ms) - earliest) * 4.0;
703 let mains: Vec<_> = config
704 .mains
705 .iter()
706 .map(|r| {
707 (
708 r.clone(),
709 positions
710 .iter()
711 .filter(|p| alignment.sub_sum_by_pos.iter().any(|(q, _)| q == *p))
712 .filter_map(|&p| used.get(p, r).map(|h| (p, h.clone())))
713 .collect(),
714 )
715 })
716 .collect();
717 ba::bus_delay(
718 &alignment,
719 &mains,
720 &prepared.align_delay_ms,
721 &xo_of,
722 lo.ceil() / 4.0,
723 &|| ctx.cancel().is_cancelled(),
724 )?
725 };
726 check(ctx)?;
727 let rows = ba::fold(
728 &alignment.rows,
729 alignment.bus_gain,
730 bus_delay_ms,
731 bus_polarity,
732 );
733 observed("ba", ba_elapsed + bus_started.elapsed());
734 let shift = if alignment.plan.as_ref().is_some_and(|p| !p.fixed.is_empty()) {
735 config.sub_main_delay_ms
736 } else {
737 (-rows
738 .iter()
739 .map(|r| r.delay_ms)
740 .reduce(f64::min)
741 .unwrap_or(0.0))
742 .max(0.0)
743 };
744 let roles = if req.subs_only && !matches!(pass, RcPass::Probe) {
745 vec![]
746 } else {
747 config.mains.iter().collect()
748 };
749 let rc_all_started = std::time::Instant::now();
750 let outputs = crate::memory::try_map(
753 ctx,
754 roles,
755 reserve.held,
756 |_| reserve.channel,
757 |role| {
758 check(ctx)?;
759 let Some(before) = prepared.h0.get(config.main_position, role) else {
760 progress.advance(1);
761 return Ok(None);
762 };
763 let rc_started = std::time::Instant::now();
764 let gain_db = ref_level - levels.iter().find(|(r, _)| r == role).unwrap().1;
765 let xo_hz = xo_of.iter().find(|(r, _)| r == role).map(|(_, x)| *x);
766 let align_delay_ms = prepared
767 .align_delay_ms
768 .iter()
769 .find(|(r, _)| r == role)
770 .unwrap()
771 .1;
772 let comb: Vec<(u32, Arc<[C]>)> = positions
773 .iter()
774 .filter_map(|&p| {
775 used.get(p, role).map(|h| {
776 let sub = alignment
777 .sub_sum_by_pos
778 .iter()
779 .find(|(q, _)| *q == p)
780 .map(|(_, h)| h.as_ref());
781 (
782 p,
783 rc::combine(
784 h,
785 align_delay_ms,
786 gain_db,
787 xo_hz,
788 sub,
789 bus_polarity,
790 bus_delay_ms,
791 )
792 .into(),
793 )
794 })
795 })
796 .collect();
797 let mut fitter = fitters(FitStage::Rc(role));
798 let rc_input = RcInput {
799 role,
800 comb: &comb,
801 main_position: config.main_position,
802 level_db: ref_level,
803 setup: &setup,
804 config,
805 tuning: req.tuning,
806 positions: &positions,
807 };
808 let result = rc::channel(&rc_input, &mut *fitter, &|| ctx.cancel().is_cancelled())?;
809 check(ctx)?;
810 let agreement = matches!(pass, RcPass::Probe)
811 .then(|| rc::agreement_limit(&rc_input, &result.response));
812 let combined = comb
813 .iter()
814 .find(|(p, _)| *p == config.main_position)
815 .unwrap()
816 .1
817 .clone();
818 let response: Arc<[C]> = result.response.into();
819 let after = combined
820 .iter()
821 .zip(response.iter())
822 .map(|(h, f)| h * f)
823 .collect();
824 let mut peq = result.out.peq;
825 peq.extend(result.gd_cuts);
826 let channel = MainChannel {
827 role: role.clone(),
828 input_peq: peq,
829 fir: result.out.fir,
830 gain_db,
831 delay_ms: align_delay_ms + shift,
832 xo_hz,
833 align_delay_ms,
834 top_peq: top_peq
835 .iter()
836 .find(|(r, _)| r == role)
837 .map_or_else(Vec::new, |(_, p)| p.clone()),
838 };
839 let main = MainDesign {
840 role: role.clone(),
841 gain_db,
842 xo_hz,
843 align_delay_ms,
844 top_response: top_responses
845 .iter()
846 .find(|(r, _)| r == role)
847 .map(|(_, p)| p.clone()),
848 response,
849 phase_strength: result.out.phase_strength,
850 phase_band: result.out.phase_band,
851 phase_frac_used: result.phase_frac_used,
852 before: before.clone(),
853 combined,
854 after,
855 };
856 observed(&format!("rc:{role}"), rc_started.elapsed());
857 progress.advance(1);
858 #[cfg(test)]
859 let elapsed = rc_started.elapsed();
860 #[cfg(not(test))]
861 let elapsed = ();
862 Ok(Some((channel, main, result.out.notes, elapsed, agreement)))
863 },
864 )?;
865 observed("rc_all", rc_all_started.elapsed());
866 let mut channels = Vec::new();
867 let mut mains = Vec::new();
868 let mut agreement_limits = Vec::new();
869 for (channel, main, channel_notes, _elapsed, agreement) in outputs.into_iter().flatten() {
870 if let Some(limit) = agreement {
871 agreement_limits.push(limit);
872 }
873 #[cfg(test)]
874 timings.push((format!("RC {}", main.role), _elapsed));
875 channels.push(channel);
876 mains.push(main);
877 notes.extend(channel_notes);
878 }
879 let latency_ms = setup.lift_ms + shift;
880 notes.push(Note::Latency {
881 latency_ms,
882 lift_ms: setup.lift_ms,
883 main_extra_ms: shift,
884 });
885 let mut subs = Vec::new();
886 for (role, row) in config.subs.iter().zip(&rows) {
887 let mut sub = SubChannel {
888 role: role.clone(),
889 gain_db: row.gain_db,
890 delay_ms: row.delay_ms + shift,
891 polarity: row.polarity,
892 allpass: row.allpass.clone(),
893 hp_hz: 0.0,
894 hp_type: BandType::Lr24,
895 lp_hz: 0.0,
896 lp_type: BandType::Lr24,
897 peq: vec![],
898 mode: ba::sub_mode_label(role, &setup, &alignment.layout_info),
899 };
900 if alignment
901 .plan
902 .as_ref()
903 .is_some_and(|p| p.fixed.contains(role))
904 {
905 let m = &setup.sub_manual.iter().find(|(r, _)| r == role).unwrap().1;
906 sub.gain_db = m.gain_db;
907 sub.delay_ms = m.delay_ms;
908 sub.polarity = if m.polarity < 0 { -1 } else { 1 };
909 sub.allpass = m.allpass.iter().copied().take(2).collect();
910 sub.mode = "fixed".into();
911 }
912 if let Some((_, f)) = alignment.sub_filters.iter().find(|(r, _)| r == role) {
913 sub.hp_hz = f.hp_hz;
914 sub.hp_type = f.hp_type;
915 sub.lp_hz = f.lp_hz;
916 sub.lp_type = f.lp_type;
917 sub.peq = f.peq.clone();
918 }
919 if config.low_protect {
920 let by_pos: Vec<_> = positions
921 .iter()
922 .filter_map(|&p| used.get(p, role).map(|h| (p, h.clone())))
923 .collect();
924 if let Some(hz) = crate::low_protect::rolloff(
925 &by_pos,
926 config.main_position,
927 Some((sub.hp_hz, sub.lp_hz)),
928 ) {
929 if sub.hp_hz == 0.0 {
930 sub.hp_hz = hz / 2.0_f64.sqrt();
931 sub.hp_type = BandType::Lr24;
932 }
933 notes.push(Note::LowProtect {
934 role: role.clone(),
935 rolloff_hz: hz,
936 hp_hz: Some(sub.hp_hz),
937 limited: true,
938 });
939 }
940 }
941 subs.push(sub);
942 }
943 let sub = if config.subs.is_empty() {
944 None
945 } else {
946 Some(SubDesign {
947 rows: config.subs.iter().cloned().zip(rows).collect(),
948 bus_gain: alignment.bus_gain,
949 bus_delay_ms,
950 bus_polarity,
951 bus_response: (!alignment.bus_peq.is_empty())
952 .then(|| response::peq_response(design_freqs(), &alignment.bus_peq).into()),
953 sum_at_reference: alignment
954 .sub_sum_by_pos
955 .iter()
956 .find(|(p, _)| *p == config.main_position)
957 .unwrap()
958 .1
959 .clone(),
960 sub_level_db: alignment.sub_level_db,
961 layout_info: alignment.layout_info,
962 plan: alignment.plan,
963 })
964 };
965 let f = design_freqs();
966 let target = config
967 .target
968 .sample(&f.iter().map(|f| f.max(1.0)).collect::<Vec<_>>());
969 let target_db: Arc<[f64]> = target.iter().map(|v| v + ref_level).collect();
970 let spl_grid = if include_scores {
971 geomspace(5.0, 20000.0, 1390)
972 } else {
973 Vec::new()
974 };
975 let gd_grid = if include_scores {
976 geomspace(5.0, 5000.0, 1070)
977 } else {
978 Vec::new()
979 };
980 let scores_started = std::time::Instant::now();
981 let scores = if include_scores {
982 crate::memory::try_map(
984 ctx,
985 mains.iter().collect(),
986 reserve.held,
987 |_| reserve.channel,
988 |main| {
989 check(ctx)?;
990 Ok(RoleScores {
991 role: main.role.clone(),
992 before: scorecard::metrics(&main.before, f, &target, (100.0, 400.0)),
993 after: scorecard::metrics(&main.after, f, &target, (100.0, 400.0)),
994 spl_before: spl_db(&main.before, &spl_grid),
995 spl_after: spl_db(&main.after, &spl_grid),
996 gd_before: response::gd_curve(
997 &main.before,
998 f,
999 &gd_grid,
1000 Fraction::Scalar(24.0),
1001 )?,
1002 gd_after: response::gd_curve(&main.after, f, &gd_grid, Fraction::Scalar(24.0))?,
1003 })
1004 },
1005 )?
1006 } else {
1007 Vec::new()
1008 };
1009 observed("scores", scores_started.elapsed());
1010 #[cfg(test)]
1011 timings.push(("scores".into(), scores_started.elapsed()));
1012 notes.push(Note::PhaseStrength(
1013 mains
1014 .iter()
1015 .map(|m| (m.role.clone(), m.phase_strength))
1016 .collect(),
1017 ));
1018 notes.push(Note::PhaseBand(
1019 mains
1020 .iter()
1021 .map(|m| (m.role.clone(), m.phase_band))
1022 .collect(),
1023 ));
1024 check(ctx)?;
1025 Ok((
1026 Design {
1027 chain: SignalChain {
1028 sample_rate: SR,
1029 mains: channels,
1030 subs,
1031 sub_bus_peq: alignment.bus_peq,
1032 main_extra_ms: shift,
1033 lift_ms: setup.lift_ms,
1034 latency_ms,
1035 },
1036 mains,
1037 subs: sub,
1038 ref_level,
1039 rc_hi: setup.rc_hi,
1040 rc_bands,
1041 rc_auto,
1042 schroeder_hz,
1043 target_display: spl_grid.iter().map(|&g| interp(g, f, &target_db)).collect(),
1044 target_db,
1045 spectra: DesignSpectra {
1046 raw: Arc::new(prepared.h0.clone()),
1047 used: Arc::new(used),
1048 main_position: config.main_position,
1049 positions,
1050 },
1051 notes,
1052 adapter_notes: req.adapter_notes.to_vec(),
1053 groups: config.groups.clone(),
1054 target: config.target.clone(),
1055 xo: config.xo.clone(),
1056 scores,
1057 replay: Arc::new(crate::DesignReplay::new(
1058 req.measurements,
1059 config,
1060 setup,
1061 prepared,
1062 )),
1063 #[cfg(test)]
1064 timings,
1065 },
1066 agreement_limits,
1067 ))
1068}