Skip to main content

secs_engine/
chain.rs

1//! The response simulator over the signal-chain value: replays any stage of
2//! the designed DSP on measured spectra.
3//!
4//! Upstream: `CorrectionEngine.stage_responses`, `input_response`,
5//! `calibration._sub_out`, `xo_preview`, `sub_combine`, `stage_items`.
6
7use crate::C;
8use crate::input::Allpass;
9use crate::measurement::ramp_phase;
10use secs_dsp::response::allpass2;
11
12/// One sub's BA row: upstream's `(gain dB, delay ms, polarity, all-passes)`.
13#[derive(Debug, Clone, PartialEq)]
14pub struct BaRow {
15    /// Gain in dB.
16    pub gain_db: f64,
17    /// Delay in ms, internal axis.
18    pub delay_ms: f64,
19    /// Polarity, `1` or `-1`.
20    pub polarity: i8,
21    /// All-passes, in order.
22    pub allpass: Vec<Allpass>,
23}
24
25/// One sub's output term, `_term`: `h · 10^(g/20) · polarity · delay · Π all-pass`
26/// on the frequencies `f` (`h` and `f` have the same length: the design grid
27/// or a subset of it). Each factor is a complex multiplication in NumPy's
28/// order and formula (a real factor is the complex `(x, 0)`).
29pub fn term(h: &[C], f: &[f64], row: &BaRow) -> Vec<C> {
30    PreparedBaTerm::new(f, row).apply(h)
31}
32
33/// Candidate-dependent factors shared by all measurement positions. Keep the
34/// factors separate: pre-multiplying them would change the floating-point order.
35pub(crate) struct PreparedBaTerm {
36    gain: C,
37    polarity: C,
38    delay: Vec<C>,
39    allpass: Vec<Vec<C>>,
40}
41
42impl PreparedBaTerm {
43    pub(crate) fn new(f: &[f64], row: &BaRow) -> Self {
44        Self {
45            gain: C::new(10f64.powf(row.gain_db / 20.0), 0.0),
46            polarity: C::new(f64::from(row.polarity), 0.0),
47            delay: f
48                .iter()
49                .map(|&f| C::from_polar(1.0, ramp_phase(f, row.delay_ms) * 0.001))
50                .collect(),
51            allpass: row
52                .allpass
53                .iter()
54                .map(|&(f0, q)| allpass2(f, f0, q))
55                .collect(),
56        }
57    }
58
59    fn apply(&self, h: &[C]) -> Vec<C> {
60        let mut out: Vec<_> = h
61            .iter()
62            .zip(&self.delay)
63            .map(|(&h, &delay)| h * self.gain * self.polarity * delay)
64            .collect();
65        for factor in &self.allpass {
66            for (v, &a) in out.iter_mut().zip(factor) {
67                *v *= a;
68            }
69        }
70        out
71    }
72}
73
74pub(crate) fn sub_sum_prepared(hs: &[Vec<C>], terms: &[PreparedBaTerm], bins: usize) -> Vec<C> {
75    let mut sum = vec![C::new(0.0, 0.0); bins];
76    for (h, term) in hs.iter().zip(terms) {
77        for (s, t) in sum.iter_mut().zip(term.apply(h)) {
78            *s += t;
79        }
80    }
81    sum
82}
83
84/// `_sub_sum` at one position: the sum of the subs' terms in sub order,
85/// starting, as upstream, from the integer 0 (so a zero first term is +0).
86pub fn sub_sum(terms: &[(&[C], &BaRow)], f: &[f64]) -> Vec<C> {
87    let mut sum = vec![C::new(0.0, 0.0); f.len()];
88    for (h, row) in terms {
89        for (s, t) in sum.iter_mut().zip(term(h, f, row)) {
90            *s += t;
91        }
92    }
93    sum
94}
95
96#[derive(Debug)]
97pub struct StageResponses {
98    pub top_xo: Vec<C>,
99    pub sub_xo: Option<Vec<C>>,
100    pub combined: Vec<C>,
101    pub output: Vec<C>,
102}
103
104/// Replays upstream's internal axis without changing the design's spectra.
105/// Supplied spectra already contain fixed and generated bundle sub filters,
106/// as do upstream's `long_spectra` results.
107pub fn stage_responses(
108    design: &crate::design::Design,
109    role: &str,
110    spectra: &crate::measurement::Spectra,
111    position: u32,
112) -> Option<StageResponses> {
113    use crate::{
114        grid::{BINS, design_freqs},
115        measurement::delay_response,
116    };
117    use secs_dsp::response::lr4;
118    let main = design.mains.iter().find(|m| m.role == role)?;
119    let h = spectra.get(position, role)?;
120    if h.len() != BINS {
121        return None;
122    }
123    let gain = C::new(10f64.powf(main.gain_db / 20.0), 0.0);
124    let mut top: Vec<_> = h
125        .iter()
126        .zip(delay_response(main.align_delay_ms))
127        .map(|(h, d)| h * d * gain)
128        .collect();
129    if let Some(p) = &main.top_response {
130        for (h, p) in top.iter_mut().zip(p.iter()) {
131            *h *= p;
132        }
133    }
134    let sub_xo = if let (Some(xo), Some(sub)) = (main.xo_hz.filter(|x| *x != 0.0), &design.subs) {
135        let terms: Option<Vec<_>> = sub
136            .rows
137            .iter()
138            .map(|(r, row)| {
139                let h = spectra.get(position, r)?;
140                (h.len() == BINS).then_some((h.as_ref(), row))
141            })
142            .collect();
143        let mut sum = sub_sum(&terms?, design_freqs());
144        if let Some(p) = &sub.bus_response {
145            for (h, p) in sum.iter_mut().zip(p.iter()) {
146                *h *= p;
147            }
148        }
149        for (h, p) in top.iter_mut().zip(lr4(design_freqs(), xo, "HP")) {
150            *h *= p;
151        }
152        for (h, p) in sum.iter_mut().zip(lr4(design_freqs(), xo, "LP")) {
153            *h *= p;
154        }
155        Some(sum)
156    } else {
157        None
158    };
159    let combined: Vec<_> = match &sub_xo {
160        Some(sub) => top.iter().zip(sub).map(|(m, s)| m + s).collect(),
161        None => top.clone(),
162    };
163    let output = combined
164        .iter()
165        .zip(main.response.iter())
166        .map(|(h, f)| h * f)
167        .collect();
168    Some(StageResponses {
169        top_xo: top,
170        sub_xo,
171        combined,
172        output,
173    })
174}
175
176pub fn input_response(
177    design: &crate::design::Design,
178    role: &str,
179    spectra: &crate::measurement::Spectra,
180    position: u32,
181) -> Option<Vec<C>> {
182    stage_responses(design, role, spectra, position).map(|s| s.output)
183}
184
185/// `h[idx]`: the bins of a design-grid spectrum at `idx`.
186pub fn gather<T: Copy>(h: &[T], idx: &[usize]) -> Vec<T> {
187    idx.iter().map(|&i| h[i]).collect()
188}