Skip to main content

secs_types/
chain.rs

1//! The designed DSP as one value: what the engine produces and what the
2//! response simulator (`secs-engine`) and the CamillaDSP writer
3//! (`secs-camilla`) read (docs/ARCHITECTURE.md §4, docs/ENGINE.md §2).
4//!
5//! Upstream spread this over `CorrectionResult.channels` / `.subs` and the
6//! engine attributes `top_peq`, `sub_bus_peq`, `align_delay_ms` and
7//! `main_extra_ms`, which `export_camilladsp.make_config` read directly.
8//!
9//! Delays here are on the **output axis**: what the DSP applies, the internal
10//! (spectral) axis plus [`SignalChain::main_extra_ms`].
11
12use serde::{Deserialize, Serialize};
13use ts_rs::TS;
14
15/// One peaking band `(frequency Hz, Q, gain dB)`, upstream's PEQ tuple.
16pub type Peq = (f64, f64, f64);
17/// One second-order all-pass `(frequency Hz, Q)`.
18pub type Allpass = (f64, f64);
19
20/// Crossover filter family (upstream `BAND_TYPES`).
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
22#[ts(export)]
23pub enum BandType {
24    /// Linkwitz–Riley 12 dB/oct.
25    #[serde(rename = "LR12")]
26    Lr12,
27    /// Linkwitz–Riley 24 dB/oct (the default).
28    #[serde(rename = "LR24")]
29    Lr24,
30    /// Linkwitz–Riley 48 dB/oct.
31    #[serde(rename = "LR48")]
32    Lr48,
33    /// Butterworth 12 dB/oct.
34    #[serde(rename = "BW12")]
35    Bw12,
36    /// Butterworth 24 dB/oct.
37    #[serde(rename = "BW24")]
38    Bw24,
39    /// Butterworth 48 dB/oct, only the common input high-pass (ADR 0031):
40    /// `from_name` never yields it, so no project or sub filter selects it.
41    #[serde(rename = "BW48")]
42    Bw48,
43}
44
45impl BandType {
46    /// Upstream name (`"LR24"` …).
47    pub fn name(self) -> &'static str {
48        match self {
49            Self::Lr12 => "LR12",
50            Self::Lr24 => "LR24",
51            Self::Lr48 => "LR48",
52            Self::Bw12 => "BW12",
53            Self::Bw24 => "BW24",
54            Self::Bw48 => "BW48",
55        }
56    }
57
58    /// The type named `name`, or `LR24` for anything else (upstream's fallback).
59    pub fn from_name(name: &str) -> Self {
60        match name {
61            "LR12" => Self::Lr12,
62            "LR48" => Self::Lr48,
63            "BW12" => Self::Bw12,
64            "BW24" => Self::Bw24,
65            _ => Self::Lr24,
66        }
67    }
68}
69
70/// A main channel: input correction before bass management, then the main
71/// output path.
72#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
73#[ts(export)]
74pub struct MainChannel {
75    /// Speaker role.
76    pub role: String,
77    /// Input PEQ, applied before the FIR (`ChannelOut.peq`).
78    pub input_peq: Vec<Peq>,
79    /// Input correction FIR at 48 kHz; `[1.0]` when the mode has no FIR.
80    pub fir: Vec<f64>,
81    /// Main output gain in dB.
82    pub gain_db: f64,
83    /// Main output delay in ms, output axis.
84    pub delay_ms: f64,
85    /// Bass-management crossover in Hz (HP on the main, LP on its sub send).
86    pub xo_hz: Option<f64>,
87    /// Acoustic-XO PEQ on the main output path (`engine.top_peq`).
88    pub top_peq: Vec<Peq>,
89    /// Arrival-alignment delay in ms, internal axis (`engine.align_delay_ms`);
90    /// MIMO paths delay by it plus `main_extra_ms`.
91    pub align_delay_ms: f64,
92}
93
94/// A sub output fed by the sub bus.
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
96#[ts(export)]
97pub struct SubChannel {
98    /// Speaker role.
99    pub role: String,
100    /// Bus-to-sub gain in dB.
101    pub gain_db: f64,
102    /// Output delay in ms, output axis.
103    pub delay_ms: f64,
104    /// Polarity, `1` or `-1`.
105    pub polarity: i8,
106    /// All-passes, in order.
107    pub allpass: Vec<Allpass>,
108    /// Fixed high-pass in Hz, 0 for none.
109    pub hp_hz: f64,
110    /// High-pass family.
111    pub hp_type: BandType,
112    /// Fixed low-pass in Hz, 0 for none.
113    pub lp_hz: f64,
114    /// Low-pass family.
115    pub lp_type: BandType,
116    /// Fixed output PEQ, then generated bundle PEQ bands.
117    pub peq: Vec<Peq>,
118    /// How BA treated the sub, upstream's label: `auto`, `fixed`,
119    /// `bundle:A`, `bundle:A:multi`, `bundle:A:dba-front`, `bundle:A:dba-rear`.
120    pub mode: String,
121}
122
123/// The whole designed DSP.
124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
125#[ts(export)]
126pub struct SignalChain {
127    /// Sample rate the coefficients are for (48 kHz).
128    pub sample_rate: u32,
129    /// Main channels in design order (`CorrectionResult.channels`).
130    pub mains: Vec<MainChannel>,
131    /// Subs in configuration order (`CorrectionResult.subs`).
132    pub subs: Vec<SubChannel>,
133    /// PEQ on the summed sub bus, before the per-sub output branches.
134    pub sub_bus_peq: Vec<Peq>,
135    /// Common shift from the internal to the output axis in ms
136    /// (`engine.main_extra_ms`).
137    pub main_extra_ms: f64,
138    /// Excess-phase lift of the input FIRs in ms.
139    pub lift_ms: f64,
140    /// Total filter latency in ms: lift plus `main_extra_ms`.
141    pub latency_ms: f64,
142    /// Common 8th-order Butterworth high-pass in Hz on every input, LFE
143    /// included, before input correction and the MIMO fork (ADR 0031).
144    /// Absent when off, so an off chain serializes and hashes as before.
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    #[ts(optional)]
147    pub input_hp_hz: Option<f64>,
148}
149
150/// What a MIMO support path filters with (upstream `mimo_paths`' filter slot).
151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
152#[ts(export)]
153pub enum MimoFilter {
154    /// IIR support (`mimo_iir.IirSupport`): gain, polarity, integer-sample
155    /// delay, peaking bands and all-passes, then the path's LR4 high-pass at
156    /// [`MimoPath::low_hz`] and LR4 low-pass at [`MimoPaths::fsiso`].
157    Iir {
158        /// Path gain in dB.
159        gain_db: f64,
160        /// Path delay in ms, added to [`MimoPath::delay_ms`].
161        delay_ms: f64,
162        /// Polarity, `1` or `-1`.
163        polarity: i8,
164        /// Peaking bands, in order.
165        peq: Vec<Peq>,
166        /// All-passes, in order.
167        allpass: Vec<Allpass>,
168    },
169    /// FIR support: the low-rate ideal FIR raised to 48 kHz
170    /// (`export_camilladsp.fir_to_48k`); the Fsiso low-pass is inside it.
171    Fir {
172        /// Taps at 48 kHz.
173        taps_48k: Vec<f64>,
174    },
175    /// FIR support retained at its design rate. Exporters materialize the
176    /// historical 48 kHz representation only when writing convolution files.
177    /// The isolation low-pass is already included in these taps.
178    FirLowRate {
179        /// Design-rate coefficients, including the isolation low-pass.
180        taps_low: Vec<f64>,
181        /// Design sample rate in hertz.
182        fs_low: f64,
183    },
184}
185
186impl MimoFilter {
187    /// Whether this support uses a finite impulse response.
188    pub fn is_fir(&self) -> bool {
189        matches!(self, Self::Fir { .. } | Self::FirLowRate { .. })
190    }
191
192    /// Number of coefficients in the compatible 48 kHz export representation.
193    pub fn fir_len_48k(&self) -> usize {
194        match self {
195            Self::Iir { .. } => 0,
196            Self::Fir { taps_48k } => taps_48k.len(),
197            Self::FirLowRate { taps_low, fs_low } => {
198                if !fs_low.is_finite() || *fs_low <= 0.0 || taps_low.is_empty() {
199                    0
200                } else {
201                    (taps_low.len() as f64 * (48_000.0 / fs_low)).ceil() as usize
202                }
203            }
204        }
205    }
206}
207
208/// One MIMO support path: input `input` also drives speaker `support`.
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
210#[ts(export)]
211pub struct MimoPath {
212    /// Input role (the channel whose raw signal the path copies).
213    pub input: String,
214    /// Support speaker role (the output the path adds to).
215    pub support: String,
216    /// The path filter.
217    pub filter: MimoFilter,
218    /// Protective LR4 high-pass in Hz: the support's F-support low edge,
219    /// 50 Hz when the speaker has no range (upstream fallback).
220    pub low_hz: f64,
221    /// The support speaker's delay in ms, output axis: its alignment delay
222    /// plus [`SignalChain::main_extra_ms`].
223    pub delay_ms: f64,
224}
225
226/// Every MIMO support path, in upstream order (input, then support).
227#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
228#[ts(export)]
229pub struct MimoPaths {
230    /// Isolation frequency in Hz (the IIR paths' LR4 low-pass).
231    pub fsiso: f64,
232    /// The paths.
233    pub paths: Vec<MimoPath>,
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn input_hp_is_absent_from_an_off_chain_and_round_trips_when_set() {
242        let mut chain = SignalChain {
243            sample_rate: 48_000,
244            mains: Vec::new(),
245            subs: Vec::new(),
246            sub_bus_peq: Vec::new(),
247            main_extra_ms: 0.0,
248            lift_ms: 0.0,
249            latency_ms: 0.0,
250            input_hp_hz: None,
251        };
252        let off = serde_json::to_value(&chain).unwrap();
253        assert!(off.get("input_hp_hz").is_none());
254        assert_eq!(serde_json::from_value::<SignalChain>(off).unwrap(), chain);
255        chain.input_hp_hz = Some(27.0);
256        let on = serde_json::to_value(&chain).unwrap();
257        assert_eq!(on["input_hp_hz"], 27.0);
258        assert_eq!(serde_json::from_value::<SignalChain>(on).unwrap(), chain);
259    }
260}