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