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