1use crate::{
4 C, Design, LayoutInfo,
5 chain::{BaRow, term},
6 grid::design_freqs,
7};
8use secs_dsp::{
9 interp, pairwise_sum,
10 smooth::{Fraction, PowerSmoother},
11};
12use secs_types::chain::{Allpass, Peq};
13
14#[derive(Debug, Clone, PartialEq)]
16pub struct XoCurves {
17 pub grid: Vec<f64>,
19 pub main: Vec<f64>,
21 pub sub: Vec<f64>,
23 pub sum: Vec<f64>,
25 pub target: Vec<f64>,
27}
28
29#[derive(Debug)]
30struct CachedMain {
31 role: String,
32 raw: Vec<C>,
33 corrected: Vec<C>,
34 correction: std::sync::Arc<[C]>,
35}
36
37#[derive(Debug)]
40pub struct XoPreview {
41 sub: Vec<C>,
42 mains: Vec<CachedMain>,
43 grid: Vec<f64>,
44 target: Vec<f64>,
45 smoother: PowerSmoother,
46}
47
48impl XoPreview {
49 pub fn new(design: &Design) -> Self {
51 let f = design_freqs();
52 let pos = design.spectra.main_position;
53 let mut sub = vec![C::new(0.0, 0.0); f.len()];
54 if let Some(s) = &design.subs {
55 let terms: Vec<_> = s
56 .rows
57 .iter()
58 .map(|(r, row)| {
59 (
60 design
61 .spectra
62 .used
63 .get(pos, r)
64 .expect("reference sub")
65 .as_ref(),
66 row,
67 )
68 })
69 .collect();
70 sub = crate::chain::sub_sum(&terms, f);
71 if let Some(bus) = &s.bus_response {
72 for (h, b) in sub.iter_mut().zip(bus.iter()) {
73 *h *= b;
74 }
75 }
76 }
77 let mains = design
78 .mains
79 .iter()
80 .filter_map(|m| {
81 let h = design.spectra.raw.get(pos, &m.role)?;
82 let gain = C::new(10f64.powf(m.gain_db / 20.0), 0.0);
83 let raw: Vec<_> = h
84 .iter()
85 .zip(crate::measurement::delay_response(m.align_delay_ms))
86 .map(|(h, d)| h * d * gain)
87 .collect();
88 let corrected = raw
89 .iter()
90 .enumerate()
91 .map(|(i, h)| {
92 h * m.top_response.as_ref().map_or(C::new(1.0, 0.0), |p| p[i])
93 * m.response[i]
94 })
95 .collect();
96 Some(CachedMain {
97 role: m.role.clone(),
98 raw,
99 corrected,
100 correction: m.response.clone(),
101 })
102 })
103 .collect();
104 let grid = geomspace(5.0, 500.0, 620);
105 let target = design
106 .target
107 .sample(&grid)
108 .iter()
109 .map(|t| t + design.ref_level)
110 .collect();
111 Self {
112 sub,
113 mains,
114 grid,
115 target,
116 smoother: PowerSmoother::new(f, Fraction::Scalar(12.0)).expect("design grid"),
117 }
118 }
119
120 pub fn curves(
124 &self,
125 design: &Design,
126 group: &str,
127 fc: f64,
128 corrected: bool,
129 role: Option<&str>,
130 ) -> XoCurves {
131 let group_roles = design
132 .groups
133 .iter()
134 .find(|(g, _)| g == group)
135 .map(|(_, roles)| roles);
136 let mut mains: Vec<_> = self
137 .mains
138 .iter()
139 .filter(|m| group_roles.is_some_and(|rs| rs.contains(&m.role)))
140 .collect();
141 if let Some(roles) = group_roles {
143 mains.sort_by_key(|m| roles.iter().position(|r| r == &m.role).unwrap());
144 }
145 if let Some(role) = role
146 && mains.iter().any(|m| m.role == role)
147 {
148 mains.retain(|m| m.role == role);
149 }
150 let f = design_freqs();
151 let hp = secs_dsp::response::lr4(f, fc, "HP");
152 let lp = secs_dsp::response::lr4(f, fc, "LP");
153 let mut main_power = vec![0.0; f.len()];
154 let mut sub_power = vec![0.0; f.len()];
155 let mut sum_power = vec![0.0; f.len()];
156 let power = |h: C| {
157 let a = h.re.hypot(h.im);
158 a * a
159 };
160 for i in 0..f.len() {
161 let sub = self.sub[i] * lp[i];
162 if mains.is_empty() {
163 sub_power[i] = power(sub);
164 sum_power[i] = power(sub);
165 }
166 for m in &mains {
167 let main = if corrected { m.corrected[i] } else { m.raw[i] } * hp[i];
168 let s = if corrected {
169 sub * m.correction[i]
170 } else {
171 sub
172 };
173 main_power[i] += power(main);
174 sub_power[i] += power(s);
175 sum_power[i] += power(main + s);
176 }
177 if !mains.is_empty() {
178 main_power[i] /= mains.len() as f64;
179 sub_power[i] /= mains.len() as f64;
180 sum_power[i] /= mains.len() as f64;
181 }
182 }
183 XoCurves {
184 grid: self.grid.clone(),
185 main: db(&main_power, &self.grid, &self.smoother),
186 sub: db(&sub_power, &self.grid, &self.smoother),
187 sum: db(&sum_power, &self.grid, &self.smoother),
188 target: self.target.clone(),
189 }
190 }
191}
192
193#[derive(Debug, Clone, PartialEq)]
195pub struct SubValue {
196 pub role: String,
198 pub gain_db: f64,
200 pub delay_ms: f64,
202 pub polarity: i8,
204 pub allpass: Vec<Allpass>,
206 pub mode: String,
208 pub peq: Vec<Peq>,
210}
211
212#[derive(Debug, Clone)]
214pub struct SubCombine {
215 pub subs: Vec<String>,
217 pub grid: Vec<f64>,
219 pub before: Vec<(String, Vec<f64>)>,
221 pub after: Vec<(String, Vec<f64>)>,
223 pub sum_before: Vec<f64>,
225 pub sum_after: Vec<f64>,
227 pub band: (f64, f64),
229 pub flat: (f64, f64),
231 pub seat: Option<(f64, f64)>,
233 pub positions: usize,
235 pub values: Vec<SubValue>,
237 pub layout_info: Vec<(String, LayoutInfo)>,
239}
240
241fn geomspace(lo: f64, hi: f64, n: usize) -> Vec<f64> {
242 let start = lo.log10();
243 let step = (hi.log10() - start) / (n - 1) as f64;
244 let mut out: Vec<_> = (0..n)
245 .map(|i| 10f64.powf(start + i as f64 * step))
246 .collect();
247 out[0] = lo;
248 out[n - 1] = hi;
249 out
250}
251fn mean(v: &[f64]) -> f64 {
252 pairwise_sum(v) / v.len() as f64
253}
254fn std(v: &[f64]) -> f64 {
255 let m = mean(v);
256 mean(&v.iter().map(|x| (x - m) * (x - m)).collect::<Vec<_>>()).sqrt()
257}
258fn db(power: &[f64], grid: &[f64], smoother: &PowerSmoother) -> Vec<f64> {
259 let f = design_freqs();
260 let values: Vec<_> = smoother
261 .apply(power)
262 .expect("design grid")
263 .iter()
264 .map(|p| 10.0 * (p + 1e-30).log10())
265 .collect();
266 grid.iter().map(|&g| interp(g, f, &values)).collect()
267}
268fn curve(h: &[C], grid: &[f64], smoother: &PowerSmoother) -> Vec<f64> {
269 let power = &h
270 .iter()
271 .map(|h| {
272 let a = h.re.hypot(h.im);
273 a * a
274 })
275 .collect::<Vec<_>>();
276 db(power, grid, smoother)
277}
278
279pub fn sub_combine(design: &Design) -> Option<SubCombine> {
283 let sub = design.subs.as_ref()?;
284 if sub.rows.len() < 2 {
285 return None;
286 }
287 let subs: Vec<_> = sub.rows.iter().map(|(r, _)| r.clone()).collect();
288 let positions: Vec<_> = design
289 .spectra
290 .positions
291 .iter()
292 .copied()
293 .filter(|&p| subs.iter().all(|r| design.spectra.raw.get(p, r).is_some()))
294 .collect();
295 let grid = geomspace(5.0, 300.0, 540);
296 let max_xo = design
297 .xo
298 .iter()
299 .filter(|(group, hz)| group != "sub" && *hz != 0.0)
300 .map(|(_, hz)| *hz)
301 .reduce(f64::max)
302 .unwrap_or(100.0);
303 let band = (20.0, max_xo * 1.5);
304 let metric_grid = geomspace(band.0, band.1, 200);
305 let raw = BaRow {
306 gain_db: 0.0,
307 delay_ms: 0.0,
308 polarity: 1,
309 allpass: vec![],
310 };
311 let display_smoother =
312 PowerSmoother::new(design_freqs(), Fraction::Scalar(12.0)).expect("design grid");
313 let metric_smoother =
314 PowerSmoother::new(design_freqs(), Fraction::Scalar(6.0)).expect("design grid");
315 let mut each = [Vec::new(), Vec::new()];
316 let mut sums = [Vec::new(), Vec::new()];
317 let mut flat = [0.0; 2];
318 let mut seat = [0.0; 2];
319 for state in 0..2 {
320 let mut levels = Vec::new();
321 for &p in &positions {
322 let mut sum = vec![C::new(0.0, 0.0); design_freqs().len()];
323 for (role, row) in &sub.rows {
324 let spectra = if state == 0 {
325 &design.spectra.raw
326 } else {
327 &design.spectra.used
328 };
329 let h = spectra.get(p, role).expect("complete sub position");
330 let mut out = term(h, design_freqs(), if state == 0 { &raw } else { row });
331 if state == 1
332 && let Some(bus) = &sub.bus_response
333 {
334 for (h, b) in out.iter_mut().zip(bus.iter()) {
335 *h *= b;
336 }
337 }
338 for (s, h) in sum.iter_mut().zip(&out) {
339 *s += h;
340 }
341 if p == design.spectra.main_position {
342 each[state].push((role.clone(), curve(&out, &grid, &display_smoother)));
343 }
344 }
345 let level = curve(&sum, &metric_grid, &metric_smoother);
346 if p == design.spectra.main_position {
347 flat[state] = std(&level);
348 sums[state] = curve(&sum, &grid, &display_smoother);
349 }
350 levels.push(level);
351 }
352 if positions.len() > 1 {
353 let deviations: Vec<_> = (0..metric_grid.len())
355 .map(|i| {
356 let m = levels.iter().fold(0.0, |s, v| s + v[i]) / levels.len() as f64;
357 (levels.iter().fold(0.0, |s, v| s + (v[i] - m).powi(2)) / levels.len() as f64)
358 .sqrt()
359 })
360 .collect();
361 seat[state] = mean(&deviations);
362 }
363 }
364 let values = design
365 .chain
366 .subs
367 .iter()
368 .map(|s| SubValue {
369 role: s.role.clone(),
370 gain_db: s.gain_db,
371 delay_ms: s.delay_ms,
372 polarity: s.polarity,
373 allpass: s.allpass.clone(),
374 mode: s.mode.clone(),
375 peq: s.peq.clone(),
376 })
377 .collect();
378 let [before, after] = each;
379 let [sum_before, sum_after] = sums;
380 Some(SubCombine {
381 subs,
382 grid,
383 before,
384 after,
385 sum_before,
386 sum_after,
387 band,
388 flat: (flat[0], flat[1]),
389 seat: (positions.len() > 1).then_some((seat[0], seat[1])),
390 positions: positions.len(),
391 values,
392 layout_info: sub.layout_info.clone(),
393 })
394}