Skip to main content

secs_engine/
export_view.rs

1//! Narrow derived state used by response exports.
2
3use crate::{EngineConfig, EngineError, MeasurementSet, Spectra, measurement, setup::SubFilter};
4
5/// Long-window spectra and timing on the engine's internal axis.
6#[derive(Debug)]
7pub struct ExportView {
8    pub spectra: Spectra,
9    pub base_s: f64,
10    pub has_sub_filters: bool,
11}
12
13/// Rebuilds the 2 s spectra, reusing the design's preparation for matching inputs.
14/// New same-input callers use `design.replay.export_view(design)` directly.
15pub fn export_view(
16    design: &crate::Design,
17    measurements: &MeasurementSet,
18    config: &EngineConfig,
19) -> Result<ExportView, EngineError> {
20    if design.replay.matches(measurements, config) {
21        design.replay.export_view(design)
22    } else {
23        let prepared =
24            measurement::prepare(measurements, config, &crate::setup::Setup::new(config))?;
25        export_view_prepared(design, measurements, &prepared, &|| false)
26    }
27}
28
29/// [`export_view`] on measurements already prepared with the same config;
30/// `Err(Cancelled)` once `cancelled()` reads true while the spectra are built.
31pub(crate) fn export_view_prepared(
32    design: &crate::Design,
33    measurements: &MeasurementSet,
34    prepared: &measurement::Prepared,
35    cancelled: &dyn Fn() -> bool,
36) -> Result<ExportView, EngineError> {
37    let sub_filters: Vec<_> = design
38        .chain
39        .subs
40        .iter()
41        .filter(|sub| sub.hp_hz != 0.0 || sub.lp_hz != 0.0 || !sub.peq.is_empty())
42        .map(|sub| {
43            (
44                sub.role.clone(),
45                SubFilter {
46                    hp_hz: sub.hp_hz,
47                    hp_type: sub.hp_type,
48                    lp_hz: sub.lp_hz,
49                    lp_type: sub.lp_type,
50                    peq: sub.peq.clone(),
51                },
52            )
53        })
54        .collect();
55    let mut spectra =
56        measurement::long_spectra_until(measurements, prepared, &sub_filters, 2000.0, cancelled)?;
57    if let Some(hz) = design.chain.input_hp_hz {
58        let highpass =
59            secs_dsp::response::band_filter(crate::grid::design_freqs(), hz, "HP", "BW48")?;
60        for position in spectra.positions().to_vec() {
61            let roles: Vec<_> = spectra.roles_at(position).map(str::to_owned).collect();
62            for role in roles {
63                if cancelled() {
64                    return Err(EngineError::Cancelled);
65                }
66                let response = spectra.get(position, &role).expect("spectrum role");
67                let filtered = response
68                    .iter()
69                    .zip(&highpass)
70                    .map(|(h, hp)| h * hp)
71                    .collect();
72                spectra.set(position, &role, filtered);
73            }
74        }
75    }
76    Ok(ExportView {
77        spectra,
78        base_s: prepared.base_s,
79        // The export tail must not reuse unfiltered design spectra (ADR 0031).
80        has_sub_filters: !sub_filters.is_empty() || design.chain.input_hp_hz.is_some(),
81    })
82}