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