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}
40
41impl BandType {
42 /// Upstream name (`"LR24"` …).
43 pub fn name(self) -> &'static str {
44 match self {
45 Self::Lr12 => "LR12",
46 Self::Lr24 => "LR24",
47 Self::Lr48 => "LR48",
48 Self::Bw12 => "BW12",
49 Self::Bw24 => "BW24",
50 }
51 }
52
53 /// The type named `name`, or `LR24` for anything else (upstream's fallback).
54 pub fn from_name(name: &str) -> Self {
55 match name {
56 "LR12" => Self::Lr12,
57 "LR48" => Self::Lr48,
58 "BW12" => Self::Bw12,
59 "BW24" => Self::Bw24,
60 _ => Self::Lr24,
61 }
62 }
63}
64
65/// A main channel: input correction before bass management, then the main
66/// output path.
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
68#[ts(export)]
69pub struct MainChannel {
70 /// Speaker role.
71 pub role: String,
72 /// Input PEQ, applied before the FIR (`ChannelOut.peq`).
73 pub input_peq: Vec<Peq>,
74 /// Input correction FIR at 48 kHz; `[1.0]` when the mode has no FIR.
75 pub fir: Vec<f64>,
76 /// Main output gain in dB.
77 pub gain_db: f64,
78 /// Main output delay in ms, output axis.
79 pub delay_ms: f64,
80 /// Bass-management crossover in Hz (HP on the main, LP on its sub send).
81 pub xo_hz: Option<f64>,
82 /// Acoustic-XO PEQ on the main output path (`engine.top_peq`).
83 pub top_peq: Vec<Peq>,
84 /// Arrival-alignment delay in ms, internal axis (`engine.align_delay_ms`);
85 /// MIMO paths delay by it plus `main_extra_ms`.
86 pub align_delay_ms: f64,
87}
88
89/// A sub output fed by the sub bus.
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
91#[ts(export)]
92pub struct SubChannel {
93 /// Speaker role.
94 pub role: String,
95 /// Bus-to-sub gain in dB.
96 pub gain_db: f64,
97 /// Output delay in ms, output axis.
98 pub delay_ms: f64,
99 /// Polarity, `1` or `-1`.
100 pub polarity: i8,
101 /// All-passes, in order.
102 pub allpass: Vec<Allpass>,
103 /// Fixed high-pass in Hz, 0 for none.
104 pub hp_hz: f64,
105 /// High-pass family.
106 pub hp_type: BandType,
107 /// Fixed low-pass in Hz, 0 for none.
108 pub lp_hz: f64,
109 /// Low-pass family.
110 pub lp_type: BandType,
111 /// Fixed output PEQ, then generated bundle PEQ bands.
112 pub peq: Vec<Peq>,
113 /// How BA treated the sub, upstream's label: `auto`, `fixed`,
114 /// `bundle:A`, `bundle:A:multi`, `bundle:A:dba-front`, `bundle:A:dba-rear`.
115 pub mode: String,
116}
117
118/// The whole designed DSP.
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
120#[ts(export)]
121pub struct SignalChain {
122 /// Sample rate the coefficients are for (48 kHz).
123 pub sample_rate: u32,
124 /// Main channels in design order (`CorrectionResult.channels`).
125 pub mains: Vec<MainChannel>,
126 /// Subs in configuration order (`CorrectionResult.subs`).
127 pub subs: Vec<SubChannel>,
128 /// PEQ on the summed sub bus, before the per-sub output branches.
129 pub sub_bus_peq: Vec<Peq>,
130 /// Common shift from the internal to the output axis in ms
131 /// (`engine.main_extra_ms`).
132 pub main_extra_ms: f64,
133 /// Excess-phase lift of the input FIRs in ms.
134 pub lift_ms: f64,
135 /// Total filter latency in ms: lift plus `main_extra_ms`.
136 pub latency_ms: f64,
137}
138
139/// What a MIMO support path filters with (upstream `mimo_paths`' filter slot).
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
141#[ts(export)]
142pub enum MimoFilter {
143 /// IIR support (`mimo_iir.IirSupport`): gain, polarity, integer-sample
144 /// delay, peaking bands and all-passes, then the path's LR4 high-pass at
145 /// [`MimoPath::low_hz`] and LR4 low-pass at [`MimoPaths::fsiso`].
146 Iir {
147 /// Path gain in dB.
148 gain_db: f64,
149 /// Path delay in ms, added to [`MimoPath::delay_ms`].
150 delay_ms: f64,
151 /// Polarity, `1` or `-1`.
152 polarity: i8,
153 /// Peaking bands, in order.
154 peq: Vec<Peq>,
155 /// All-passes, in order.
156 allpass: Vec<Allpass>,
157 },
158 /// FIR support: the low-rate ideal FIR raised to 48 kHz
159 /// (`export_camilladsp.fir_to_48k`); the Fsiso low-pass is inside it.
160 Fir {
161 /// Taps at 48 kHz.
162 taps_48k: Vec<f64>,
163 },
164 /// FIR support retained at its design rate. Exporters materialize the
165 /// historical 48 kHz representation only when writing convolution files.
166 /// The isolation low-pass is already included in these taps.
167 FirLowRate {
168 /// Design-rate coefficients, including the isolation low-pass.
169 taps_low: Vec<f64>,
170 /// Design sample rate in hertz.
171 fs_low: f64,
172 },
173}
174
175impl MimoFilter {
176 /// Whether this support uses a finite impulse response.
177 pub fn is_fir(&self) -> bool {
178 matches!(self, Self::Fir { .. } | Self::FirLowRate { .. })
179 }
180
181 /// Number of coefficients in the compatible 48 kHz export representation.
182 pub fn fir_len_48k(&self) -> usize {
183 match self {
184 Self::Iir { .. } => 0,
185 Self::Fir { taps_48k } => taps_48k.len(),
186 Self::FirLowRate { taps_low, fs_low } => {
187 if !fs_low.is_finite() || *fs_low <= 0.0 || taps_low.is_empty() {
188 0
189 } else {
190 (taps_low.len() as f64 * (48_000.0 / fs_low)).ceil() as usize
191 }
192 }
193 }
194 }
195}
196
197/// One MIMO support path: input `input` also drives speaker `support`.
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
199#[ts(export)]
200pub struct MimoPath {
201 /// Input role (the channel whose raw signal the path copies).
202 pub input: String,
203 /// Support speaker role (the output the path adds to).
204 pub support: String,
205 /// The path filter.
206 pub filter: MimoFilter,
207 /// Protective LR4 high-pass in Hz: the support's F-support low edge,
208 /// 50 Hz when the speaker has no range (upstream fallback).
209 pub low_hz: f64,
210 /// The support speaker's delay in ms, output axis: its alignment delay
211 /// plus [`SignalChain::main_extra_ms`].
212 pub delay_ms: f64,
213}
214
215/// Every MIMO support path, in upstream order (input, then support).
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
217#[ts(export)]
218pub struct MimoPaths {
219 /// Isolation frequency in Hz (the IIR paths' LR4 low-pass).
220 pub fsiso: f64,
221 /// The paths.
222 pub paths: Vec<MimoPath>,
223}