use crate::{
C, Design, LayoutInfo,
chain::{BaRow, term},
grid::design_freqs,
};
use secs_dsp::{
interp, pairwise_sum,
smooth::{Fraction, PowerSmoother},
};
use secs_types::chain::{Allpass, Peq};
#[derive(Debug, Clone, PartialEq)]
pub struct XoCurves {
pub grid: Vec<f64>,
pub main: Vec<f64>,
pub sub: Vec<f64>,
pub sum: Vec<f64>,
pub target: Vec<f64>,
}
#[derive(Debug)]
struct CachedMain {
role: String,
raw: Vec<C>,
corrected: Vec<C>,
correction: std::sync::Arc<[C]>,
}
#[derive(Debug)]
pub struct XoPreview {
sub: Vec<C>,
mains: Vec<CachedMain>,
grid: Vec<f64>,
target: Vec<f64>,
smoother: PowerSmoother,
}
impl XoPreview {
pub fn new(design: &Design) -> Self {
let f = design_freqs();
let pos = design.spectra.main_position;
let mut sub = vec![C::new(0.0, 0.0); f.len()];
if let Some(s) = &design.subs {
let terms: Vec<_> = s
.rows
.iter()
.map(|(r, row)| {
(
design
.spectra
.used
.get(pos, r)
.expect("reference sub")
.as_ref(),
row,
)
})
.collect();
sub = crate::chain::sub_sum(&terms, f);
if let Some(bus) = &s.bus_response {
for (h, b) in sub.iter_mut().zip(bus.iter()) {
*h *= b;
}
}
}
let mains = design
.mains
.iter()
.filter_map(|m| {
let h = design.spectra.raw.get(pos, &m.role)?;
let gain = C::new(10f64.powf(m.gain_db / 20.0), 0.0);
let raw: Vec<_> = h
.iter()
.zip(crate::measurement::delay_response(m.align_delay_ms))
.map(|(h, d)| h * d * gain)
.collect();
let corrected = raw
.iter()
.enumerate()
.map(|(i, h)| {
h * m.top_response.as_ref().map_or(C::new(1.0, 0.0), |p| p[i])
* m.response[i]
})
.collect();
Some(CachedMain {
role: m.role.clone(),
raw,
corrected,
correction: m.response.clone(),
})
})
.collect();
let grid = geomspace(5.0, 500.0, 620);
let target = design
.target
.sample(&grid)
.iter()
.map(|t| t + design.ref_level)
.collect();
Self {
sub,
mains,
grid,
target,
smoother: PowerSmoother::new(f, Fraction::Scalar(12.0)).expect("design grid"),
}
}
pub fn curves(
&self,
design: &Design,
group: &str,
fc: f64,
corrected: bool,
role: Option<&str>,
) -> XoCurves {
let group_roles = design
.groups
.iter()
.find(|(g, _)| g == group)
.map(|(_, roles)| roles);
let mut mains: Vec<_> = self
.mains
.iter()
.filter(|m| group_roles.is_some_and(|rs| rs.contains(&m.role)))
.collect();
if let Some(roles) = group_roles {
mains.sort_by_key(|m| roles.iter().position(|r| r == &m.role).unwrap());
}
if let Some(role) = role
&& mains.iter().any(|m| m.role == role)
{
mains.retain(|m| m.role == role);
}
let f = design_freqs();
let hp = secs_dsp::response::lr4(f, fc, "HP");
let lp = secs_dsp::response::lr4(f, fc, "LP");
let mut main_power = vec![0.0; f.len()];
let mut sub_power = vec![0.0; f.len()];
let mut sum_power = vec![0.0; f.len()];
let power = |h: C| {
let a = h.re.hypot(h.im);
a * a
};
for i in 0..f.len() {
let sub = self.sub[i] * lp[i];
if mains.is_empty() {
sub_power[i] = power(sub);
sum_power[i] = power(sub);
}
for m in &mains {
let main = if corrected { m.corrected[i] } else { m.raw[i] } * hp[i];
let s = if corrected {
sub * m.correction[i]
} else {
sub
};
main_power[i] += power(main);
sub_power[i] += power(s);
sum_power[i] += power(main + s);
}
if !mains.is_empty() {
main_power[i] /= mains.len() as f64;
sub_power[i] /= mains.len() as f64;
sum_power[i] /= mains.len() as f64;
}
}
XoCurves {
grid: self.grid.clone(),
main: db(&main_power, &self.grid, &self.smoother),
sub: db(&sub_power, &self.grid, &self.smoother),
sum: db(&sum_power, &self.grid, &self.smoother),
target: self.target.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SubValue {
pub role: String,
pub gain_db: f64,
pub delay_ms: f64,
pub polarity: i8,
pub allpass: Vec<Allpass>,
pub mode: String,
pub peq: Vec<Peq>,
}
#[derive(Debug, Clone)]
pub struct SubCombine {
pub subs: Vec<String>,
pub grid: Vec<f64>,
pub before: Vec<(String, Vec<f64>)>,
pub after: Vec<(String, Vec<f64>)>,
pub sum_before: Vec<f64>,
pub sum_after: Vec<f64>,
pub band: (f64, f64),
pub flat: (f64, f64),
pub seat: Option<(f64, f64)>,
pub positions: usize,
pub values: Vec<SubValue>,
pub layout_info: Vec<(String, LayoutInfo)>,
}
fn geomspace(lo: f64, hi: f64, n: usize) -> Vec<f64> {
let start = lo.log10();
let step = (hi.log10() - start) / (n - 1) as f64;
let mut out: Vec<_> = (0..n)
.map(|i| 10f64.powf(start + i as f64 * step))
.collect();
out[0] = lo;
out[n - 1] = hi;
out
}
fn mean(v: &[f64]) -> f64 {
pairwise_sum(v) / v.len() as f64
}
fn std(v: &[f64]) -> f64 {
let m = mean(v);
mean(&v.iter().map(|x| (x - m) * (x - m)).collect::<Vec<_>>()).sqrt()
}
fn db(power: &[f64], grid: &[f64], smoother: &PowerSmoother) -> Vec<f64> {
let f = design_freqs();
let values: Vec<_> = smoother
.apply(power)
.expect("design grid")
.iter()
.map(|p| 10.0 * (p + 1e-30).log10())
.collect();
grid.iter().map(|&g| interp(g, f, &values)).collect()
}
fn curve(h: &[C], grid: &[f64], smoother: &PowerSmoother) -> Vec<f64> {
let power = &h
.iter()
.map(|h| {
let a = h.re.hypot(h.im);
a * a
})
.collect::<Vec<_>>();
db(power, grid, smoother)
}
pub fn sub_combine(design: &Design) -> Option<SubCombine> {
let sub = design.subs.as_ref()?;
if sub.rows.len() < 2 {
return None;
}
let subs: Vec<_> = sub.rows.iter().map(|(r, _)| r.clone()).collect();
let positions: Vec<_> = design
.spectra
.positions
.iter()
.copied()
.filter(|&p| subs.iter().all(|r| design.spectra.raw.get(p, r).is_some()))
.collect();
let grid = geomspace(5.0, 300.0, 540);
let max_xo = design
.xo
.iter()
.filter(|(group, hz)| group != "sub" && *hz != 0.0)
.map(|(_, hz)| *hz)
.reduce(f64::max)
.unwrap_or(100.0);
let band = (20.0, max_xo * 1.5);
let metric_grid = geomspace(band.0, band.1, 200);
let raw = BaRow {
gain_db: 0.0,
delay_ms: 0.0,
polarity: 1,
allpass: vec![],
};
let display_smoother =
PowerSmoother::new(design_freqs(), Fraction::Scalar(12.0)).expect("design grid");
let metric_smoother =
PowerSmoother::new(design_freqs(), Fraction::Scalar(6.0)).expect("design grid");
let mut each = [Vec::new(), Vec::new()];
let mut sums = [Vec::new(), Vec::new()];
let mut flat = [0.0; 2];
let mut seat = [0.0; 2];
for state in 0..2 {
let mut levels = Vec::new();
for &p in &positions {
let mut sum = vec![C::new(0.0, 0.0); design_freqs().len()];
for (role, row) in &sub.rows {
let spectra = if state == 0 {
&design.spectra.raw
} else {
&design.spectra.used
};
let h = spectra.get(p, role).expect("complete sub position");
let mut out = term(h, design_freqs(), if state == 0 { &raw } else { row });
if state == 1
&& let Some(bus) = &sub.bus_response
{
for (h, b) in out.iter_mut().zip(bus.iter()) {
*h *= b;
}
}
for (s, h) in sum.iter_mut().zip(&out) {
*s += h;
}
if p == design.spectra.main_position {
each[state].push((role.clone(), curve(&out, &grid, &display_smoother)));
}
}
let level = curve(&sum, &metric_grid, &metric_smoother);
if p == design.spectra.main_position {
flat[state] = std(&level);
sums[state] = curve(&sum, &grid, &display_smoother);
}
levels.push(level);
}
if positions.len() > 1 {
let deviations: Vec<_> = (0..metric_grid.len())
.map(|i| {
let m = levels.iter().fold(0.0, |s, v| s + v[i]) / levels.len() as f64;
(levels.iter().fold(0.0, |s, v| s + (v[i] - m).powi(2)) / levels.len() as f64)
.sqrt()
})
.collect();
seat[state] = mean(&deviations);
}
}
let values = design
.chain
.subs
.iter()
.map(|s| SubValue {
role: s.role.clone(),
gain_db: s.gain_db,
delay_ms: s.delay_ms,
polarity: s.polarity,
allpass: s.allpass.clone(),
mode: s.mode.clone(),
peq: s.peq.clone(),
})
.collect();
let [before, after] = each;
let [sum_before, sum_after] = sums;
Some(SubCombine {
subs,
grid,
before,
after,
sum_before,
sum_after,
band,
flat: (flat[0], flat[1]),
seat: (positions.len() > 1).then_some((seat[0], seat[1])),
positions: positions.len(),
values,
layout_info: sub.layout_info.clone(),
})
}