secs_engine/input.rs
1//! What the engine is given: measurements and the resolved design settings.
2//!
3//! Upstream: the arguments of `CorrectionEngine.__init__`
4//! (`correction_engine.py:411–417`) as `calibration.make_engine` passes them
5//! (`calibration.py:564–581`), plus the attributes it writes after
6//! construction (`infra_boost_max_db`, `xo_peq_*`, the lowered
7//! `max_boost_db`). The `.secs`-level settings (`CalibrationSettings`) are
8//! resolved into these values by the orchestrator, not here.
9
10use secs_types::target::TargetCurve;
11use std::sync::Arc;
12
13/// Impulse-response samples in the precision they were stored with.
14///
15/// Upstream widens every IR to float64 (`np.asarray(ir, float)`) before any
16/// arithmetic, which [`Samples::at`] reproduces exactly; keeping float32
17/// sources as float32 halves their memory.
18#[derive(Debug, Clone)]
19pub enum Samples {
20 /// WAV, REW and mdat measurements.
21 F32(Arc<[f32]>),
22 /// Synthetic or computed responses.
23 F64(Arc<[f64]>),
24}
25
26impl Samples {
27 /// Number of samples.
28 pub fn len(&self) -> usize {
29 match self {
30 Self::F32(s) => s.len(),
31 Self::F64(s) => s.len(),
32 }
33 }
34
35 /// Whether there are no samples.
36 pub fn is_empty(&self) -> bool {
37 self.len() == 0
38 }
39
40 /// Sample `i` widened to f64 (exact for float32).
41 pub fn at(&self, i: usize) -> f64 {
42 match self {
43 Self::F32(s) => f64::from(s[i]),
44 Self::F64(s) => s[i],
45 }
46 }
47}
48
49/// One measured impulse response: upstream's `(ir, sr, t0)` tuple.
50#[derive(Debug, Clone)]
51pub struct Measurement {
52 /// The impulse response.
53 pub samples: Samples,
54 /// Its sample rate in Hz.
55 pub sample_rate: u32,
56 /// Time in seconds of sample 0 relative to the measurement's own clock
57 /// (REW's `timingOffset`), 0 when unknown.
58 pub t0: f64,
59}
60
61/// Measurements by position, then role.
62///
63/// Positions are ascending (upstream iterates `sorted(irs)`); roles keep the
64/// order they were given in, which is the order upstream's dictionaries
65/// iterate.
66#[derive(Debug, Clone, Default)]
67pub struct MeasurementSet {
68 /// `(position, [(role, measurement)])`, positions strictly ascending.
69 pub positions: Vec<(u32, Vec<(String, Measurement)>)>,
70}
71
72impl MeasurementSet {
73 /// The measurement of `role` at `position`, if any.
74 pub fn get(&self, position: u32, role: &str) -> Option<&Measurement> {
75 self.positions
76 .iter()
77 .find(|(p, _)| *p == position)
78 .and_then(|(_, roles)| roles.iter().find(|(r, _)| r == role).map(|(_, m)| m))
79 }
80}
81
82pub use secs_types::chain::{Allpass, BandType, Peq};
83
84/// How BA treats a sub (R27).
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum SubMode {
87 /// The engine finds gain, delay, polarity and all-passes.
88 Auto,
89 /// The values are used as given (they are DSP output values).
90 Fixed,
91 /// Relative values inside the bundle are kept; the engine finds the
92 /// bundle's common gain, delay and polarity.
93 Bundle,
94}
95
96/// DBA row of a sub in a DBA layout.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum DbaRow {
99 /// Front array.
100 Front,
101 /// Rear array (inverted and delayed by the propagation time).
102 Rear,
103}
104
105/// A sub's manual settings: upstream `calibration.sub_entry()`, normalised.
106#[derive(Debug, Clone, PartialEq)]
107pub struct SubManual {
108 /// BA mode.
109 pub mode: SubMode,
110 /// Bundle name (`"A"`, `"B"`) when `mode` is [`SubMode::Bundle`].
111 pub bundle: String,
112 /// Output gain in dB.
113 pub gain_db: f64,
114 /// Output delay in ms.
115 pub delay_ms: f64,
116 /// Output polarity, `1` or `-1`.
117 pub polarity: i8,
118 /// Up to two all-passes.
119 pub allpass: Vec<Allpass>,
120 /// Whether the sub may serve as a MIMO support (M3).
121 pub mimo_support: bool,
122 /// Fixed high-pass in Hz, 0 for none.
123 pub hp_hz: f64,
124 /// High-pass family.
125 pub hp_type: BandType,
126 /// Fixed low-pass in Hz, 0 for none.
127 pub lp_hz: f64,
128 /// Low-pass family.
129 pub lp_type: BandType,
130 /// Fixed PEQ bands, at most three.
131 pub peq: Vec<Peq>,
132 /// DBA row, used only by a DBA layout.
133 pub dba_row: DbaRow,
134}
135
136/// Bundle layout kind (R30).
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum LayoutKind {
139 /// Double bass array: front row, inverted and delayed rear row.
140 Dba,
141 /// Position-based multi-sub: one common filter, per-sub level.
142 Multi,
143}
144
145/// A bundle layout (`bundle_layouts` entry other than `'values'`).
146#[derive(Debug, Clone, PartialEq)]
147pub struct BundleLayout {
148 /// Layout kind.
149 pub kind: LayoutKind,
150 /// User trim of the rear-row delay in ms (DBA).
151 pub rear_trim_ms: f64,
152}
153
154/// The resolved design settings: the engine constructor's surface.
155#[derive(Debug, Clone, PartialEq)]
156pub struct EngineConfig {
157 /// Main roles, in configuration group order.
158 pub mains: Vec<String>,
159 /// Sub roles, in configuration order.
160 pub subs: Vec<String>,
161 /// Configuration groups and their roles, in order (`SpeakerConfig.groups()`).
162 pub groups: Vec<(String, Vec<String>)>,
163 /// Target curve (a copy taken when the computation starts).
164 pub target: TargetCurve,
165 /// Crossover in Hz per main group, in upstream dictionary order.
166 pub xo: Vec<(String, f64)>,
167 /// Reference (listening) position.
168 pub main_position: u32,
169 /// Channel FIR taps; 0 means PEQ only.
170 pub taps: usize,
171 /// Excess-phase lift in ms, before the FIR-length cap.
172 pub lift_ms: f64,
173 /// Boost ceiling in dB for RC, and the cap of the XO and bus PEQ boosts.
174 pub max_boost_db: f64,
175 /// Input PEQ bands; `None` means 16.
176 pub peq_bands: Option<usize>,
177 /// Total filter latency budget in ms; `None` means lift + 15.
178 pub max_latency_ms: Option<f64>,
179 /// RC range `(low Hz, high Hz)`; a missing high bound means automatic
180 /// (Schroeder frequency, at most 500 Hz).
181 pub rc_range: (f64, Option<f64>),
182 /// Additional manually bounded correction bands (ADR 0024).
183 pub rc_bands: Vec<(f64, f64)>,
184 /// Protect measured low-frequency rolloff (ADR 0022), off by default.
185 pub low_protect: bool,
186 /// Common input high-pass in Hz, applied only after design (ADR 0031).
187 pub input_hp_hz: Option<f64>,
188 /// Common LF arrival for the mains (ADR 0025), off by default.
189 pub lf_align: bool,
190 /// Pole-replacing cuts for long modes (ADR 0025), off by default.
191 pub mode_decay: bool,
192 /// Unwind constant FIR phase above the correction bands (ADR 0030).
193 pub hf_phase: bool,
194 /// Room volume in m³, for the Schroeder frequency.
195 pub room_volume_m3: Option<f64>,
196 /// Automatic upper-limit evidence; ignored for a manual RC range.
197 pub rc_hi_basis: secs_types::calibration::RcHiBasis,
198 /// Schroeder transition multiplier: 1, 2 or 4.
199 pub rc_hi_transition: u8,
200 /// Octave fraction of the excess-phase smoothing (6, or 24 in linear mode).
201 pub phase_frac: f64,
202 /// Manual sub settings by role, in the order the settings hold them.
203 pub sub_manual: Vec<(String, SubManual)>,
204 /// Main output delay added when fixed subs are present, in ms.
205 pub sub_main_delay_ms: f64,
206 /// Bundle layouts by bundle name, in the order the settings hold them.
207 pub bundle_layouts: Vec<(String, BundleLayout)>,
208 /// Ceiling in dB of the two-way infra sub level match.
209 pub infra_boost_max_db: f64,
210 /// Whether the main acoustic-XO PEQ runs.
211 pub xo_peq_on: bool,
212 /// Lower edge of the XO PEQ fitting range in Hz, 0 for automatic.
213 pub xo_peq_lo_hz: f64,
214 /// Upper edge of the XO PEQ fitting range in Hz, 0 for automatic.
215 pub xo_peq_hi_hz: f64,
216}
217
218/// Which positions the common excess phase is measured over (`SECS_PHASE_POS`).
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum PhasePositions {
221 /// All positions where the channel was measured (default).
222 Common,
223 /// The reference position only.
224 Main,
225}
226
227/// BA all-pass policy (constructor `sub_allpass`).
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub enum SubAllpass {
230 /// No all-passes (production; their DE variables still exist, D11).
231 None,
232 /// Q up to 3.
233 Full,
234 /// Q up to 1.
235 LowQ,
236}
237
238/// Bundle gain policy (constructor `bundle_gain`).
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub enum BundleGain {
241 /// The engine finds the bundle's common gain (production).
242 Auto,
243 /// The bundle keeps the gains as entered.
244 Fixed,
245}
246
247/// Engine knobs SECS 2 kept in environment variables or constructor
248/// defaults that production never changes. Defaults are upstream's.
249#[derive(Debug, Clone, PartialEq)]
250pub struct Tuning {
251 /// `SECS_BOOST_RING_MS`: ringing cap of RC PEQ bands in ms; `None` disables.
252 pub boost_ring_ms: Option<f64>,
253 /// `SECS_BA_DELAY_MS`: BA fine-delay search half-width in ms.
254 pub ba_delay_ms: f64,
255 /// `SECS_BA_EFF`: weight of the summing-efficiency reward in the BA cost.
256 pub ba_eff_weight: f64,
257 /// `SECS_SUB_BUS_FIX`: bus PEQ capped at 1.25 × XO and the second BA pass.
258 pub sub_bus_fix: bool,
259 /// `SECS_MAIN_W_LO`: reference-position weight below 200 Hz in RC.
260 pub main_weight_lo: f64,
261 /// `SECS_PHASE_POS`: positions of the common excess phase.
262 pub phase_positions: PhasePositions,
263 /// `SECS_LONG_BOOST_DB`: boost ceiling at long room modes in dB.
264 pub long_boost_db: f64,
265 /// `SECS_NOBOOST_MS`: GD-spike threshold of the no-boost mask in ms
266 /// (default 1e9, effectively off).
267 pub noboost_ms: f64,
268 /// Constructor `xo_stage`: acoustic-XO target PEQ and bus PEQ.
269 pub xo_stage: bool,
270 /// Constructor `sub_allpass`.
271 pub sub_allpass: SubAllpass,
272 /// Constructor `gd_cuts`: high-Q cuts at excess-GD peaks.
273 pub gd_cuts: bool,
274 /// Constructor `bundle_gain`.
275 pub bundle_gain: BundleGain,
276 /// `SECS_MIMO_BAND_DB`: MIMO path PEQ gain limit (dB).
277 pub mimo_band_db: f64,
278 /// `SECS_MIMO_Q_MAX`: MIMO path PEQ Q upper bound.
279 pub mimo_q_max: f64,
280 /// `SECS_MIMO_IN_BAND`: keep MIMO path PEQ and all-pass centres inside the support band.
281 pub mimo_in_band: bool,
282 /// `SECS_MIMO_BAND_REG`: MIMO path band-gain penalty weight.
283 pub mimo_band_reg: f64,
284 /// `SECS_MIMO_EFFORT`: MIMO support output penalty, larger where coupling is weak.
285 pub mimo_effort: f64,
286 /// `SECS_MIMO_WORSE_DB`: band late-energy worsening that lowers MIMO support gain (dB).
287 pub mimo_worse_db: f64,
288 /// `SECS_MIMO_GAIN_MAX_DB`: MIMO path gain upper bound (dB).
289 pub mimo_gain_max_db: f64,
290 /// `K_BANDS`: peaking biquads per MIMO path.
291 pub mimo_k_bands: usize,
292 /// `A_BANDS`: all-pass sections per MIMO path.
293 pub mimo_a_bands: usize,
294 /// `MAX_NFEV`: residual evaluations of the joint MIMO IIR fit.
295 pub mimo_max_nfev: usize,
296}
297
298impl Default for Tuning {
299 fn default() -> Self {
300 Self {
301 boost_ring_ms: Some(30.0),
302 ba_delay_ms: 12.0,
303 ba_eff_weight: 1.0,
304 sub_bus_fix: false,
305 main_weight_lo: 5.0,
306 phase_positions: PhasePositions::Common,
307 long_boost_db: 3.0,
308 noboost_ms: 1e9,
309 xo_stage: true,
310 sub_allpass: SubAllpass::None,
311 gd_cuts: false,
312 bundle_gain: BundleGain::Auto,
313 mimo_band_db: 15.0,
314 mimo_q_max: 10.0,
315 mimo_in_band: true,
316 mimo_band_reg: 0.05,
317 mimo_effort: 0.05,
318 mimo_worse_db: 1.0,
319 mimo_gain_max_db: 20.0,
320 mimo_k_bands: 9,
321 mimo_a_bands: 3,
322 mimo_max_nfev: 150,
323 }
324 }
325}