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}
137fn check(ctx: &Context) -> Result<(), EngineError> {
138 ctx.cancel().check().map_err(|_| EngineError::Cancelled)
139}
140
141pub(crate) fn check_taps(config: &EngineConfig) -> Result<(), EngineError> {
145 if config.taps > N_FFT {
146 return Err(EngineError::Input(format!(
147 "FIR taps {} outside 0..={N_FFT} (0 is PEQ only)",
148 config.taps
149 )));
150 }
151 Ok(())
152}
153
154const RC_SCRATCH_SPECTRA: u64 = 24;
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub(crate) struct DesignBytes {
169 pub retained: u64,
170 pub channel: u64,
171}
172
173impl DesignBytes {
174 pub(crate) fn new(
188 set: &MeasurementSet,
189 config: &EngineConfig,
190 setup: &Setup,
191 subs_only: bool,
192 ) -> Self {
193 let positions = set.positions.len() as u64;
194 let subs = config.subs.len() as u64;
195 let mains = if subs_only {
196 0
197 } else {
198 config.mains.len() as u64
199 };
200 let ba = if config.subs.is_empty() {
201 0
202 } else {
203 measurement::cells(set, Some(&config.subs))
204 .saturating_mul(setup.layouts.len() as u64)
205 .saturating_add(measurement::cells(set, Some(&config.mains)))
206 .saturating_add(2 * positions)
207 .saturating_add(subs + 2)
208 .saturating_add(config.mains.len() as u64)
209 };
210 let spectra = |n: u64| n.saturating_mul(measurement::SPECTRUM_BYTES);
211 Self {
212 retained: spectra(ba.saturating_add(3 * mains))
213 .saturating_add(crate::DesignReplay::bytes(set, config, setup)),
214 channel: if mains == 0 {
215 0
216 } else {
217 spectra(positions + 4 + RC_SCRATCH_SPECTRA)
218 },
219 }
220 }
221}
222pub(crate) fn geomspace(lo: f64, hi: f64, n: usize) -> Vec<f64> {
223 let start = lo.log10();
224 let step = (hi.log10() - start) / (n - 1) as f64;
225 let mut out: Vec<_> = (0..n)
226 .map(|i| 10f64.powf(start + i as f64 * step))
227 .collect();
228 out[0] = lo;
229 out[n - 1] = hi;
230 out
231}
232pub(crate) fn spl_db(h: &[C], grid: &[f64]) -> Vec<f64> {
233 let p: Vec<_> = h
234 .iter()
235 .map(|v| {
236 let a = v.re.hypot(v.im);
237 a * a
238 })
239 .collect();
240 let db: Vec<_> = smooth::smooth_power(design_freqs(), &p, Fraction::Scalar(24.0))
241 .expect("design grid")
242 .iter()
243 .map(|p| 10.0 * (p + 1e-30).log10())
244 .collect();
245 grid.iter()
246 .map(|&g| interp(g, design_freqs(), &db))
247 .collect()
248}
249
250pub fn design(
251 req: &DesignRequest<'_>,
252 ctx: &Context,
253 fitters: &FitterFactory<'_>,
254) -> Result<Design, EngineError> {
255 design_observed(req, ctx, fitters, &|_, _| {})
256}
257
258pub(crate) fn design_observed(
260 req: &DesignRequest<'_>,
261 ctx: &Context,
262 fitters: &FitterFactory<'_>,
263 observed: &(dyn Fn(&str, std::time::Duration) + Sync),
264) -> Result<Design, EngineError> {
265 check(ctx)?;
266 check_taps(req.config)?;
267 let prep_started = std::time::Instant::now();
268 let config = req.config;
269 let setup = Setup::new(config);
270 let bytes = DesignBytes::new(req.measurements, config, &setup, req.subs_only);
274 let held =
275 measurement::preparation_bytes(req.measurements, &setup).saturating_add(bytes.retained);
276 let _permit = crate::memory::reserve(ctx, held, bytes.channel)?;
277 let prepared = measurement::prepare_until(req.measurements, config, &setup, &|| {
278 ctx.cancel().is_cancelled()
279 })?;
280 observed("prepare", prep_started.elapsed());
281 design_prepared_scored(
282 req,
283 ctx,
284 fitters,
285 &prepared,
286 observed,
287 true,
288 ChannelBytes {
289 held,
290 channel: bytes.channel,
291 },
292 )
293}
294
295pub(crate) fn design_prepared(
301 req: &DesignRequest<'_>,
302 ctx: &Context,
303 fitters: &FitterFactory<'_>,
304 prepared: &measurement::Prepared,
305 observed: &(dyn Fn(&str, std::time::Duration) + Sync),
306) -> Result<Design, EngineError> {
307 let bytes = DesignBytes::new(
308 req.measurements,
309 req.config,
310 &Setup::new(req.config),
311 req.subs_only,
312 );
313 let _permit = crate::memory::reserve(ctx, bytes.retained, bytes.channel)?;
314 design_prepared_scored(
315 req,
316 ctx,
317 fitters,
318 prepared,
319 observed,
320 true,
321 ChannelBytes {
322 held: bytes.retained,
323 channel: bytes.channel,
324 },
325 )
326}
327
328pub(crate) fn design_prepared_for_xo(
333 req: &DesignRequest<'_>,
334 ctx: &Context,
335 fitters: &FitterFactory<'_>,
336 prepared: &measurement::Prepared,
337) -> Result<Design, EngineError> {
338 design_prepared_scored(
339 req,
340 ctx,
341 fitters,
342 prepared,
343 &|_, _| {},
344 false,
345 ChannelBytes::RESERVED,
346 )
347}
348
349#[derive(Debug, Clone, Copy)]
352struct ChannelBytes {
353 held: u64,
354 channel: u64,
355}
356
357impl ChannelBytes {
358 const RESERVED: Self = Self {
360 held: 0,
361 channel: 0,
362 };
363}
364
365fn design_prepared_scored(
366 req: &DesignRequest<'_>,
367 ctx: &Context,
368 fitters: &FitterFactory<'_>,
369 prepared: &measurement::Prepared,
370 observed: &(dyn Fn(&str, std::time::Duration) + Sync),
371 include_scores: bool,
372 reserve: ChannelBytes,
373) -> Result<Design, EngineError> {
374 check(ctx)?;
375 check_taps(req.config)?;
376 let config = req.config;
377 let mut setup = Setup::new(config);
378 let positions = prepared.h.positions().to_vec();
379 let progress = ctx.stage("engine.design", 1 + config.mains.len() as u64);
380 let mut notes = Vec::new();
381 if let Some(from_ms) = setup.lift_capped_from_ms {
382 notes.push(Note::LiftCapped {
383 from_ms,
384 taps: config.taps,
385 lift_ms: setup.lift_ms,
386 });
387 }
388 if !setup.sub_filters.is_empty() {
389 notes.push(Note::SubFilters(setup.sub_filters.clone()));
390 }
391 let rc_auto = setup.rc_hi_manual.is_none();
392 let schroeder_hz = rc_auto
393 .then(|| levels::schroeder_hz(req.measurements, config))
394 .flatten();
395 if rc_auto {
396 setup.rc_hi = schroeder_hz.map_or(RC_AUTO_MAX_HZ, |f| f.clamp(100.0, RC_AUTO_MAX_HZ));
397 }
398 notes.push(Note::RcLimit {
399 hz: setup.rc_hi,
400 auto: rc_auto,
401 schroeder_hz,
402 });
403 let xo_of: Vec<_> = config
404 .groups
405 .iter()
406 .filter(|(g, _)| g != "sub")
407 .flat_map(|(g, roles)| {
408 roles.iter().filter_map(move |r| {
409 config
410 .xo
411 .iter()
412 .find(|(name, x)| name == g && *x != 0.0 && !config.subs.is_empty())
413 .map(|(_, x)| (r.clone(), *x))
414 })
415 })
416 .collect();
417 let weights = levels::weights(&positions, config.main_position);
418 let levels: Vec<_> = config
419 .mains
420 .iter()
421 .filter(|r| prepared.h.get(config.main_position, r).is_some())
422 .map(|r| {
423 (
424 r.clone(),
425 levels::band_level(&prepared.h, r, &weights, &config.target, 300.0, 3000.0),
426 )
427 })
428 .collect();
429 let ref_level = levels
430 .iter()
431 .map(|(_, v)| *v)
432 .reduce(f64::min)
433 .ok_or_else(|| EngineError::Input("no measured main".into()))?;
434 let input = BaInput {
435 measurements: req.measurements,
436 prepared,
437 setup: &setup,
438 config,
439 tuning: req.tuning,
440 xo_of: &xo_of,
441 ref_level,
442 cancel: ctx.cancel(),
443 };
444 let ba_started = std::time::Instant::now();
445 let mut ba_fitter = fitters(FitStage::Ba);
446 let alignment = ba::align(&input, &mut *ba_fitter)?;
447 check(ctx)?;
448 #[cfg(test)]
449 let mut timings = vec![("BA".into(), ba_started.elapsed())];
450 notes.extend(alignment.notes.clone());
451 progress.advance(1);
452 let ba_elapsed = ba_started.elapsed();
453 let mut used = prepared.h.clone();
454 for (p, r, h) in &alignment.h_subs {
455 used.set(*p, r, h.clone());
456 }
457 let top_started = std::time::Instant::now();
458 let mut top_peq = Vec::new();
459 let mut top_responses = Vec::new();
460 if !config.subs.is_empty() && req.tuning.xo_stage {
461 for (role, xo) in &xo_of {
462 check(ctx)?;
463 let lo = if config.xo_peq_lo_hz == 0.0 {
464 xo / 2.0
465 } else {
466 config.xo_peq_lo_hz
467 };
468 let hi = if config.xo_peq_hi_hz == 0.0 {
469 xo * 4.0
470 } else {
471 config.xo_peq_hi_hz
472 };
473 if !config.xo_peq_on || hi <= lo {
474 continue;
475 }
476 let level = levels
477 .iter()
478 .find(|(r, _)| r == role)
479 .ok_or_else(|| EngineError::Input(format!("XO role {role} missing at reference")))?
480 .1;
481 let gain = C::new(10f64.powf((ref_level - level) / 20.0), 0.0);
482 let by_pos: Vec<_> = positions
483 .iter()
484 .zip(&weights)
485 .filter_map(|(&p, &w)| {
486 used.get(p, role)
487 .map(|h| (w, h.iter().map(|v| v * gain).collect::<Vec<_>>()))
488 })
489 .collect();
490 let weighted: Vec<_> = by_pos.iter().map(|(w, h)| (*w, h.as_slice())).collect();
491 let peq = fit::xo_target_peq(
492 &weighted,
493 &config.target,
494 ref_level,
495 lo,
496 hi,
497 6,
498 config.max_boost_db.min(6.0),
499 Some(hi),
500 &mut *fitters(FitStage::Top(role)),
501 );
502 check(ctx)?;
503 let response: Arc<[C]> = response::peq_response(design_freqs(), &peq).into();
504 for &p in &positions {
505 if let Some(h) = used.get(p, role) {
506 used.set(
507 p,
508 role,
509 h.iter().zip(response.iter()).map(|(h, p)| h * p).collect(),
510 );
511 }
512 }
513 top_responses.push((role.clone(), response));
514 top_peq.push((role.clone(), peq));
515 }
516 }
517 #[cfg(test)]
518 timings.push(("top PEQ".into(), top_started.elapsed()));
519 observed("top", top_started.elapsed());
520 let bus_started = std::time::Instant::now();
521 let (bus_delay_ms, bus_polarity) = if config.subs.is_empty() {
522 (0.0, 1)
523 } else {
524 let earliest = alignment
525 .rows
526 .iter()
527 .map(|r| r.delay_ms)
528 .fold(f64::INFINITY, f64::min);
529 let lo = (-(setup.max_latency_ms - setup.lift_ms) - earliest) * 4.0;
530 let mains: Vec<_> = config
531 .mains
532 .iter()
533 .map(|r| {
534 (
535 r.clone(),
536 positions
537 .iter()
538 .filter(|p| alignment.sub_sum_by_pos.iter().any(|(q, _)| q == *p))
539 .filter_map(|&p| used.get(p, r).map(|h| (p, h.clone())))
540 .collect(),
541 )
542 })
543 .collect();
544 ba::bus_delay(
545 &alignment,
546 &mains,
547 &prepared.align_delay_ms,
548 &xo_of,
549 lo.ceil() / 4.0,
550 &|| ctx.cancel().is_cancelled(),
551 )?
552 };
553 check(ctx)?;
554 let rows = ba::fold(
555 &alignment.rows,
556 alignment.bus_gain,
557 bus_delay_ms,
558 bus_polarity,
559 );
560 observed("ba", ba_elapsed + bus_started.elapsed());
561 let shift = if alignment.plan.as_ref().is_some_and(|p| !p.fixed.is_empty()) {
562 config.sub_main_delay_ms
563 } else {
564 (-rows
565 .iter()
566 .map(|r| r.delay_ms)
567 .reduce(f64::min)
568 .unwrap_or(0.0))
569 .max(0.0)
570 };
571 let roles = if req.subs_only {
572 vec![]
573 } else {
574 config.mains.iter().collect()
575 };
576 let rc_all_started = std::time::Instant::now();
577 let outputs = crate::memory::try_map(
580 ctx,
581 roles,
582 reserve.held,
583 |_| reserve.channel,
584 |role| {
585 check(ctx)?;
586 let Some(before) = prepared.h0.get(config.main_position, role) else {
587 progress.advance(1);
588 return Ok(None);
589 };
590 let rc_started = std::time::Instant::now();
591 let gain_db = ref_level - levels.iter().find(|(r, _)| r == role).unwrap().1;
592 let xo_hz = xo_of.iter().find(|(r, _)| r == role).map(|(_, x)| *x);
593 let align_delay_ms = prepared
594 .align_delay_ms
595 .iter()
596 .find(|(r, _)| r == role)
597 .unwrap()
598 .1;
599 let comb: Vec<(u32, Arc<[C]>)> = positions
600 .iter()
601 .filter_map(|&p| {
602 used.get(p, role).map(|h| {
603 let sub = alignment
604 .sub_sum_by_pos
605 .iter()
606 .find(|(q, _)| *q == p)
607 .map(|(_, h)| h.as_ref());
608 (
609 p,
610 rc::combine(
611 h,
612 align_delay_ms,
613 gain_db,
614 xo_hz,
615 sub,
616 bus_polarity,
617 bus_delay_ms,
618 )
619 .into(),
620 )
621 })
622 })
623 .collect();
624 let mut fitter = fitters(FitStage::Rc(role));
625 let result = rc::channel(
626 &RcInput {
627 role,
628 comb: &comb,
629 main_position: config.main_position,
630 level_db: ref_level,
631 setup: &setup,
632 config,
633 tuning: req.tuning,
634 positions: &positions,
635 },
636 &mut *fitter,
637 &|| ctx.cancel().is_cancelled(),
638 )?;
639 check(ctx)?;
640 let combined = comb
641 .iter()
642 .find(|(p, _)| *p == config.main_position)
643 .unwrap()
644 .1
645 .clone();
646 let response: Arc<[C]> = result.response.into();
647 let after = combined
648 .iter()
649 .zip(response.iter())
650 .map(|(h, f)| h * f)
651 .collect();
652 let mut peq = result.out.peq;
653 peq.extend(result.gd_cuts);
654 let channel = MainChannel {
655 role: role.clone(),
656 input_peq: peq,
657 fir: result.out.fir,
658 gain_db,
659 delay_ms: align_delay_ms + shift,
660 xo_hz,
661 align_delay_ms,
662 top_peq: top_peq
663 .iter()
664 .find(|(r, _)| r == role)
665 .map_or_else(Vec::new, |(_, p)| p.clone()),
666 };
667 let main = MainDesign {
668 role: role.clone(),
669 gain_db,
670 xo_hz,
671 align_delay_ms,
672 top_response: top_responses
673 .iter()
674 .find(|(r, _)| r == role)
675 .map(|(_, p)| p.clone()),
676 response,
677 phase_strength: result.out.phase_strength,
678 phase_band: result.out.phase_band,
679 phase_frac_used: result.phase_frac_used,
680 before: before.clone(),
681 combined,
682 after,
683 };
684 observed(&format!("rc:{role}"), rc_started.elapsed());
685 progress.advance(1);
686 #[cfg(test)]
687 let elapsed = rc_started.elapsed();
688 #[cfg(not(test))]
689 let elapsed = ();
690 Ok(Some((channel, main, result.out.notes, elapsed)))
691 },
692 )?;
693 observed("rc_all", rc_all_started.elapsed());
694 let mut channels = Vec::new();
695 let mut mains = Vec::new();
696 for (channel, main, channel_notes, _elapsed) in outputs.into_iter().flatten() {
697 #[cfg(test)]
698 timings.push((format!("RC {}", main.role), _elapsed));
699 channels.push(channel);
700 mains.push(main);
701 notes.extend(channel_notes);
702 }
703 let latency_ms = setup.lift_ms + shift;
704 notes.push(Note::Latency {
705 latency_ms,
706 lift_ms: setup.lift_ms,
707 main_extra_ms: shift,
708 });
709 let mut subs = Vec::new();
710 for (role, row) in config.subs.iter().zip(&rows) {
711 let mut sub = SubChannel {
712 role: role.clone(),
713 gain_db: row.gain_db,
714 delay_ms: row.delay_ms + shift,
715 polarity: row.polarity,
716 allpass: row.allpass.clone(),
717 hp_hz: 0.0,
718 hp_type: BandType::Lr24,
719 lp_hz: 0.0,
720 lp_type: BandType::Lr24,
721 peq: vec![],
722 mode: ba::sub_mode_label(role, &setup, &alignment.layout_info),
723 };
724 if alignment
725 .plan
726 .as_ref()
727 .is_some_and(|p| p.fixed.contains(role))
728 {
729 let m = &setup.sub_manual.iter().find(|(r, _)| r == role).unwrap().1;
730 sub.gain_db = m.gain_db;
731 sub.delay_ms = m.delay_ms;
732 sub.polarity = if m.polarity < 0 { -1 } else { 1 };
733 sub.allpass = m.allpass.iter().copied().take(2).collect();
734 sub.mode = "fixed".into();
735 }
736 if let Some((_, f)) = alignment.sub_filters.iter().find(|(r, _)| r == role) {
737 sub.hp_hz = f.hp_hz;
738 sub.hp_type = f.hp_type;
739 sub.lp_hz = f.lp_hz;
740 sub.lp_type = f.lp_type;
741 sub.peq = f.peq.clone();
742 }
743 subs.push(sub);
744 }
745 let sub = if config.subs.is_empty() {
746 None
747 } else {
748 Some(SubDesign {
749 rows: config.subs.iter().cloned().zip(rows).collect(),
750 bus_gain: alignment.bus_gain,
751 bus_delay_ms,
752 bus_polarity,
753 bus_response: (!alignment.bus_peq.is_empty())
754 .then(|| response::peq_response(design_freqs(), &alignment.bus_peq).into()),
755 sum_at_reference: alignment
756 .sub_sum_by_pos
757 .iter()
758 .find(|(p, _)| *p == config.main_position)
759 .unwrap()
760 .1
761 .clone(),
762 sub_level_db: alignment.sub_level_db,
763 layout_info: alignment.layout_info,
764 plan: alignment.plan,
765 })
766 };
767 let f = design_freqs();
768 let target = config
769 .target
770 .sample(&f.iter().map(|f| f.max(1.0)).collect::<Vec<_>>());
771 let target_db: Arc<[f64]> = target.iter().map(|v| v + ref_level).collect();
772 let spl_grid = if include_scores {
773 geomspace(5.0, 20000.0, 1390)
774 } else {
775 Vec::new()
776 };
777 let gd_grid = if include_scores {
778 geomspace(5.0, 5000.0, 1070)
779 } else {
780 Vec::new()
781 };
782 let scores_started = std::time::Instant::now();
783 let scores = if include_scores {
784 crate::memory::try_map(
786 ctx,
787 mains.iter().collect(),
788 reserve.held,
789 |_| reserve.channel,
790 |main| {
791 check(ctx)?;
792 Ok(RoleScores {
793 role: main.role.clone(),
794 before: scorecard::metrics(&main.before, f, &target, (100.0, 400.0)),
795 after: scorecard::metrics(&main.after, f, &target, (100.0, 400.0)),
796 spl_before: spl_db(&main.before, &spl_grid),
797 spl_after: spl_db(&main.after, &spl_grid),
798 gd_before: response::gd_curve(
799 &main.before,
800 f,
801 &gd_grid,
802 Fraction::Scalar(24.0),
803 )?,
804 gd_after: response::gd_curve(&main.after, f, &gd_grid, Fraction::Scalar(24.0))?,
805 })
806 },
807 )?
808 } else {
809 Vec::new()
810 };
811 observed("scores", scores_started.elapsed());
812 #[cfg(test)]
813 timings.push(("scores".into(), scores_started.elapsed()));
814 notes.push(Note::PhaseStrength(
815 mains
816 .iter()
817 .map(|m| (m.role.clone(), m.phase_strength))
818 .collect(),
819 ));
820 notes.push(Note::PhaseBand(
821 mains
822 .iter()
823 .map(|m| (m.role.clone(), m.phase_band))
824 .collect(),
825 ));
826 check(ctx)?;
827 Ok(Design {
828 chain: SignalChain {
829 sample_rate: SR,
830 mains: channels,
831 subs,
832 sub_bus_peq: alignment.bus_peq,
833 main_extra_ms: shift,
834 lift_ms: setup.lift_ms,
835 latency_ms,
836 },
837 mains,
838 subs: sub,
839 ref_level,
840 rc_hi: setup.rc_hi,
841 rc_auto,
842 schroeder_hz,
843 target_display: spl_grid.iter().map(|&g| interp(g, f, &target_db)).collect(),
844 target_db,
845 spectra: DesignSpectra {
846 raw: Arc::new(prepared.h0.clone()),
847 used: Arc::new(used),
848 main_position: config.main_position,
849 positions,
850 },
851 notes,
852 adapter_notes: req.adapter_notes.to_vec(),
853 groups: config.groups.clone(),
854 target: config.target.clone(),
855 xo: config.xo.clone(),
856 scores,
857 replay: Arc::new(crate::DesignReplay::new(
858 req.measurements,
859 config,
860 setup,
861 prepared,
862 )),
863 #[cfg(test)]
864 timings,
865 })
866}