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