1use std::collections::BTreeSet;
4
5use secs_types::{
6 calibration::{CalibrationSettings, RcBand, SubEntry, mode_params},
7 chain::BandType,
8 rules::CALIBRATION_LIMITS,
9 target::TargetCurve,
10};
11
12use crate::{
13 EngineError,
14 input::{BundleLayout, DbaRow, EngineConfig, LayoutKind, MeasurementSet, SubManual, SubMode},
15};
16
17#[derive(Debug)]
18pub struct ProjectView<'a> {
19 pub groups: &'a [(String, Vec<String>)],
20 pub project_xo: &'a [(String, f64)],
21 pub target: &'a TargetCurve,
22 pub inverted: &'a [String],
23}
24
25#[derive(Debug)]
26pub struct EngineInputs {
27 pub measurements: MeasurementSet,
28 pub config: EngineConfig,
29 pub notes: Vec<String>,
30}
31
32#[derive(Debug)]
33pub struct CalibrationRequest<'a> {
34 pub view: &'a ProjectView<'a>,
35 pub irs: &'a MeasurementSet,
36 pub settings: &'a CalibrationSettings,
37 pub subs_only: bool,
38}
39
40#[derive(Debug)]
41pub struct Calibration {
42 pub design: crate::Design,
43 pub measurements: MeasurementSet,
45 pub config: EngineConfig,
46 pub resampled: usize,
48}
49
50pub fn calibrate(
52 req: &CalibrationRequest<'_>,
53 ctx: &secs_compute::Context,
54) -> Result<Calibration, EngineError> {
55 let derivatives = if req.settings.peq_analytic_derivatives {
56 crate::PeqDerivatives::Analytic
57 } else {
58 crate::PeqDerivatives::TwoPoint
59 };
60 calibrate_with(
61 req,
62 ctx,
63 &*crate::production_fitters_with(ctx.cancel(), derivatives),
64 )
65}
66
67pub fn calibrate_with(
69 req: &CalibrationRequest<'_>,
70 ctx: &secs_compute::Context,
71 fitters: &crate::FitterFactory<'_>,
72) -> Result<Calibration, EngineError> {
73 ctx.cancel().check().map_err(|_| EngineError::Cancelled)?;
74 let inputs = engine_inputs(req.view, req.irs, req.settings)?;
75 let (inputs, resampled) = prepare_inputs(inputs, req.settings.sample_rate_policy, ctx)?;
76 let design = crate::design(
77 &crate::DesignRequest {
78 measurements: &inputs.measurements,
79 config: &inputs.config,
80 tuning: &crate::Tuning::default(),
81 subs_only: req.subs_only,
82 adapter_notes: &inputs.notes,
83 },
84 ctx,
85 fitters,
86 )?;
87 Ok(Calibration {
88 design,
89 measurements: inputs.measurements,
90 config: inputs.config,
91 resampled,
92 })
93}
94
95pub(crate) fn prepare_inputs(
96 mut inputs: EngineInputs,
97 policy: secs_types::calibration::SampleRatePolicy,
98 ctx: &secs_compute::Context,
99) -> Result<(EngineInputs, usize), EngineError> {
100 ctx.cancel().check().map_err(|_| EngineError::Cancelled)?;
101 let resampled = inputs
102 .measurements
103 .positions
104 .iter()
105 .flat_map(|(_, roles)| roles)
106 .filter(|(_, measurement)| measurement.sample_rate != crate::grid::SR)
107 .count();
108 inputs.measurements = crate::normalize_measurements(&inputs.measurements, policy, ctx)?;
109 Ok((inputs, resampled))
110}
111
112pub fn engine_inputs(
113 view: &ProjectView<'_>,
114 irs: &MeasurementSet,
115 settings: &CalibrationSettings,
116) -> Result<EngineInputs, EngineError> {
117 let mut skipped = BTreeSet::new();
118 let mut positions = Vec::new();
119 for (position, roles) in &irs.positions {
120 let clean: Vec<_> = roles
121 .iter()
122 .filter_map(|(role, measurement)| {
123 if role.contains(':') {
124 skipped.insert(role.clone());
125 None
126 } else {
127 let mut measurement = measurement.clone();
128 if view.inverted.contains(role) {
129 measurement.samples = match &measurement.samples {
130 crate::Samples::F32(samples) => crate::Samples::F32(
131 samples
132 .iter()
133 .map(|sample| -*sample)
134 .collect::<Vec<_>>()
135 .into(),
136 ),
137 crate::Samples::F64(samples) => crate::Samples::F64(
138 samples
139 .iter()
140 .map(|sample| -*sample)
141 .collect::<Vec<_>>()
142 .into(),
143 ),
144 };
145 }
146 Some((role.clone(), measurement))
147 }
148 })
149 .collect();
150 if !clean.is_empty() {
151 positions.push((*position, clean));
152 }
153 }
154 let Some(main_position) = positions.iter().map(|(position, _)| *position).min() else {
155 return Err(EngineError::Input("no measured impulse responses".into()));
156 };
157 let have: BTreeSet<_> = positions
158 .iter()
159 .find(|(position, _)| *position == main_position)
160 .expect("minimum position exists")
161 .1
162 .iter()
163 .map(|(role, _)| role.as_str())
164 .collect();
165
166 let groups: Vec<_> = view
167 .groups
168 .iter()
169 .filter_map(|(group, roles)| {
170 let roles: Vec<_> = roles
171 .iter()
172 .filter(|role| have.contains(role.as_str()))
173 .cloned()
174 .collect();
175 (!roles.is_empty()).then(|| (group.clone(), roles))
176 })
177 .collect();
178 let subs = groups
179 .iter()
180 .find(|(group, _)| group == "sub")
181 .map_or_else(Vec::new, |(_, roles)| roles.clone());
182 let mains: Vec<_> = groups
183 .iter()
184 .filter(|(group, _)| group != "sub")
185 .flat_map(|(_, roles)| roles.iter().cloned())
186 .collect();
187 if mains.is_empty() {
188 return Err(EngineError::Input("no measured main channels".into()));
189 }
190
191 let keep: BTreeSet<_> = mains.iter().chain(&subs).map(String::as_str).collect();
192 for (_, roles) in &mut positions {
193 roles.retain(|(role, _)| keep.contains(role.as_str()));
194 }
195
196 let xo = resolve_xo(&groups, &settings.xo, view.project_xo);
197 let mode = mode_params(settings.mode, settings.ultra_ms);
198 let sub_manual = settings
199 .subs_manual
200 .iter()
201 .filter(|(_, entry)| entry.mode != "auto" || sub_has_filters(entry))
202 .map(|(role, entry)| (role.clone(), manual(entry)))
203 .collect();
204 let bundle_layouts = settings
205 .bundle_layouts
206 .iter()
207 .filter_map(|(bundle, entry)| {
208 let kind = match entry.kind.as_str() {
209 "dba" => LayoutKind::Dba,
210 "multi" => LayoutKind::Multi,
211 _ => return None,
212 };
213 Some((
214 bundle.clone(),
215 BundleLayout {
216 kind,
217 rear_trim_ms: entry.rear_trim_ms,
218 },
219 ))
220 })
221 .collect();
222 let notes = if skipped.is_empty() {
223 Vec::new()
224 } else {
225 vec![format!(
226 "바이앰프 대역 측정 제외: {}",
227 skipped.into_iter().collect::<Vec<_>>().join(", ")
228 )]
229 };
230
231 Ok(EngineInputs {
232 measurements: MeasurementSet { positions },
233 config: EngineConfig {
234 inverted_mains: view
235 .inverted
236 .iter()
237 .filter(|role| mains.contains(role))
238 .cloned()
239 .collect(),
240 mains,
241 subs,
242 groups,
243 target: view.target.clone(),
244 xo,
245 main_position,
246 taps: mode.taps,
247 lift_ms: mode.lift_ms,
248 max_boost_db: 10.0_f64.min(settings.boost_max_db),
249 peq_bands: mode.peq_bands,
250 max_latency_ms: Some(mode.max_latency_ms),
251 rc_range: (settings.rc_lo, settings.rc_hi),
252 rc_bands: rc_bands(&settings.rc_bands),
253 low_protect: settings.low_protect,
254 input_hp_hz: settings.input_hp_hz,
255 lf_align: settings.lf_align,
256 mode_decay: settings.mode_decay,
257 hf_phase: settings.hf_phase,
258 dip_limit: settings.dip_limit,
259 room_volume_m3: settings.room_volume_m3,
260 rc_hi_basis: settings.rc_hi_basis,
261 rc_hi_transition: settings.rc_hi_transition,
262 phase_frac: mode.phase_frac,
263 sub_manual,
264 sub_main_delay_ms: settings.sub_main_delay_ms,
265 bundle_layouts,
266 infra_boost_max_db: settings.infra_boost_max_db,
267 xo_peq_on: settings.xo_peq_on,
268 xo_peq_lo_hz: settings.xo_peq_lo,
269 xo_peq_hi_hz: settings.xo_peq_hi,
270 },
271 notes,
272 })
273}
274
275fn rc_bands(bands: &[RcBand]) -> Vec<(f64, f64)> {
280 let limits = CALIBRATION_LIMITS.rc_band.0..=CALIBRATION_LIMITS.rc_band.1;
281 let mut bands: Vec<_> = bands
282 .iter()
283 .filter(|band| {
284 band.lo_hz.is_finite()
285 && band.hi_hz.is_finite()
286 && limits.contains(&band.lo_hz)
287 && limits.contains(&band.hi_hz)
288 && band.lo_hz < band.hi_hz
289 })
290 .map(|band| (band.lo_hz, band.hi_hz))
291 .collect();
292 bands.sort_by(|left, right| left.0.total_cmp(&right.0));
293 bands.truncate(CALIBRATION_LIMITS.rc_bands_max.1 as usize);
294 bands
295}
296
297pub(crate) fn resolve_xo(
298 groups: &[(String, Vec<String>)],
299 selected: &[(String, f64)],
300 project: &[(String, f64)],
301) -> Vec<(String, f64)> {
302 groups
303 .iter()
304 .filter(|(group, _)| group != "sub")
305 .map(|(group, _)| {
306 let value = selected
307 .iter()
308 .find(|(name, value)| name == group && *value != 0.0)
309 .or_else(|| {
310 project
311 .iter()
312 .find(|(name, value)| name == group && *value != 0.0)
313 })
314 .map_or(80.0, |(_, value)| *value);
315 (group.clone(), value)
316 })
317 .collect()
318}
319
320fn sub_has_filters(entry: &SubEntry) -> bool {
321 entry.hp_hz != 0.0 || entry.lp_hz != 0.0 || entry.peq.iter().any(|band| band.2 != 0.0)
322}
323
324fn manual(entry: &SubEntry) -> SubManual {
325 SubManual {
326 mode: match entry.mode.as_str() {
327 "fixed" => SubMode::Fixed,
328 "bundle" => SubMode::Bundle,
329 _ => SubMode::Auto,
330 },
331 bundle: entry.bundle.clone(),
332 gain_db: entry.gain_db,
333 delay_ms: entry.delay_ms,
334 polarity: entry.polarity,
335 allpass: entry.allpass.clone(),
336 mimo_support: entry.mimo_support,
337 hp_hz: entry.hp_hz,
338 hp_type: BandType::from_name(&entry.hp_type),
339 lp_hz: entry.lp_hz,
340 lp_type: BandType::from_name(&entry.lp_type),
341 peq: entry.peq.clone(),
342 dba_row: if entry.dba_row == "rear" {
343 DbaRow::Rear
344 } else {
345 DbaRow::Front
346 },
347 }
348}
349
350#[cfg(test)]
351mod direct_tests {
352 use super::*;
353 use crate::input::{Measurement, Samples};
354 use std::sync::Arc;
355
356 fn measurement() -> Measurement {
357 Measurement {
358 samples: Samples::F32(Arc::from([1.0])),
359 sample_rate: 48_000,
360 t0: 0.0,
361 }
362 }
363
364 #[test]
365 fn excludes_biamp_and_non_reference_only_roles() {
366 let groups = vec![
367 ("front".into(), vec!["L".into(), "R".into()]),
368 ("sub".into(), vec!["SW1".into()]),
369 ];
370 let target = TargetCurve::default();
371 for (fixture, positions) in [
372 (
373 include_str!(concat!(
374 env!("CARGO_MANIFEST_DIR"),
375 "/../../oracle/goldens/calibration/adapter_biamp.json"
376 )),
377 vec![(
378 1,
379 vec![("L".into(), measurement()), ("L:LF".into(), measurement())],
380 )],
381 ),
382 (
383 include_str!(concat!(
384 env!("CARGO_MANIFEST_DIR"),
385 "/../../oracle/goldens/calibration/adapter_non_reference_only.json"
386 )),
387 vec![
388 (1, vec![("L".into(), measurement())]),
389 (
390 2,
391 vec![
392 ("L".into(), measurement()),
393 ("R".into(), measurement()),
394 ("SW1".into(), measurement()),
395 ],
396 ),
397 ],
398 ),
399 ] {
400 let expected: serde_json::Value = serde_json::from_str(fixture).unwrap();
401 let expected = &expected["outputs"];
402 let out = engine_inputs(
403 &ProjectView {
404 groups: &groups,
405 project_xo: &[],
406 target: &target,
407 inverted: &[],
408 },
409 &MeasurementSet { positions },
410 &CalibrationSettings::default(),
411 )
412 .unwrap();
413 assert_eq!(
414 serde_json::to_value(&out.config.mains).unwrap(),
415 expected["mains"]
416 );
417 assert_eq!(
418 serde_json::to_value(&out.config.subs).unwrap(),
419 expected["subs"]
420 );
421 assert_eq!(
422 serde_json::to_value(&out.config.groups).unwrap(),
423 expected["groups"]
424 );
425 assert_eq!(
426 serde_json::to_value(&out.config.xo).unwrap(),
427 expected["xo"]
428 );
429 assert_eq!(
430 out.config.main_position,
431 expected["main_position"].as_u64().unwrap() as u32
432 );
433 assert_eq!(serde_json::to_value(&out.notes).unwrap(), expected["notes"]);
434 let actual_positions: Vec<_> = out
435 .measurements
436 .positions
437 .iter()
438 .map(|(position, roles)| {
439 (
440 *position,
441 roles.iter().map(|(role, _)| role).collect::<Vec<_>>(),
442 )
443 })
444 .collect();
445 assert_eq!(
446 serde_json::to_value(actual_positions).unwrap(),
447 expected["positions"]
448 );
449 }
450 }
451}