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 setup.hf_phase = false;
522 }
523 let positions = prepared.h.positions().to_vec();
524 let progress = ctx.stage("engine.design", 1 + config.mains.len() as u64);
525 let mut notes = Vec::new();
526 if let Some(from_ms) = setup.lift_capped_from_ms {
527 notes.push(Note::LiftCapped {
528 from_ms,
529 taps: config.taps,
530 lift_ms: setup.lift_ms,
531 });
532 }
533 if !setup.sub_filters.is_empty() {
534 notes.push(Note::SubFilters(setup.sub_filters.clone()));
535 }
536 let rc_auto = setup.rc_hi_manual.is_none();
537 let schroeder_hz = rc_auto
538 .then(|| levels::schroeder_hz(req.measurements, config))
539 .flatten();
540 let lenient = (rc_auto && config.rc_hi_basis == RcHiBasis::Lenient)
541 .then(|| levels::lenient_schroeder_hz(req.measurements, config))
542 .flatten();
543 if rc_auto {
544 if !matches!(config.rc_hi_transition, 1 | 2 | 4) {
545 return Err(EngineError::Input(
546 "RC transition factor must be 1, 2 or 4".into(),
547 ));
548 }
549 setup.rc_hi = match pass {
550 RcPass::Probe => 2000.0,
551 RcPass::Final(hz) => hz,
552 RcPass::Normal => {
553 levels::schroeder_limit(schroeder_hz, lenient.map(|v| v.0), config.rc_hi_transition)
554 }
555 };
556 }
557 notes.push(Note::RcLimit {
558 hz: setup.rc_hi,
559 auto: rc_auto,
560 schroeder_hz,
561 basis: config.rc_hi_basis,
562 factor: config.rc_hi_transition,
563 band_hz: lenient.map(|v| v.0),
564 band_t60_s: lenient.map(|v| v.1),
565 positions: (rc_auto && config.rc_hi_basis == RcHiBasis::Agreement)
566 .then(|| agreement_positions(req.measurements, config)),
567 });
568 let rc_bands = if setup.rc_bands.is_empty() {
569 Vec::new()
570 } else {
571 let bands = rc::band_set(setup.rc_lo, setup.rc_hi, &setup.rc_bands);
572 notes.push(Note::RcBands {
573 bands: bands.clone(),
574 });
575 bands
576 };
577 let xo_of: Vec<_> = config
578 .groups
579 .iter()
580 .filter(|(g, _)| g != "sub")
581 .flat_map(|(g, roles)| {
582 roles.iter().filter_map(move |r| {
583 config
584 .xo
585 .iter()
586 .find(|(name, x)| name == g && *x != 0.0 && !config.subs.is_empty())
587 .map(|(_, x)| (r.clone(), *x))
588 })
589 })
590 .collect();
591 let weights = levels::weights(&positions, config.main_position);
592 let levels: Vec<_> = config
593 .mains
594 .iter()
595 .filter(|r| prepared.h.get(config.main_position, r).is_some())
596 .map(|r| {
597 (
598 r.clone(),
599 levels::band_level(&prepared.h, r, &weights, &config.target, 300.0, 3000.0),
600 )
601 })
602 .collect();
603 let ref_level = levels
604 .iter()
605 .map(|(_, v)| *v)
606 .reduce(f64::min)
607 .ok_or_else(|| EngineError::Input("no measured main".into()))?;
608 let input = BaInput {
609 measurements: req.measurements,
610 prepared,
611 setup: &setup,
612 config,
613 tuning: req.tuning,
614 xo_of: &xo_of,
615 ref_level,
616 cancel: ctx.cancel(),
617 };
618 let ba_started = std::time::Instant::now();
619 let mut ba_fitter = fitters(FitStage::Ba);
620 let alignment = ba::align(&input, &mut *ba_fitter)?;
621 check(ctx)?;
622 #[cfg(test)]
623 let mut timings = vec![("BA".into(), ba_started.elapsed())];
624 notes.extend(alignment.notes.clone());
625 progress.advance(1);
626 let ba_elapsed = ba_started.elapsed();
627 let mut used = prepared.h.clone();
628 for (p, r, h) in &alignment.h_subs {
629 used.set(*p, r, h.clone());
630 }
631 let top_started = std::time::Instant::now();
632 let mut top_peq = Vec::new();
633 let mut top_responses = Vec::new();
634 if !config.subs.is_empty() && req.tuning.xo_stage {
635 for (role, xo) in &xo_of {
636 check(ctx)?;
637 let lo = if config.xo_peq_lo_hz == 0.0 {
638 xo / 2.0
639 } else {
640 config.xo_peq_lo_hz
641 };
642 let hi = if config.xo_peq_hi_hz == 0.0 {
643 xo * 4.0
644 } else {
645 config.xo_peq_hi_hz
646 };
647 if !config.xo_peq_on || hi <= lo {
648 continue;
649 }
650 let level = levels
651 .iter()
652 .find(|(r, _)| r == role)
653 .ok_or_else(|| EngineError::Input(format!("XO role {role} missing at reference")))?
654 .1;
655 let gain = C::new(10f64.powf((ref_level - level) / 20.0), 0.0);
656 let by_pos: Vec<_> = positions
657 .iter()
658 .zip(&weights)
659 .filter_map(|(&p, &w)| {
660 used.get(p, role)
661 .map(|h| (w, h.iter().map(|v| v * gain).collect::<Vec<_>>()))
662 })
663 .collect();
664 let weighted: Vec<_> = by_pos.iter().map(|(w, h)| (*w, h.as_slice())).collect();
665 let peq = fit::xo_target_peq(
666 &weighted,
667 &config.target,
668 ref_level,
669 lo,
670 hi,
671 6,
672 config.max_boost_db.min(6.0),
673 Some(hi),
674 &mut *fitters(FitStage::Top(role)),
675 );
676 check(ctx)?;
677 let response: Arc<[C]> = response::peq_response(design_freqs(), &peq).into();
678 for &p in &positions {
679 if let Some(h) = used.get(p, role) {
680 used.set(
681 p,
682 role,
683 h.iter().zip(response.iter()).map(|(h, p)| h * p).collect(),
684 );
685 }
686 }
687 top_responses.push((role.clone(), response));
688 top_peq.push((role.clone(), peq));
689 }
690 }
691 #[cfg(test)]
692 timings.push(("top PEQ".into(), top_started.elapsed()));
693 observed("top", top_started.elapsed());
694 let bus_started = std::time::Instant::now();
695 let (bus_delay_ms, bus_polarity) = if config.subs.is_empty() {
696 (0.0, 1)
697 } else {
698 let earliest = alignment
699 .rows
700 .iter()
701 .map(|r| r.delay_ms)
702 .fold(f64::INFINITY, f64::min);
703 let lo = (-(setup.max_latency_ms - setup.lift_ms) - earliest) * 4.0;
704 let mains: Vec<_> = config
705 .mains
706 .iter()
707 .map(|r| {
708 (
709 r.clone(),
710 positions
711 .iter()
712 .filter(|p| alignment.sub_sum_by_pos.iter().any(|(q, _)| q == *p))
713 .filter_map(|&p| used.get(p, r).map(|h| (p, h.clone())))
714 .collect(),
715 )
716 })
717 .collect();
718 ba::bus_delay(
719 &alignment,
720 &mains,
721 &prepared.align_delay_ms,
722 &xo_of,
723 lo.ceil() / 4.0,
724 &|| ctx.cancel().is_cancelled(),
725 )?
726 };
727 check(ctx)?;
728 let rows = ba::fold(
729 &alignment.rows,
730 alignment.bus_gain,
731 bus_delay_ms,
732 bus_polarity,
733 );
734 observed("ba", ba_elapsed + bus_started.elapsed());
735 let shift = if alignment.plan.as_ref().is_some_and(|p| !p.fixed.is_empty()) {
736 config.sub_main_delay_ms
737 } else {
738 (-rows
739 .iter()
740 .map(|r| r.delay_ms)
741 .reduce(f64::min)
742 .unwrap_or(0.0))
743 .max(0.0)
744 };
745 let roles = if req.subs_only && !matches!(pass, RcPass::Probe) {
746 vec![]
747 } else {
748 config.mains.iter().collect()
749 };
750 let rc_all_started = std::time::Instant::now();
751 let outputs = crate::memory::try_map(
754 ctx,
755 roles,
756 reserve.held,
757 |_| reserve.channel,
758 |role| {
759 check(ctx)?;
760 let Some(before) = prepared.h0.get(config.main_position, role) else {
761 progress.advance(1);
762 return Ok(None);
763 };
764 let rc_started = std::time::Instant::now();
765 let gain_db = ref_level - levels.iter().find(|(r, _)| r == role).unwrap().1;
766 let xo_hz = xo_of.iter().find(|(r, _)| r == role).map(|(_, x)| *x);
767 let align_delay_ms = prepared
768 .align_delay_ms
769 .iter()
770 .find(|(r, _)| r == role)
771 .unwrap()
772 .1;
773 let comb: Vec<(u32, Arc<[C]>)> = positions
774 .iter()
775 .filter_map(|&p| {
776 used.get(p, role).map(|h| {
777 let sub = alignment
778 .sub_sum_by_pos
779 .iter()
780 .find(|(q, _)| *q == p)
781 .map(|(_, h)| h.as_ref());
782 (
783 p,
784 rc::combine(
785 h,
786 align_delay_ms,
787 gain_db,
788 xo_hz,
789 sub,
790 bus_polarity,
791 bus_delay_ms,
792 )
793 .into(),
794 )
795 })
796 })
797 .collect();
798 let mut fitter = fitters(FitStage::Rc(role));
799 let rc_input = RcInput {
800 role,
801 comb: &comb,
802 main_position: config.main_position,
803 level_db: ref_level,
804 setup: &setup,
805 config,
806 tuning: req.tuning,
807 positions: &positions,
808 bus_rolloff_hz: if config.low_protect && xo_hz.is_some_and(|hz| hz != 0.0) {
809 alignment.bus_rolloff_hz
810 } else {
811 None
812 },
813 };
814 let result = rc::channel(&rc_input, &mut *fitter, &|| ctx.cancel().is_cancelled())?;
815 check(ctx)?;
816 let agreement = matches!(pass, RcPass::Probe)
817 .then(|| rc::agreement_limit(&rc_input, &result.response));
818 let combined = comb
819 .iter()
820 .find(|(p, _)| *p == config.main_position)
821 .unwrap()
822 .1
823 .clone();
824 let response: Arc<[C]> = result.response.into();
825 let after = combined
826 .iter()
827 .zip(response.iter())
828 .map(|(h, f)| h * f)
829 .collect();
830 let mut peq = result.out.peq;
831 peq.extend(result.gd_cuts);
832 let channel = MainChannel {
833 role: role.clone(),
834 input_peq: peq,
835 fir: result.out.fir,
836 gain_db,
837 delay_ms: align_delay_ms + shift,
838 xo_hz,
839 align_delay_ms,
840 top_peq: top_peq
841 .iter()
842 .find(|(r, _)| r == role)
843 .map_or_else(Vec::new, |(_, p)| p.clone()),
844 };
845 let main = MainDesign {
846 role: role.clone(),
847 gain_db,
848 xo_hz,
849 align_delay_ms,
850 top_response: top_responses
851 .iter()
852 .find(|(r, _)| r == role)
853 .map(|(_, p)| p.clone()),
854 response,
855 phase_strength: result.out.phase_strength,
856 phase_band: result.out.phase_band,
857 phase_frac_used: result.phase_frac_used,
858 before: before.clone(),
859 combined,
860 after,
861 };
862 observed(&format!("rc:{role}"), rc_started.elapsed());
863 progress.advance(1);
864 #[cfg(test)]
865 let elapsed = rc_started.elapsed();
866 #[cfg(not(test))]
867 let elapsed = ();
868 Ok(Some((channel, main, result.out.notes, elapsed, agreement)))
869 },
870 )?;
871 observed("rc_all", rc_all_started.elapsed());
872 let mut channels = Vec::new();
873 let mut mains = Vec::new();
874 let mut agreement_limits = Vec::new();
875 for (channel, main, channel_notes, _elapsed, agreement) in outputs.into_iter().flatten() {
876 if let Some(limit) = agreement {
877 agreement_limits.push(limit);
878 }
879 #[cfg(test)]
880 timings.push((format!("RC {}", main.role), _elapsed));
881 channels.push(channel);
882 mains.push(main);
883 notes.extend(channel_notes);
884 }
885 let latency_ms = setup.lift_ms + shift;
886 notes.push(Note::Latency {
887 latency_ms,
888 lift_ms: setup.lift_ms,
889 main_extra_ms: shift,
890 });
891 let mut subs = Vec::new();
892 for (role, row) in config.subs.iter().zip(&rows) {
893 let mut sub = SubChannel {
894 role: role.clone(),
895 gain_db: row.gain_db,
896 delay_ms: row.delay_ms + shift,
897 polarity: row.polarity,
898 allpass: row.allpass.clone(),
899 hp_hz: 0.0,
900 hp_type: BandType::Lr24,
901 lp_hz: 0.0,
902 lp_type: BandType::Lr24,
903 peq: vec![],
904 mode: ba::sub_mode_label(role, &setup, &alignment.layout_info),
905 };
906 if alignment
907 .plan
908 .as_ref()
909 .is_some_and(|p| p.fixed.contains(role))
910 {
911 let m = &setup.sub_manual.iter().find(|(r, _)| r == role).unwrap().1;
912 sub.gain_db = m.gain_db;
913 sub.delay_ms = m.delay_ms;
914 sub.polarity = if m.polarity < 0 { -1 } else { 1 };
915 sub.allpass = m.allpass.iter().copied().take(2).collect();
916 sub.mode = "fixed".into();
917 }
918 if let Some((_, f)) = alignment.sub_filters.iter().find(|(r, _)| r == role) {
919 sub.hp_hz = f.hp_hz;
920 sub.hp_type = f.hp_type;
921 sub.lp_hz = f.lp_hz;
922 sub.lp_type = f.lp_type;
923 sub.peq = f.peq.clone();
924 }
925 if config.low_protect {
926 let by_pos: Vec<_> = positions
927 .iter()
928 .filter_map(|&p| used.get(p, role).map(|h| (p, h.clone())))
929 .collect();
930 if let Some(hz) = crate::low_protect::rolloff(
931 &by_pos,
932 config.main_position,
933 Some((sub.hp_hz, sub.lp_hz)),
934 ) {
935 if sub.hp_hz == 0.0 {
936 sub.hp_hz = hz / 2.0_f64.sqrt();
937 sub.hp_type = BandType::Lr24;
938 }
939 notes.push(Note::LowProtect {
940 role: role.clone(),
941 rolloff_hz: hz,
942 hp_hz: Some(sub.hp_hz),
943 limited: true,
944 reduced: 0,
945 cuts: 0,
946 });
947 }
948 }
949 subs.push(sub);
950 }
951 let sub = if config.subs.is_empty() {
952 None
953 } else {
954 Some(SubDesign {
955 rows: config.subs.iter().cloned().zip(rows).collect(),
956 bus_gain: alignment.bus_gain,
957 bus_delay_ms,
958 bus_polarity,
959 bus_response: (!alignment.bus_peq.is_empty())
960 .then(|| response::peq_response(design_freqs(), &alignment.bus_peq).into()),
961 sum_at_reference: alignment
962 .sub_sum_by_pos
963 .iter()
964 .find(|(p, _)| *p == config.main_position)
965 .unwrap()
966 .1
967 .clone(),
968 sub_level_db: alignment.sub_level_db,
969 layout_info: alignment.layout_info,
970 plan: alignment.plan,
971 })
972 };
973 let f = design_freqs();
974 let target = config
975 .target
976 .sample(&f.iter().map(|f| f.max(1.0)).collect::<Vec<_>>());
977 let target_db: Arc<[f64]> = target.iter().map(|v| v + ref_level).collect();
978 let spl_grid = if include_scores {
979 geomspace(5.0, 20000.0, 1390)
980 } else {
981 Vec::new()
982 };
983 let gd_grid = if include_scores {
984 geomspace(5.0, 5000.0, 1070)
985 } else {
986 Vec::new()
987 };
988 let scores_started = std::time::Instant::now();
989 let scores = if include_scores {
990 crate::memory::try_map(
992 ctx,
993 mains.iter().collect(),
994 reserve.held,
995 |_| reserve.channel,
996 |main| {
997 check(ctx)?;
998 Ok(RoleScores {
999 role: main.role.clone(),
1000 before: scorecard::metrics(&main.before, f, &target, (100.0, 400.0)),
1001 after: scorecard::metrics(&main.after, f, &target, (100.0, 400.0)),
1002 spl_before: spl_db(&main.before, &spl_grid),
1003 spl_after: spl_db(&main.after, &spl_grid),
1004 gd_before: response::gd_curve(
1005 &main.before,
1006 f,
1007 &gd_grid,
1008 Fraction::Scalar(24.0),
1009 )?,
1010 gd_after: response::gd_curve(&main.after, f, &gd_grid, Fraction::Scalar(24.0))?,
1011 })
1012 },
1013 )?
1014 } else {
1015 Vec::new()
1016 };
1017 observed("scores", scores_started.elapsed());
1018 #[cfg(test)]
1019 timings.push(("scores".into(), scores_started.elapsed()));
1020 notes.push(Note::PhaseStrength(
1021 mains
1022 .iter()
1023 .map(|m| (m.role.clone(), m.phase_strength))
1024 .collect(),
1025 ));
1026 notes.push(Note::PhaseBand(
1027 mains
1028 .iter()
1029 .map(|m| (m.role.clone(), m.phase_band))
1030 .collect(),
1031 ));
1032 check(ctx)?;
1033 Ok((
1034 Design {
1035 chain: SignalChain {
1036 sample_rate: SR,
1037 mains: channels,
1038 subs,
1039 sub_bus_peq: alignment.bus_peq,
1040 main_extra_ms: shift,
1041 lift_ms: setup.lift_ms,
1042 latency_ms,
1043 },
1044 mains,
1045 subs: sub,
1046 ref_level,
1047 rc_hi: setup.rc_hi,
1048 rc_bands,
1049 rc_auto,
1050 schroeder_hz,
1051 target_display: spl_grid.iter().map(|&g| interp(g, f, &target_db)).collect(),
1052 target_db,
1053 spectra: DesignSpectra {
1054 raw: Arc::new(prepared.h0.clone()),
1055 used: Arc::new(used),
1056 main_position: config.main_position,
1057 positions,
1058 },
1059 notes,
1060 adapter_notes: req.adapter_notes.to_vec(),
1061 groups: config.groups.clone(),
1062 target: config.target.clone(),
1063 xo: config.xo.clone(),
1064 scores,
1065 replay: Arc::new(crate::DesignReplay::new(
1066 req.measurements,
1067 config,
1068 setup,
1069 prepared,
1070 )),
1071 #[cfg(test)]
1072 timings,
1073 },
1074 agreement_limits,
1075 ))
1076}