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 lf_align: settings.lf_align,
229 mode_decay: settings.mode_decay,
230 hf_phase: settings.hf_phase,
231 room_volume_m3: settings.room_volume_m3,
232 rc_hi_basis: settings.rc_hi_basis,
233 rc_hi_transition: settings.rc_hi_transition,
234 phase_frac: mode.phase_frac,
235 sub_manual,
236 sub_main_delay_ms: settings.sub_main_delay_ms,
237 bundle_layouts,
238 infra_boost_max_db: settings.infra_boost_max_db,
239 xo_peq_on: settings.xo_peq_on,
240 xo_peq_lo_hz: settings.xo_peq_lo,
241 xo_peq_hi_hz: settings.xo_peq_hi,
242 },
243 notes,
244 })
245}
246
247fn rc_bands(bands: &[RcBand]) -> Vec<(f64, f64)> {
252 let limits = CALIBRATION_LIMITS.rc_band.0..=CALIBRATION_LIMITS.rc_band.1;
253 let mut bands: Vec<_> = bands
254 .iter()
255 .filter(|band| {
256 band.lo_hz.is_finite()
257 && band.hi_hz.is_finite()
258 && limits.contains(&band.lo_hz)
259 && limits.contains(&band.hi_hz)
260 && band.lo_hz < band.hi_hz
261 })
262 .map(|band| (band.lo_hz, band.hi_hz))
263 .collect();
264 bands.sort_by(|left, right| left.0.total_cmp(&right.0));
265 bands.truncate(CALIBRATION_LIMITS.rc_bands_max.1 as usize);
266 bands
267}
268
269pub(crate) fn resolve_xo(
270 groups: &[(String, Vec<String>)],
271 selected: &[(String, f64)],
272 project: &[(String, f64)],
273) -> Vec<(String, f64)> {
274 groups
275 .iter()
276 .filter(|(group, _)| group != "sub")
277 .map(|(group, _)| {
278 let value = selected
279 .iter()
280 .find(|(name, value)| name == group && *value != 0.0)
281 .or_else(|| {
282 project
283 .iter()
284 .find(|(name, value)| name == group && *value != 0.0)
285 })
286 .map_or(80.0, |(_, value)| *value);
287 (group.clone(), value)
288 })
289 .collect()
290}
291
292fn sub_has_filters(entry: &SubEntry) -> bool {
293 entry.hp_hz != 0.0 || entry.lp_hz != 0.0 || entry.peq.iter().any(|band| band.2 != 0.0)
294}
295
296fn manual(entry: &SubEntry) -> SubManual {
297 SubManual {
298 mode: match entry.mode.as_str() {
299 "fixed" => SubMode::Fixed,
300 "bundle" => SubMode::Bundle,
301 _ => SubMode::Auto,
302 },
303 bundle: entry.bundle.clone(),
304 gain_db: entry.gain_db,
305 delay_ms: entry.delay_ms,
306 polarity: entry.polarity,
307 allpass: entry.allpass.clone(),
308 mimo_support: entry.mimo_support,
309 hp_hz: entry.hp_hz,
310 hp_type: BandType::from_name(&entry.hp_type),
311 lp_hz: entry.lp_hz,
312 lp_type: BandType::from_name(&entry.lp_type),
313 peq: entry.peq.clone(),
314 dba_row: if entry.dba_row == "rear" {
315 DbaRow::Rear
316 } else {
317 DbaRow::Front
318 },
319 }
320}
321
322#[cfg(test)]
323mod direct_tests {
324 use super::*;
325 use crate::input::{Measurement, Samples};
326 use std::sync::Arc;
327
328 fn measurement() -> Measurement {
329 Measurement {
330 samples: Samples::F32(Arc::from([1.0])),
331 sample_rate: 48_000,
332 t0: 0.0,
333 }
334 }
335
336 #[test]
337 fn excludes_biamp_and_non_reference_only_roles() {
338 let groups = vec![
339 ("front".into(), vec!["L".into(), "R".into()]),
340 ("sub".into(), vec!["SW1".into()]),
341 ];
342 let target = TargetCurve::default();
343 for (fixture, positions) in [
344 (
345 include_str!(concat!(
346 env!("CARGO_MANIFEST_DIR"),
347 "/../../oracle/goldens/calibration/adapter_biamp.json"
348 )),
349 vec![(
350 1,
351 vec![("L".into(), measurement()), ("L:LF".into(), measurement())],
352 )],
353 ),
354 (
355 include_str!(concat!(
356 env!("CARGO_MANIFEST_DIR"),
357 "/../../oracle/goldens/calibration/adapter_non_reference_only.json"
358 )),
359 vec![
360 (1, vec![("L".into(), measurement())]),
361 (
362 2,
363 vec![
364 ("L".into(), measurement()),
365 ("R".into(), measurement()),
366 ("SW1".into(), measurement()),
367 ],
368 ),
369 ],
370 ),
371 ] {
372 let expected: serde_json::Value = serde_json::from_str(fixture).unwrap();
373 let expected = &expected["outputs"];
374 let out = engine_inputs(
375 &ProjectView {
376 groups: &groups,
377 project_xo: &[],
378 target: &target,
379 },
380 &MeasurementSet { positions },
381 &CalibrationSettings::default(),
382 )
383 .unwrap();
384 assert_eq!(
385 serde_json::to_value(&out.config.mains).unwrap(),
386 expected["mains"]
387 );
388 assert_eq!(
389 serde_json::to_value(&out.config.subs).unwrap(),
390 expected["subs"]
391 );
392 assert_eq!(
393 serde_json::to_value(&out.config.groups).unwrap(),
394 expected["groups"]
395 );
396 assert_eq!(
397 serde_json::to_value(&out.config.xo).unwrap(),
398 expected["xo"]
399 );
400 assert_eq!(
401 out.config.main_position,
402 expected["main_position"].as_u64().unwrap() as u32
403 );
404 assert_eq!(serde_json::to_value(&out.notes).unwrap(), expected["notes"]);
405 let actual_positions: Vec<_> = out
406 .measurements
407 .positions
408 .iter()
409 .map(|(position, roles)| {
410 (
411 *position,
412 roles.iter().map(|(role, _)| role).collect::<Vec<_>>(),
413 )
414 })
415 .collect();
416 assert_eq!(
417 serde_json::to_value(actual_positions).unwrap(),
418 expected["positions"]
419 );
420 }
421 }
422}