Skip to main content

secs_engine/
replay.rs

1//! The inputs and preparation retained by an immutable design.
2
3use std::sync::Arc;
4
5use crate::{
6    Design, EngineConfig, EngineError, ExportView, MeasurementSet, measurement::Prepared,
7    setup::Setup,
8};
9
10/// Time origin for a REW export item. Corrected responses retain SECS 2's
11/// latency subtraction (D24 is deliberately not changed here).
12#[derive(Debug, Clone, Copy)]
13pub enum ExportAxis {
14    Measurement,
15    Corrected,
16}
17
18/// What a design was made from. Sample buffers and prepared spectra are shared,
19/// never copied. Long-window spectra are computed on request, never cached.
20#[derive(Debug)]
21pub struct DesignReplay {
22    measurements: Arc<MeasurementSet>,
23    config: EngineConfig,
24    setup: Setup,
25    prepared: Prepared,
26}
27
28impl DesignReplay {
29    pub(crate) fn new(
30        measurements: &MeasurementSet,
31        config: &EngineConfig,
32        setup: Setup,
33        prepared: &Prepared,
34    ) -> Self {
35        Self {
36            measurements: Arc::new(measurements.clone()),
37            config: config.clone(),
38            setup,
39            prepared: prepared.clone(),
40        }
41    }
42
43    pub fn measurements(&self) -> &MeasurementSet {
44        &self.measurements
45    }
46
47    pub fn config(&self) -> &EngineConfig {
48        &self.config
49    }
50
51    pub fn base_s(&self) -> f64 {
52        self.prepared.base_s
53    }
54
55    /// Rebuilds the 2 s spectra used by SECS 2's REW exports.
56    /// `design` must be the design that owns this replay.
57    pub fn export_view(&self, design: &Design) -> Result<ExportView, EngineError> {
58        self.export_view_until(design, &|| false)
59    }
60
61    /// Converts a design-grid export response to REW samples and its time origin.
62    /// The spectrum comes from this replay's export view, stage responses, or
63    /// applied MIMO response. Keeping that view at the caller avoids rebuilding
64    /// the long window for every role and stage. `design` must own this replay.
65    pub fn export_ir(
66        &self,
67        design: &Design,
68        spectrum: &[crate::C],
69        axis: ExportAxis,
70    ) -> (Vec<f64>, f64) {
71        let samples = secs_dsp::fft::irfft(spectrum, crate::grid::N_FFT)
72            .into_iter()
73            .map(|sample| f64::from(sample as f32))
74            .collect();
75        let t0 = match axis {
76            ExportAxis::Measurement => self.base_s(),
77            ExportAxis::Corrected => self.base_s() - design.chain.latency_ms / 1000.0,
78        };
79        (samples, t0)
80    }
81
82    /// Applies cached IIR, full-rate FIR or low-rate FIR support paths to the
83    /// long-window responses. `design` must be the design that owns this replay.
84    pub fn mimo_responses(
85        &self,
86        design: &Design,
87        paths: &secs_types::chain::MimoPaths,
88    ) -> Result<Vec<(String, Vec<crate::C>)>, EngineError> {
89        crate::replay_mimo::responses(design, &self.export_view(design)?, paths)
90    }
91
92    pub(crate) fn export_view_until(
93        &self,
94        design: &Design,
95        cancelled: &dyn Fn() -> bool,
96    ) -> Result<ExportView, EngineError> {
97        crate::export_view::export_view_prepared(
98            design,
99            self.measurements(),
100            &self.prepared,
101            cancelled,
102        )
103    }
104
105    /// Legacy callers may supply a different input. Comparing shared storage
106    /// avoids scanning samples; independently loaded equivalent inputs safely
107    /// take the fresh-preparation path.
108    pub fn matches(&self, set: &MeasurementSet, config: &EngineConfig) -> bool {
109        self.config == *config
110            && self.measurements.positions.len() == set.positions.len()
111            && self
112                .measurements
113                .positions
114                .iter()
115                .zip(&set.positions)
116                .all(|((p, a), (q, b))| {
117                    p == q
118                        && a.len() == b.len()
119                        && a.iter().zip(b).all(|((r, a), (s, b))| {
120                            r == s
121                                && a.sample_rate == b.sample_rate
122                                && a.t0.to_bits() == b.t0.to_bits()
123                                && match (&a.samples, &b.samples) {
124                                    (crate::Samples::F32(a), crate::Samples::F32(b)) => {
125                                        Arc::ptr_eq(a, b)
126                                    }
127                                    (crate::Samples::F64(a), crate::Samples::F64(b)) => {
128                                        Arc::ptr_eq(a, b)
129                                    }
130                                    _ => false,
131                                }
132                        })
133                })
134    }
135
136    pub(crate) fn prepared(&self) -> &Prepared {
137        &self.prepared
138    }
139
140    pub(crate) fn setup(&self) -> &Setup {
141        &self.setup
142    }
143
144    /// Owned metadata added to the TD-24 reservation. Samples and spectrum
145    /// buffers are Arc-shared with the input and the already-counted preparation.
146    /// Count cloned containers and strings, with room for their allocation headers.
147    pub(crate) fn bytes(set: &MeasurementSet, config: &EngineConfig, setup: &Setup) -> u64 {
148        fn strings(values: &[String]) -> usize {
149            values.iter().map(|s| 32 + s.len()).sum()
150        }
151        fn manual(values: &[(String, crate::input::SubManual)]) -> usize {
152            values
153                .iter()
154                .map(|(r, m)| {
155                    256 + r.len() + m.bundle.len() + m.allpass.len() * 16 + m.peq.len() * 24
156                })
157                .sum()
158        }
159        let measurements: usize = set
160            .positions
161            .iter()
162            .map(|(_, roles)| {
163                // Measurement metadata, two cloned Spectra cell tables, and timing
164                // entries (only the reference has timing, so all cells overcount it).
165                128 + roles.iter().map(|(r, _)| 320 + 5 * r.len()).sum::<usize>()
166            })
167            .sum();
168        let config_bytes = strings(&config.mains)
169            + strings(&config.subs)
170            + config
171                .groups
172                .iter()
173                .map(|(g, rs)| 64 + g.len() + strings(rs))
174                .sum::<usize>()
175            + config.target.points.len() * 16
176            + config.xo.iter().map(|(g, _)| 48 + g.len()).sum::<usize>()
177            + manual(&config.sub_manual)
178            + config
179                .bundle_layouts
180                .iter()
181                .map(|(g, _)| 64 + g.len())
182                .sum::<usize>();
183        let setup_bytes = manual(&setup.sub_manual)
184            + setup
185                .sub_filters
186                .iter()
187                .map(|(r, f)| 96 + r.len() + f.peq.len() * 24)
188                .sum::<usize>()
189            + setup
190                .layouts
191                .iter()
192                .map(|(g, l)| 80 + g.len() + strings(&l.members))
193                .sum::<usize>();
194        (std::mem::size_of::<Self>() + 128 + measurements + config_bytes + setup_bytes) as u64
195    }
196}