use crate::Curve;
use crate::error::{AutoeqError, Result};
use log::{debug, info, warn};
use ndarray::Array1;
use num_complex::Complex64;
use std::f64::consts::PI;
use super::types::{MultiSeatConfig, MultiSeatStrategy};
#[derive(Debug, Clone)]
pub struct MultiSeatOptimizationResult {
pub gains: Vec<f64>,
pub delays: Vec<f64>,
pub variance_before: f64,
pub variance_after: f64,
pub improvement_db: f64,
}
#[derive(Debug, Clone)]
pub struct MultiSeatMeasurements {
pub measurements: Vec<Vec<Curve>>,
pub num_subs: usize,
pub num_seats: usize,
}
impl MultiSeatMeasurements {
pub fn new(measurements: Vec<Vec<Curve>>) -> Result<Self> {
if measurements.is_empty() {
return Err(AutoeqError::InvalidConfiguration {
message: "At least one subwoofer required".to_string(),
});
}
let num_subs = measurements.len();
let num_seats = measurements[0].len();
for (i, sub_measurements) in measurements.iter().enumerate() {
if sub_measurements.len() != num_seats {
return Err(AutoeqError::InvalidConfiguration {
message: format!(
"Subwoofer {} has {} seats, expected {}",
i,
sub_measurements.len(),
num_seats
),
});
}
}
if num_seats < 2 {
return Err(AutoeqError::InvalidConfiguration {
message: "At least 2 seats required for multi-seat optimization".to_string(),
});
}
Ok(Self {
measurements,
num_subs,
num_seats,
})
}
}
pub fn optimize_multiseat(
measurements: &MultiSeatMeasurements,
config: &MultiSeatConfig,
freq_range: (f64, f64),
_sample_rate: f64,
) -> Result<MultiSeatOptimizationResult> {
let (min_freq, max_freq) = freq_range;
if config.strategy == MultiSeatStrategy::PrimaryWithConstraints
&& config.primary_seat >= measurements.num_seats
{
return Err(AutoeqError::InvalidConfiguration {
message: format!(
"primary_seat {} out of range (only {} seats)",
config.primary_seat, measurements.num_seats
),
});
}
let freqs = create_eval_frequency_grid(measurements, min_freq, max_freq);
let interpolated = interpolate_all_measurements(measurements, &freqs)?;
let initial_gains = vec![0.0; measurements.num_subs];
let initial_delays = vec![0.0; measurements.num_subs];
let variance_before = compute_seat_variance(
&interpolated,
&freqs,
&initial_gains,
&initial_delays,
min_freq,
max_freq,
);
info!(
" Initial variance across {} seats: {:.2} dB",
measurements.num_seats, variance_before
);
let (optimal_gains, optimal_delays) = match config.strategy {
MultiSeatStrategy::MinimizeVariance => optimize_minimize_variance(
&interpolated,
&freqs,
measurements.num_subs,
min_freq,
max_freq,
),
MultiSeatStrategy::Average => optimize_average_response(
&interpolated,
&freqs,
measurements.num_subs,
min_freq,
max_freq,
),
MultiSeatStrategy::PrimaryWithConstraints => optimize_primary_with_constraints(
&interpolated,
&freqs,
measurements.num_subs,
config.primary_seat,
config.max_deviation_db,
min_freq,
max_freq,
),
};
let variance_after = compute_seat_variance(
&interpolated,
&freqs,
&optimal_gains,
&optimal_delays,
min_freq,
max_freq,
);
let improvement_db = variance_before - variance_after;
info!(
" Optimized variance: {:.2} dB (improvement: {:.2} dB)",
variance_after, improvement_db
);
Ok(MultiSeatOptimizationResult {
gains: optimal_gains,
delays: optimal_delays,
variance_before,
variance_after,
improvement_db,
})
}
fn create_eval_frequency_grid(
measurements: &MultiSeatMeasurements,
min_freq: f64,
max_freq: f64,
) -> Array1<f64> {
let mut f_min = min_freq;
let mut f_max = max_freq;
for sub_measurements in &measurements.measurements {
for curve in sub_measurements {
f_min = f_min.max(*curve.freq.first().unwrap_or(&20.0));
f_max = f_max.min(*curve.freq.last().unwrap_or(&20000.0));
}
}
let num_points = 50; let log_min = f_min.log10();
let log_max = f_max.log10();
Array1::from_shape_fn(num_points, |i| {
let log_f = log_min + (log_max - log_min) * (i as f64 / (num_points - 1) as f64);
10.0_f64.powf(log_f)
})
}
fn interpolate_all_measurements(
measurements: &MultiSeatMeasurements,
freqs: &Array1<f64>,
) -> Result<Vec<Vec<Vec<Complex64>>>> {
let mut result = Vec::new();
for sub_measurements in &measurements.measurements {
let mut sub_interp = Vec::new();
for curve in sub_measurements {
let interp = interpolate_curve_to_grid(curve, freqs)?;
sub_interp.push(interp);
}
result.push(sub_interp);
}
Ok(result)
}
fn interpolate_curve_to_grid(curve: &Curve, freqs: &Array1<f64>) -> Result<Vec<Complex64>> {
if curve.phase.is_none() {
warn!(
"Curve has no phase data; assuming 0° phase. Complex summation will \
overstate coherent combination — provide phase measurements for \
accurate multi-seat optimization."
);
}
let mut result = Vec::with_capacity(freqs.len());
for &f in freqs.iter() {
let (lower_idx, upper_idx) = find_bracket_indices(&curve.freq, f);
let f_low = curve.freq[lower_idx];
let f_high = curve.freq[upper_idx];
let t = if f_high > f_low {
(f - f_low) / (f_high - f_low)
} else {
0.0
};
let spl_interp = curve.spl[lower_idx] + t * (curve.spl[upper_idx] - curve.spl[lower_idx]);
let phase_rad = if let Some(phase) = &curve.phase {
let mut diff = phase[upper_idx] - phase[lower_idx];
diff -= 360.0 * (diff / 360.0).round();
(phase[lower_idx] + t * diff).to_radians()
} else {
0.0
};
let magnitude = 10.0_f64.powf(spl_interp / 20.0);
result.push(Complex64::from_polar(magnitude, phase_rad));
}
Ok(result)
}
fn find_bracket_indices(freqs: &Array1<f64>, target: f64) -> (usize, usize) {
for i in 0..freqs.len().saturating_sub(1) {
if freqs[i] <= target && freqs[i + 1] >= target {
return (i, i + 1);
}
}
if target <= freqs[0] {
(0, 0)
} else {
let last = freqs.len().saturating_sub(1);
(last, last)
}
}
fn compute_combined_responses(
interpolated: &[Vec<Vec<Complex64>>], freqs: &Array1<f64>,
gains: &[f64],
delays: &[f64],
min_freq: f64,
max_freq: f64,
) -> Vec<Vec<f64>> {
let num_seats = interpolated[0].len();
let mut seat_responses: Vec<Vec<f64>> = Vec::with_capacity(num_seats);
for seat_idx in 0..num_seats {
let mut combined_spl = Vec::new();
for (freq_idx, &f) in freqs.iter().enumerate() {
if f < min_freq || f > max_freq {
continue;
}
let mut combined = Complex64::new(0.0, 0.0);
for (sub_idx, sub_data) in interpolated.iter().enumerate() {
let gain_linear = 10.0_f64.powf(gains[sub_idx] / 20.0);
let delay_s = delays[sub_idx] / 1000.0;
let omega = 2.0 * PI * f;
let delay_phase = Complex64::from_polar(1.0, -omega * delay_s);
combined += sub_data[seat_idx][freq_idx] * gain_linear * delay_phase;
}
combined_spl.push(20.0 * combined.norm().max(1e-12).log10());
}
seat_responses.push(combined_spl);
}
seat_responses
}
fn variance_from_responses(responses: &[Vec<f64>]) -> f64 {
let num_freqs = responses[0].len();
let mut total_std = 0.0;
for freq_idx in 0..num_freqs {
let mean: f64 =
responses.iter().map(|s| s[freq_idx]).sum::<f64>() / responses.len() as f64;
let variance =
responses.iter().map(|s| (s[freq_idx] - mean).powi(2)).sum::<f64>()
/ responses.len() as f64;
total_std += variance.sqrt();
}
total_std / num_freqs as f64
}
fn average_flatness_from_responses(responses: &[Vec<f64>]) -> f64 {
let num_freqs = responses[0].len();
let num_seats = responses.len() as f64;
let avg_spl: Vec<f64> = (0..num_freqs)
.map(|fi| responses.iter().map(|s| s[fi]).sum::<f64>() / num_seats)
.collect();
let mean = avg_spl.iter().sum::<f64>() / avg_spl.len() as f64;
let variance = avg_spl.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / avg_spl.len() as f64;
variance.sqrt()
}
fn primary_constrained_from_responses(
responses: &[Vec<f64>],
primary_seat: usize,
max_deviation_db: f64,
) -> f64 {
let num_freqs = responses[0].len();
let primary = &responses[primary_seat];
let mean = primary.iter().sum::<f64>() / primary.len() as f64;
let primary_flatness =
(primary.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / primary.len() as f64).sqrt();
let mut penalty_sum = 0.0;
let mut penalty_count = 0usize;
for (seat_idx, seat) in responses.iter().enumerate() {
if seat_idx == primary_seat {
continue;
}
for fi in 0..num_freqs {
let deviation = (seat[fi] - primary[fi]).abs();
if deviation > max_deviation_db {
penalty_sum += (deviation - max_deviation_db).powi(2);
}
penalty_count += 1;
}
}
let penalty = if penalty_count > 0 {
(penalty_sum / penalty_count as f64).sqrt()
} else {
0.0
};
primary_flatness + 10.0 * penalty
}
fn compute_seat_variance(
interpolated: &[Vec<Vec<Complex64>>],
freqs: &Array1<f64>,
gains: &[f64],
delays: &[f64],
min_freq: f64,
max_freq: f64,
) -> f64 {
let responses =
compute_combined_responses(interpolated, freqs, gains, delays, min_freq, max_freq);
variance_from_responses(&responses)
}
fn build_range(min: f64, max: f64, step: f64) -> Vec<f64> {
let n = ((max - min) / step).round() as usize + 1;
(0..n).map(|i| min + i as f64 * step).collect()
}
fn two_pass_search(
num_subs: usize,
eval: &dyn Fn(&[f64], &[f64]) -> f64,
) -> (Vec<f64>, Vec<f64>) {
let mut best_gains = vec![0.0; num_subs];
let mut best_delays = vec![0.0; num_subs];
let mut best_loss = eval(&best_gains, &best_delays);
let gain_range = build_range(-6.0, 6.0, 1.0);
let delay_range = build_range(0.0, 20.0, 1.0);
if num_subs == 2 {
for &g in &gain_range {
for &d in &delay_range {
let gains = vec![0.0, g];
let delays = vec![0.0, d];
let loss = eval(&gains, &delays);
if loss < best_loss {
best_loss = loss;
best_gains = gains;
best_delays = delays;
}
}
}
} else {
for _ in 0..5 {
for sub_idx in 1..num_subs {
for &g in &gain_range {
let mut test_gains = best_gains.clone();
test_gains[sub_idx] = g;
let loss = eval(&test_gains, &best_delays);
if loss < best_loss {
best_loss = loss;
best_gains = test_gains;
}
}
for &d in &delay_range {
let mut test_delays = best_delays.clone();
test_delays[sub_idx] = d;
let loss = eval(&best_gains, &test_delays);
if loss < best_loss {
best_loss = loss;
best_delays = test_delays;
}
}
}
}
}
let fine_passes = if num_subs == 2 { 1 } else { 5 };
for _ in 0..fine_passes {
for sub_idx in 1..num_subs {
let g_center = best_gains[sub_idx];
let d_center = best_delays[sub_idx];
let fine_gains = build_range(
(g_center - 2.0).max(-6.0),
(g_center + 2.0).min(6.0),
0.1,
);
let fine_delays = build_range(
(d_center - 2.0).max(0.0),
(d_center + 2.0).min(20.0),
0.1,
);
if num_subs == 2 {
for &g in &fine_gains {
for &d in &fine_delays {
let gains = vec![0.0, g];
let delays = vec![0.0, d];
let loss = eval(&gains, &delays);
if loss < best_loss {
best_loss = loss;
best_gains = gains;
best_delays = delays;
}
}
}
} else {
for &g in &fine_gains {
let mut test_gains = best_gains.clone();
test_gains[sub_idx] = g;
let loss = eval(&test_gains, &best_delays);
if loss < best_loss {
best_loss = loss;
best_gains = test_gains;
}
}
for &d in &fine_delays {
let mut test_delays = best_delays.clone();
test_delays[sub_idx] = d;
let loss = eval(&best_gains, &test_delays);
if loss < best_loss {
best_loss = loss;
best_delays = test_delays;
}
}
}
}
}
debug!(
" Search result: gains={:?}, delays={:?}, loss={:.4}",
best_gains, best_delays, best_loss
);
(best_gains, best_delays)
}
fn optimize_minimize_variance(
interpolated: &[Vec<Vec<Complex64>>],
freqs: &Array1<f64>,
num_subs: usize,
min_freq: f64,
max_freq: f64,
) -> (Vec<f64>, Vec<f64>) {
two_pass_search(num_subs, &|gains, delays| {
let r = compute_combined_responses(interpolated, freqs, gains, delays, min_freq, max_freq);
variance_from_responses(&r)
})
}
fn optimize_average_response(
interpolated: &[Vec<Vec<Complex64>>],
freqs: &Array1<f64>,
num_subs: usize,
min_freq: f64,
max_freq: f64,
) -> (Vec<f64>, Vec<f64>) {
two_pass_search(num_subs, &|gains, delays| {
let r = compute_combined_responses(interpolated, freqs, gains, delays, min_freq, max_freq);
average_flatness_from_responses(&r)
})
}
fn optimize_primary_with_constraints(
interpolated: &[Vec<Vec<Complex64>>],
freqs: &Array1<f64>,
num_subs: usize,
primary_seat: usize,
max_deviation_db: f64,
min_freq: f64,
max_freq: f64,
) -> (Vec<f64>, Vec<f64>) {
two_pass_search(num_subs, &|gains, delays| {
let r = compute_combined_responses(interpolated, freqs, gains, delays, min_freq, max_freq);
primary_constrained_from_responses(&r, primary_seat, max_deviation_db)
})
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_curve(spl_offset: f64, phase_offset: f64) -> Curve {
let freqs: Vec<f64> = (0..50)
.map(|i| 20.0 * (200.0 / 20.0_f64).powf(i as f64 / 49.0))
.collect();
let spl: Vec<f64> = freqs.iter().map(|_| 90.0 + spl_offset).collect();
let phase: Vec<f64> = freqs
.iter()
.map(|f| -180.0 * f / 100.0 + phase_offset)
.collect();
Curve {
freq: Array1::from(freqs),
spl: Array1::from(spl),
phase: Some(Array1::from(phase)),
..Default::default()
}
}
#[test]
fn test_multiseat_measurements_creation() {
let measurements = vec![
vec![create_test_curve(0.0, 0.0), create_test_curve(2.0, 10.0)],
vec![create_test_curve(-1.0, 5.0), create_test_curve(1.0, 15.0)],
];
let ms = MultiSeatMeasurements::new(measurements).expect("Should create successfully");
assert_eq!(ms.num_subs, 2);
assert_eq!(ms.num_seats, 2);
}
#[test]
fn test_multiseat_measurements_validation() {
let measurements = vec![
vec![create_test_curve(0.0, 0.0), create_test_curve(2.0, 10.0)],
vec![create_test_curve(-1.0, 5.0)], ];
let result = MultiSeatMeasurements::new(measurements);
assert!(result.is_err());
}
#[test]
fn test_primary_seat_out_of_range() {
let measurements = vec![
vec![create_test_curve(0.0, 0.0), create_test_curve(2.0, 10.0)],
vec![create_test_curve(-1.0, 5.0), create_test_curve(1.0, 15.0)],
];
let ms = MultiSeatMeasurements::new(measurements).expect("Should create");
let config = MultiSeatConfig {
enabled: true,
strategy: MultiSeatStrategy::PrimaryWithConstraints,
primary_seat: 5, max_deviation_db: 6.0,
};
let result = optimize_multiseat(&ms, &config, (20.0, 120.0), 48000.0);
assert!(result.is_err());
}
#[test]
fn test_optimize_multiseat_basic() {
let measurements = vec![
vec![create_test_curve(0.0, 0.0), create_test_curve(3.0, 20.0)],
vec![create_test_curve(0.0, 10.0), create_test_curve(-2.0, 30.0)],
];
let ms = MultiSeatMeasurements::new(measurements).expect("Should create");
let config = MultiSeatConfig {
enabled: true,
strategy: MultiSeatStrategy::MinimizeVariance,
primary_seat: 0,
max_deviation_db: 6.0,
};
let result =
optimize_multiseat(&ms, &config, (20.0, 120.0), 48000.0).expect("Should optimize");
assert_eq!(result.gains.len(), 2);
assert_eq!(result.delays.len(), 2);
assert_eq!(result.gains[0], 0.0);
assert_eq!(result.delays[0], 0.0);
}
#[test]
fn test_compute_seat_variance() {
let curve1 = create_test_curve(0.0, 0.0);
let curve2 = create_test_curve(0.0, 0.0);
let measurements = vec![vec![curve1.clone(), curve2.clone()]];
let ms = MultiSeatMeasurements::new(measurements).expect("Should create");
let freqs = create_eval_frequency_grid(&ms, 30.0, 120.0);
let interpolated = interpolate_all_measurements(&ms, &freqs).expect("Should interpolate");
let variance = compute_seat_variance(&interpolated, &freqs, &[0.0], &[0.0], 30.0, 120.0);
assert!(
variance < 0.01,
"Identical curves should have near-zero variance, got {}",
variance
);
}
#[test]
fn test_average_strategy_differs_from_minimize_variance() {
let make_curve = |spl_fn: &dyn Fn(f64) -> f64, phase_off: f64| -> Curve {
let freqs: Vec<f64> = (0..50)
.map(|i| 20.0 * (200.0 / 20.0_f64).powf(i as f64 / 49.0))
.collect();
let spl: Vec<f64> = freqs.iter().map(|f| spl_fn(*f)).collect();
let phase: Vec<f64> = freqs
.iter()
.map(|f| -180.0 * f / 100.0 + phase_off)
.collect();
Curve {
freq: Array1::from(freqs),
spl: Array1::from(spl),
phase: Some(Array1::from(phase)),
..Default::default()
}
};
let flat = |_f: f64| 90.0;
let dipped = |f: f64| if f < 60.0 { 84.0 } else { 90.0 };
let peaked = |f: f64| if f < 60.0 { 96.0 } else { 90.0 };
let measurements = vec![
vec![make_curve(&flat, 0.0), make_curve(&dipped, 10.0)],
vec![make_curve(&peaked, 5.0), make_curve(&flat, 15.0)],
];
let ms = MultiSeatMeasurements::new(measurements).expect("Should create");
let var_config = MultiSeatConfig {
enabled: true,
strategy: MultiSeatStrategy::MinimizeVariance,
primary_seat: 0,
max_deviation_db: 6.0,
};
let avg_config = MultiSeatConfig {
strategy: MultiSeatStrategy::Average,
..var_config.clone()
};
let var_result =
optimize_multiseat(&ms, &var_config, (20.0, 120.0), 48000.0).expect("var");
let avg_result =
optimize_multiseat(&ms, &avg_config, (20.0, 120.0), 48000.0).expect("avg");
assert_eq!(avg_result.gains.len(), 2);
assert_eq!(avg_result.delays.len(), 2);
assert_eq!(avg_result.gains[0], 0.0);
assert_eq!(avg_result.delays[0], 0.0);
assert!(var_result.improvement_db >= -0.01);
assert!(avg_result.improvement_db >= -1.0); }
#[test]
fn test_primary_with_constraints_favors_primary_seat() {
let make_curve = |spl_val: f64, phase_off: f64| -> Curve {
let freqs: Vec<f64> = (0..50)
.map(|i| 20.0 * (200.0 / 20.0_f64).powf(i as f64 / 49.0))
.collect();
let spl: Vec<f64> = freqs.iter().map(|_| spl_val).collect();
let phase: Vec<f64> = freqs
.iter()
.map(|f| -180.0 * f / 100.0 + phase_off)
.collect();
Curve {
freq: Array1::from(freqs),
spl: Array1::from(spl),
phase: Some(Array1::from(phase)),
..Default::default()
}
};
let measurements = vec![
vec![make_curve(90.0, 0.0), make_curve(85.0, 20.0)],
vec![make_curve(88.0, 10.0), make_curve(92.0, 30.0)],
];
let ms = MultiSeatMeasurements::new(measurements).expect("Should create");
let config = MultiSeatConfig {
enabled: true,
strategy: MultiSeatStrategy::PrimaryWithConstraints,
primary_seat: 0,
max_deviation_db: 6.0,
};
let result =
optimize_multiseat(&ms, &config, (20.0, 120.0), 48000.0).expect("Should optimize");
assert_eq!(result.gains.len(), 2);
assert_eq!(result.delays.len(), 2);
assert_eq!(result.gains[0], 0.0);
assert_eq!(result.delays[0], 0.0);
}
#[test]
fn test_phase_wrap_interpolation() {
let freqs = vec![50.0, 60.0, 70.0, 80.0];
let spl = vec![90.0, 90.0, 90.0, 90.0];
let phase = vec![170.0, 179.0, -179.0, -170.0];
let curve = Curve {
freq: Array1::from(freqs),
spl: Array1::from(spl),
phase: Some(Array1::from(phase)),
..Default::default()
};
let grid = Array1::from(vec![65.0]); let result = interpolate_curve_to_grid(&curve, &grid).expect("Should interpolate");
let phase_deg = result[0].arg().to_degrees();
assert!(
phase_deg.abs() > 170.0,
"Phase should be near ±180°, got {:.1}°",
phase_deg
);
}
#[test]
fn test_fine_resolution_finds_better_solution() {
let measurements = vec![
vec![create_test_curve(0.0, 0.0), create_test_curve(3.0, 20.0)],
vec![create_test_curve(0.0, 10.0), create_test_curve(-2.0, 30.0)],
];
let ms = MultiSeatMeasurements::new(measurements).expect("Should create");
let freqs = create_eval_frequency_grid(&ms, 20.0, 120.0);
let interpolated = interpolate_all_measurements(&ms, &freqs).expect("Should interpolate");
let (gains, delays) =
optimize_minimize_variance(&interpolated, &freqs, 2, 20.0, 120.0);
let has_fractional_gain = gains.iter().any(|g| (g * 10.0).fract().abs() > 0.001);
let has_fractional_delay = delays.iter().any(|d| (d * 10.0).fract().abs() > 0.001);
assert_eq!(gains[0], 0.0);
assert_eq!(delays[0], 0.0);
let _ = (has_fractional_gain, has_fractional_delay); }
}