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 chain::{BandType, MainChannel, SignalChain, SubChannel},
14 target::TargetCurve,
15};
16
17use crate::{
18 C, EngineConfig, EngineError, MeasurementSet, Tuning,
19 ba::{self, BaInput, LayoutInfo, ManualPlan},
20 chain::BaRow,
21 fit::{self, Fitter, PeqDerivatives},
22 grid::{N_FFT, SR, design_freqs},
23 levels,
24 measurement::{self, Spectra},
25 notes::Note,
26 rc::{self, RcInput},
27 scorecard::{self, Metrics},
28 setup::Setup,
29};
30
31#[derive(Debug)]
32pub enum FitStage<'a> {
33 Ba,
34 Top(&'a str),
35 Rc(&'a str),
36}
37pub type FitterFactory<'f> = dyn Fn(FitStage<'_>) -> Box<Fitter<'f>> + Sync + 'f;
38pub fn production_fitters(cancel: &CancelToken) -> Box<FitterFactory<'_>> {
39 production_fitters_with(cancel, PeqDerivatives::TwoPoint)
40}
41
42pub fn production_fitters_with(
43 cancel: &CancelToken,
44 derivatives: PeqDerivatives,
45) -> Box<FitterFactory<'_>> {
46 Box::new(move |_| {
47 Box::new(move |request| {
48 if cancel.is_cancelled() {
49 Vec::new()
50 } else {
51 fit::fit_peq_with(request, &|| cancel.is_cancelled(), derivatives)
52 }
53 })
54 })
55}
56
57const RC_AUTO_MAX_HZ: f64 = 500.0;
59
60#[derive(Debug)]
61pub struct DesignRequest<'a> {
62 pub measurements: &'a MeasurementSet,
63 pub config: &'a EngineConfig,
64 pub tuning: &'a Tuning,
65 pub subs_only: bool,
66 pub adapter_notes: &'a [String],
67}
68#[derive(Debug)]
69pub struct Design {
70 pub replay: Arc<crate::DesignReplay>,
71 pub chain: SignalChain,
72 pub mains: Vec<MainDesign>,
73 pub subs: Option<SubDesign>,
74 pub ref_level: f64,
75 pub rc_hi: 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 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 check(ctx)?;
428 check_taps(req.config)?;
429 let config = req.config;
430 let mut setup = Setup::new(config);
431 let positions = prepared.h.positions().to_vec();
432 let progress = ctx.stage("engine.design", 1 + config.mains.len() as u64);
433 let mut notes = Vec::new();
434 if let Some(from_ms) = setup.lift_capped_from_ms {
435 notes.push(Note::LiftCapped {
436 from_ms,
437 taps: config.taps,
438 lift_ms: setup.lift_ms,
439 });
440 }
441 if !setup.sub_filters.is_empty() {
442 notes.push(Note::SubFilters(setup.sub_filters.clone()));
443 }
444 let rc_auto = setup.rc_hi_manual.is_none();
445 let schroeder_hz = rc_auto
446 .then(|| levels::schroeder_hz(req.measurements, config))
447 .flatten();
448 if rc_auto {
449 setup.rc_hi = schroeder_hz.map_or(RC_AUTO_MAX_HZ, |f| f.clamp(100.0, RC_AUTO_MAX_HZ));
450 }
451 notes.push(Note::RcLimit {
452 hz: setup.rc_hi,
453 auto: rc_auto,
454 schroeder_hz,
455 });
456 let xo_of: Vec<_> = config
457 .groups
458 .iter()
459 .filter(|(g, _)| g != "sub")
460 .flat_map(|(g, roles)| {
461 roles.iter().filter_map(move |r| {
462 config
463 .xo
464 .iter()
465 .find(|(name, x)| name == g && *x != 0.0 && !config.subs.is_empty())
466 .map(|(_, x)| (r.clone(), *x))
467 })
468 })
469 .collect();
470 let weights = levels::weights(&positions, config.main_position);
471 let levels: Vec<_> = config
472 .mains
473 .iter()
474 .filter(|r| prepared.h.get(config.main_position, r).is_some())
475 .map(|r| {
476 (
477 r.clone(),
478 levels::band_level(&prepared.h, r, &weights, &config.target, 300.0, 3000.0),
479 )
480 })
481 .collect();
482 let ref_level = levels
483 .iter()
484 .map(|(_, v)| *v)
485 .reduce(f64::min)
486 .ok_or_else(|| EngineError::Input("no measured main".into()))?;
487 let input = BaInput {
488 measurements: req.measurements,
489 prepared,
490 setup: &setup,
491 config,
492 tuning: req.tuning,
493 xo_of: &xo_of,
494 ref_level,
495 cancel: ctx.cancel(),
496 };
497 let ba_started = std::time::Instant::now();
498 let mut ba_fitter = fitters(FitStage::Ba);
499 let alignment = ba::align(&input, &mut *ba_fitter)?;
500 check(ctx)?;
501 #[cfg(test)]
502 let mut timings = vec![("BA".into(), ba_started.elapsed())];
503 notes.extend(alignment.notes.clone());
504 progress.advance(1);
505 let ba_elapsed = ba_started.elapsed();
506 let mut used = prepared.h.clone();
507 for (p, r, h) in &alignment.h_subs {
508 used.set(*p, r, h.clone());
509 }
510 let top_started = std::time::Instant::now();
511 let mut top_peq = Vec::new();
512 let mut top_responses = Vec::new();
513 if !config.subs.is_empty() && req.tuning.xo_stage {
514 for (role, xo) in &xo_of {
515 check(ctx)?;
516 let lo = if config.xo_peq_lo_hz == 0.0 {
517 xo / 2.0
518 } else {
519 config.xo_peq_lo_hz
520 };
521 let hi = if config.xo_peq_hi_hz == 0.0 {
522 xo * 4.0
523 } else {
524 config.xo_peq_hi_hz
525 };
526 if !config.xo_peq_on || hi <= lo {
527 continue;
528 }
529 let level = levels
530 .iter()
531 .find(|(r, _)| r == role)
532 .ok_or_else(|| EngineError::Input(format!("XO role {role} missing at reference")))?
533 .1;
534 let gain = C::new(10f64.powf((ref_level - level) / 20.0), 0.0);
535 let by_pos: Vec<_> = positions
536 .iter()
537 .zip(&weights)
538 .filter_map(|(&p, &w)| {
539 used.get(p, role)
540 .map(|h| (w, h.iter().map(|v| v * gain).collect::<Vec<_>>()))
541 })
542 .collect();
543 let weighted: Vec<_> = by_pos.iter().map(|(w, h)| (*w, h.as_slice())).collect();
544 let peq = fit::xo_target_peq(
545 &weighted,
546 &config.target,
547 ref_level,
548 lo,
549 hi,
550 6,
551 config.max_boost_db.min(6.0),
552 Some(hi),
553 &mut *fitters(FitStage::Top(role)),
554 );
555 check(ctx)?;
556 let response: Arc<[C]> = response::peq_response(design_freqs(), &peq).into();
557 for &p in &positions {
558 if let Some(h) = used.get(p, role) {
559 used.set(
560 p,
561 role,
562 h.iter().zip(response.iter()).map(|(h, p)| h * p).collect(),
563 );
564 }
565 }
566 top_responses.push((role.clone(), response));
567 top_peq.push((role.clone(), peq));
568 }
569 }
570 #[cfg(test)]
571 timings.push(("top PEQ".into(), top_started.elapsed()));
572 observed("top", top_started.elapsed());
573 let bus_started = std::time::Instant::now();
574 let (bus_delay_ms, bus_polarity) = if config.subs.is_empty() {
575 (0.0, 1)
576 } else {
577 let earliest = alignment
578 .rows
579 .iter()
580 .map(|r| r.delay_ms)
581 .fold(f64::INFINITY, f64::min);
582 let lo = (-(setup.max_latency_ms - setup.lift_ms) - earliest) * 4.0;
583 let mains: Vec<_> = config
584 .mains
585 .iter()
586 .map(|r| {
587 (
588 r.clone(),
589 positions
590 .iter()
591 .filter(|p| alignment.sub_sum_by_pos.iter().any(|(q, _)| q == *p))
592 .filter_map(|&p| used.get(p, r).map(|h| (p, h.clone())))
593 .collect(),
594 )
595 })
596 .collect();
597 ba::bus_delay(
598 &alignment,
599 &mains,
600 &prepared.align_delay_ms,
601 &xo_of,
602 lo.ceil() / 4.0,
603 &|| ctx.cancel().is_cancelled(),
604 )?
605 };
606 check(ctx)?;
607 let rows = ba::fold(
608 &alignment.rows,
609 alignment.bus_gain,
610 bus_delay_ms,
611 bus_polarity,
612 );
613 observed("ba", ba_elapsed + bus_started.elapsed());
614 let shift = if alignment.plan.as_ref().is_some_and(|p| !p.fixed.is_empty()) {
615 config.sub_main_delay_ms
616 } else {
617 (-rows
618 .iter()
619 .map(|r| r.delay_ms)
620 .reduce(f64::min)
621 .unwrap_or(0.0))
622 .max(0.0)
623 };
624 let roles = if req.subs_only {
625 vec![]
626 } else {
627 config.mains.iter().collect()
628 };
629 let rc_all_started = std::time::Instant::now();
630 let outputs = crate::memory::try_map(
633 ctx,
634 roles,
635 reserve.held,
636 |_| reserve.channel,
637 |role| {
638 check(ctx)?;
639 let Some(before) = prepared.h0.get(config.main_position, role) else {
640 progress.advance(1);
641 return Ok(None);
642 };
643 let rc_started = std::time::Instant::now();
644 let gain_db = ref_level - levels.iter().find(|(r, _)| r == role).unwrap().1;
645 let xo_hz = xo_of.iter().find(|(r, _)| r == role).map(|(_, x)| *x);
646 let align_delay_ms = prepared
647 .align_delay_ms
648 .iter()
649 .find(|(r, _)| r == role)
650 .unwrap()
651 .1;
652 let comb: Vec<(u32, Arc<[C]>)> = positions
653 .iter()
654 .filter_map(|&p| {
655 used.get(p, role).map(|h| {
656 let sub = alignment
657 .sub_sum_by_pos
658 .iter()
659 .find(|(q, _)| *q == p)
660 .map(|(_, h)| h.as_ref());
661 (
662 p,
663 rc::combine(
664 h,
665 align_delay_ms,
666 gain_db,
667 xo_hz,
668 sub,
669 bus_polarity,
670 bus_delay_ms,
671 )
672 .into(),
673 )
674 })
675 })
676 .collect();
677 let mut fitter = fitters(FitStage::Rc(role));
678 let result = rc::channel(
679 &RcInput {
680 role,
681 comb: &comb,
682 main_position: config.main_position,
683 level_db: ref_level,
684 setup: &setup,
685 config,
686 tuning: req.tuning,
687 positions: &positions,
688 },
689 &mut *fitter,
690 &|| ctx.cancel().is_cancelled(),
691 )?;
692 check(ctx)?;
693 let combined = comb
694 .iter()
695 .find(|(p, _)| *p == config.main_position)
696 .unwrap()
697 .1
698 .clone();
699 let response: Arc<[C]> = result.response.into();
700 let after = combined
701 .iter()
702 .zip(response.iter())
703 .map(|(h, f)| h * f)
704 .collect();
705 let mut peq = result.out.peq;
706 peq.extend(result.gd_cuts);
707 let channel = MainChannel {
708 role: role.clone(),
709 input_peq: peq,
710 fir: result.out.fir,
711 gain_db,
712 delay_ms: align_delay_ms + shift,
713 xo_hz,
714 align_delay_ms,
715 top_peq: top_peq
716 .iter()
717 .find(|(r, _)| r == role)
718 .map_or_else(Vec::new, |(_, p)| p.clone()),
719 };
720 let main = MainDesign {
721 role: role.clone(),
722 gain_db,
723 xo_hz,
724 align_delay_ms,
725 top_response: top_responses
726 .iter()
727 .find(|(r, _)| r == role)
728 .map(|(_, p)| p.clone()),
729 response,
730 phase_strength: result.out.phase_strength,
731 phase_band: result.out.phase_band,
732 phase_frac_used: result.phase_frac_used,
733 before: before.clone(),
734 combined,
735 after,
736 };
737 observed(&format!("rc:{role}"), rc_started.elapsed());
738 progress.advance(1);
739 #[cfg(test)]
740 let elapsed = rc_started.elapsed();
741 #[cfg(not(test))]
742 let elapsed = ();
743 Ok(Some((channel, main, result.out.notes, elapsed)))
744 },
745 )?;
746 observed("rc_all", rc_all_started.elapsed());
747 let mut channels = Vec::new();
748 let mut mains = Vec::new();
749 for (channel, main, channel_notes, _elapsed) in outputs.into_iter().flatten() {
750 #[cfg(test)]
751 timings.push((format!("RC {}", main.role), _elapsed));
752 channels.push(channel);
753 mains.push(main);
754 notes.extend(channel_notes);
755 }
756 let latency_ms = setup.lift_ms + shift;
757 notes.push(Note::Latency {
758 latency_ms,
759 lift_ms: setup.lift_ms,
760 main_extra_ms: shift,
761 });
762 let mut subs = Vec::new();
763 for (role, row) in config.subs.iter().zip(&rows) {
764 let mut sub = SubChannel {
765 role: role.clone(),
766 gain_db: row.gain_db,
767 delay_ms: row.delay_ms + shift,
768 polarity: row.polarity,
769 allpass: row.allpass.clone(),
770 hp_hz: 0.0,
771 hp_type: BandType::Lr24,
772 lp_hz: 0.0,
773 lp_type: BandType::Lr24,
774 peq: vec![],
775 mode: ba::sub_mode_label(role, &setup, &alignment.layout_info),
776 };
777 if alignment
778 .plan
779 .as_ref()
780 .is_some_and(|p| p.fixed.contains(role))
781 {
782 let m = &setup.sub_manual.iter().find(|(r, _)| r == role).unwrap().1;
783 sub.gain_db = m.gain_db;
784 sub.delay_ms = m.delay_ms;
785 sub.polarity = if m.polarity < 0 { -1 } else { 1 };
786 sub.allpass = m.allpass.iter().copied().take(2).collect();
787 sub.mode = "fixed".into();
788 }
789 if let Some((_, f)) = alignment.sub_filters.iter().find(|(r, _)| r == role) {
790 sub.hp_hz = f.hp_hz;
791 sub.hp_type = f.hp_type;
792 sub.lp_hz = f.lp_hz;
793 sub.lp_type = f.lp_type;
794 sub.peq = f.peq.clone();
795 }
796 subs.push(sub);
797 }
798 let sub = if config.subs.is_empty() {
799 None
800 } else {
801 Some(SubDesign {
802 rows: config.subs.iter().cloned().zip(rows).collect(),
803 bus_gain: alignment.bus_gain,
804 bus_delay_ms,
805 bus_polarity,
806 bus_response: (!alignment.bus_peq.is_empty())
807 .then(|| response::peq_response(design_freqs(), &alignment.bus_peq).into()),
808 sum_at_reference: alignment
809 .sub_sum_by_pos
810 .iter()
811 .find(|(p, _)| *p == config.main_position)
812 .unwrap()
813 .1
814 .clone(),
815 sub_level_db: alignment.sub_level_db,
816 layout_info: alignment.layout_info,
817 plan: alignment.plan,
818 })
819 };
820 let f = design_freqs();
821 let target = config
822 .target
823 .sample(&f.iter().map(|f| f.max(1.0)).collect::<Vec<_>>());
824 let target_db: Arc<[f64]> = target.iter().map(|v| v + ref_level).collect();
825 let spl_grid = if include_scores {
826 geomspace(5.0, 20000.0, 1390)
827 } else {
828 Vec::new()
829 };
830 let gd_grid = if include_scores {
831 geomspace(5.0, 5000.0, 1070)
832 } else {
833 Vec::new()
834 };
835 let scores_started = std::time::Instant::now();
836 let scores = if include_scores {
837 crate::memory::try_map(
839 ctx,
840 mains.iter().collect(),
841 reserve.held,
842 |_| reserve.channel,
843 |main| {
844 check(ctx)?;
845 Ok(RoleScores {
846 role: main.role.clone(),
847 before: scorecard::metrics(&main.before, f, &target, (100.0, 400.0)),
848 after: scorecard::metrics(&main.after, f, &target, (100.0, 400.0)),
849 spl_before: spl_db(&main.before, &spl_grid),
850 spl_after: spl_db(&main.after, &spl_grid),
851 gd_before: response::gd_curve(
852 &main.before,
853 f,
854 &gd_grid,
855 Fraction::Scalar(24.0),
856 )?,
857 gd_after: response::gd_curve(&main.after, f, &gd_grid, Fraction::Scalar(24.0))?,
858 })
859 },
860 )?
861 } else {
862 Vec::new()
863 };
864 observed("scores", scores_started.elapsed());
865 #[cfg(test)]
866 timings.push(("scores".into(), scores_started.elapsed()));
867 notes.push(Note::PhaseStrength(
868 mains
869 .iter()
870 .map(|m| (m.role.clone(), m.phase_strength))
871 .collect(),
872 ));
873 notes.push(Note::PhaseBand(
874 mains
875 .iter()
876 .map(|m| (m.role.clone(), m.phase_band))
877 .collect(),
878 ));
879 check(ctx)?;
880 Ok(Design {
881 chain: SignalChain {
882 sample_rate: SR,
883 mains: channels,
884 subs,
885 sub_bus_peq: alignment.bus_peq,
886 main_extra_ms: shift,
887 lift_ms: setup.lift_ms,
888 latency_ms,
889 },
890 mains,
891 subs: sub,
892 ref_level,
893 rc_hi: setup.rc_hi,
894 rc_auto,
895 schroeder_hz,
896 target_display: spl_grid.iter().map(|&g| interp(g, f, &target_db)).collect(),
897 target_db,
898 spectra: DesignSpectra {
899 raw: Arc::new(prepared.h0.clone()),
900 used: Arc::new(used),
901 main_position: config.main_position,
902 positions,
903 },
904 notes,
905 adapter_notes: req.adapter_notes.to_vec(),
906 groups: config.groups.clone(),
907 target: config.target.clone(),
908 xo: config.xo.clone(),
909 scores,
910 replay: Arc::new(crate::DesignReplay::new(
911 req.measurements,
912 config,
913 setup,
914 prepared,
915 )),
916 #[cfg(test)]
917 timings,
918 })
919}