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