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