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