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