use super::types::{
ChannelDspChain, DriverDspChain, DspChainOutput, MixedModeConfig, OptimizationMetadata,
PluginConfigWrapper,
};
use math_audio_iir_fir::Biquad;
use ndarray::Array1;
use serde_json::json;
use std::collections::HashMap;
use std::error::Error;
const DISPLAY_MIN_FREQ: f64 = math_audio_iir_fir::AUDIBLE_MIN_FREQ;
const DISPLAY_MAX_FREQ: f64 = math_audio_iir_fir::AUDIBLE_MAX_FREQ;
pub fn extend_curve_to_full_range(curve: &crate::Curve) -> crate::Curve {
if curve.freq.is_empty() {
return curve.clone();
}
let meas_min = curve.freq[0];
let meas_max = *curve.freq.last().unwrap();
if meas_min <= DISPLAY_MIN_FREQ * 1.05 && meas_max >= DISPLAY_MAX_FREQ * 0.95 {
return curve.clone();
}
let first_spl = curve.spl[0];
let last_spl = *curve.spl.last().unwrap();
let points_per_decade = 50;
let mut freq_vec = Vec::new();
let mut spl_vec = Vec::new();
if meas_min > DISPLAY_MIN_FREQ * 1.05 {
let log_start = DISPLAY_MIN_FREQ.log10();
let log_end = meas_min.log10();
let decades = log_end - log_start;
let n_points = ((decades * points_per_decade as f64).ceil() as usize).max(1);
for i in 0..n_points {
let t = i as f64 / n_points as f64;
let f = 10f64.powf(log_start + t * (log_end - log_start));
freq_vec.push(f);
spl_vec.push(first_spl);
}
}
freq_vec.extend(curve.freq.iter());
spl_vec.extend(curve.spl.iter());
if meas_max < DISPLAY_MAX_FREQ * 0.95 {
let log_start = meas_max.log10();
let log_end = DISPLAY_MAX_FREQ.log10();
let decades = log_end - log_start;
let n_points = ((decades * points_per_decade as f64).ceil() as usize).max(1);
for i in 1..=n_points {
let t = i as f64 / n_points as f64;
let f = 10f64
.powf(log_start + t * (log_end - log_start))
.min(DISPLAY_MAX_FREQ);
freq_vec.push(f);
spl_vec.push(last_spl);
}
}
crate::Curve {
freq: Array1::from(freq_vec),
spl: Array1::from(spl_vec),
phase: None,
}
}
fn biquad_to_json(biquad: &Biquad) -> serde_json::Value {
json!({
"filter_type": biquad.filter_type.long_name().to_lowercase(),
"freq": biquad.freq,
"q": biquad.q,
"db_gain": biquad.db_gain,
})
}
pub fn create_gain_plugin(gain_db: f64) -> PluginConfigWrapper {
PluginConfigWrapper {
plugin_type: "gain".to_string(),
parameters: json!({
"gain_db": gain_db
}),
}
}
pub fn create_gain_plugin_with_invert(gain_db: f64, invert: bool) -> PluginConfigWrapper {
PluginConfigWrapper {
plugin_type: "gain".to_string(),
parameters: json!({
"gain_db": gain_db,
"invert": invert
}),
}
}
pub fn create_eq_plugin(filters: &[Biquad]) -> PluginConfigWrapper {
let filter_configs: Vec<serde_json::Value> = filters.iter().map(biquad_to_json).collect();
PluginConfigWrapper {
plugin_type: "eq".to_string(),
parameters: json!({
"filters": filter_configs
}),
}
}
pub fn create_labeled_eq_plugin(filters: &[Biquad], label: &str) -> PluginConfigWrapper {
let filter_configs: Vec<serde_json::Value> = filters.iter().map(biquad_to_json).collect();
PluginConfigWrapper {
plugin_type: "eq".to_string(),
parameters: json!({
"label": label,
"filters": filter_configs
}),
}
}
pub fn create_crossover_plugin(
crossover_type: &str,
frequency: f64,
output: &str, ) -> PluginConfigWrapper {
PluginConfigWrapper {
plugin_type: "crossover".to_string(),
parameters: json!({
"type": crossover_type,
"frequency": frequency,
"output": output
}),
}
}
fn get_driver_name(index: usize, n_drivers: usize) -> String {
match (n_drivers, index) {
(2, 0) => "woofer",
(2, 1) => "tweeter",
(3, 0) => "woofer",
(3, 1) => "midrange",
(3, 2) => "tweeter",
(4, 0) => "woofer",
(4, 1) => "lower_midrange",
(4, 2) => "upper_midrange",
(4, 3) => "tweeter",
_ => return format!("driver_{}", index),
}
.to_string()
}
pub fn build_channel_dsp_chain(
channel_name: &str,
gain_db: Option<f64>,
crossovers: Vec<PluginConfigWrapper>,
eq_filters: &[Biquad],
) -> ChannelDspChain {
build_channel_dsp_chain_with_curves(channel_name, gain_db, crossovers, eq_filters, None, None)
}
pub fn build_channel_dsp_chain_with_curves(
channel_name: &str,
gain_db: Option<f64>,
crossovers: Vec<PluginConfigWrapper>,
eq_filters: &[Biquad],
initial_curve: Option<&crate::Curve>,
final_curve: Option<&crate::Curve>,
) -> ChannelDspChain {
let mut plugins = Vec::new();
if let Some(gain) = gain_db
&& gain.abs() > 0.01
{
plugins.push(create_gain_plugin(gain));
}
plugins.extend(crossovers);
if !eq_filters.is_empty() {
plugins.push(create_eq_plugin(eq_filters));
}
ChannelDspChain {
channel: channel_name.to_string(),
plugins,
drivers: None,
initial_curve: initial_curve.map(|c| c.into()),
final_curve: final_curve.map(|c| c.into()),
eq_response: None,
pre_ir: None,
post_ir: None,
target_curve: None,
}
}
pub fn create_delay_plugin(delay_ms: f64) -> PluginConfigWrapper {
PluginConfigWrapper {
plugin_type: "delay".to_string(),
parameters: json!({
"delay_ms": delay_ms
}),
}
}
pub fn create_convolution_plugin(wav_path: &str) -> PluginConfigWrapper {
PluginConfigWrapper {
plugin_type: "convolution".to_string(),
parameters: json!({
"ir_file": wav_path
}),
}
}
#[allow(clippy::too_many_arguments)]
pub fn build_multidriver_dsp_chain(
channel_name: &str,
gains: &[f64],
delays: &[f64],
inverts: Option<&[bool]>,
crossover_freqs: &[f64],
crossover_type: &str,
eq_filters: &[Biquad],
driver_eqs: Option<&[Vec<Biquad>]>,
) -> ChannelDspChain {
build_multidriver_dsp_chain_with_curves(
channel_name,
gains,
delays,
inverts,
crossover_freqs,
crossover_type,
eq_filters,
driver_eqs,
None,
None,
None,
)
}
#[allow(clippy::too_many_arguments)]
pub fn build_multidriver_dsp_chain_with_curves(
channel_name: &str,
gains: &[f64],
delays: &[f64],
inverts: Option<&[bool]>,
crossover_freqs: &[f64],
crossover_type: &str,
eq_filters: &[Biquad],
driver_eqs: Option<&[Vec<Biquad>]>,
initial_curve: Option<&crate::Curve>,
final_curve: Option<&crate::Curve>,
driver_initial_curves: Option<&[crate::Curve]>,
) -> ChannelDspChain {
let n_drivers = gains.len();
let mut driver_chains = Vec::new();
for i in 0..n_drivers {
let mut driver_plugins = Vec::new();
let invert = inverts.and_then(|inv| inv.get(i)).copied().unwrap_or(false);
if invert || gains[i].abs() > 0.01 {
if invert {
driver_plugins.push(create_gain_plugin_with_invert(gains[i], true));
} else {
driver_plugins.push(create_gain_plugin(gains[i]));
}
}
if i < delays.len() && delays[i].abs() > 0.001 {
driver_plugins.push(create_delay_plugin(delays[i]));
}
if let Some(eqs) = driver_eqs
&& let Some(filters) = eqs.get(i)
&& !filters.is_empty()
{
driver_plugins.push(create_eq_plugin(filters));
}
if i > 0 {
let crossover_freq = crossover_freqs[i - 1];
driver_plugins.push(create_crossover_plugin(
crossover_type,
crossover_freq,
"high",
));
}
if i < n_drivers - 1 {
let crossover_freq = crossover_freqs[i];
driver_plugins.push(create_crossover_plugin(
crossover_type,
crossover_freq,
"low",
));
}
let driver_curve = driver_initial_curves
.and_then(|curves| curves.get(i))
.map(|c| c.into());
driver_chains.push(DriverDspChain {
name: get_driver_name(i, n_drivers),
index: i,
plugins: driver_plugins,
initial_curve: driver_curve,
});
}
let mut combined_plugins = Vec::new();
if !eq_filters.is_empty() {
combined_plugins.push(create_eq_plugin(eq_filters));
}
ChannelDspChain {
channel: channel_name.to_string(),
plugins: combined_plugins,
drivers: Some(driver_chains),
initial_curve: initial_curve.map(|c| c.into()),
final_curve: final_curve.map(|c| c.into()),
eq_response: None,
pre_ir: None,
post_ir: None,
target_curve: None,
}
}
pub fn build_multisub_dsp_chain(
channel_name: &str,
group_name: &str,
n_subs: usize,
gains: &[f64],
delays: &[f64],
eq_filters: &[Biquad],
) -> ChannelDspChain {
build_multisub_dsp_chain_with_curves(
channel_name,
group_name,
n_subs,
gains,
delays,
eq_filters,
None,
None,
None,
)
}
pub fn build_multisub_dsp_chain_with_curves(
channel_name: &str,
group_name: &str,
n_subs: usize,
gains: &[f64],
delays: &[f64],
eq_filters: &[Biquad],
initial_curve: Option<&crate::Curve>,
final_curve: Option<&crate::Curve>,
driver_initial_curves: Option<&[crate::Curve]>,
) -> ChannelDspChain {
build_multisub_dsp_chain_with_allpass(
channel_name,
group_name,
n_subs,
gains,
delays,
eq_filters,
initial_curve,
final_curve,
driver_initial_curves,
None,
48000.0, )
}
pub fn build_multisub_dsp_chain_with_allpass(
channel_name: &str,
group_name: &str,
n_subs: usize,
gains: &[f64],
delays: &[f64],
eq_filters: &[Biquad],
initial_curve: Option<&crate::Curve>,
final_curve: Option<&crate::Curve>,
driver_initial_curves: Option<&[crate::Curve]>,
allpass_filters: Option<&[(f64, f64)]>,
sample_rate: f64,
) -> ChannelDspChain {
let mut driver_chains = Vec::new();
for i in 0..n_subs {
let mut sub_plugins = Vec::new();
if i < gains.len() && gains[i].abs() > 0.01 {
sub_plugins.push(create_gain_plugin(gains[i]));
}
if i < delays.len() && delays[i].abs() > 0.001 {
sub_plugins.push(create_delay_plugin(delays[i]));
}
if let Some(ap_filters) = allpass_filters
&& let Some(&(freq, q)) = ap_filters.get(i)
{
let ap_biquad = Biquad::new(
math_audio_iir_fir::BiquadFilterType::AllPass,
freq,
sample_rate,
q,
0.0,
);
sub_plugins.push(create_eq_plugin(&[ap_biquad]));
}
let driver_curve = driver_initial_curves
.and_then(|curves| curves.get(i))
.map(|c| c.into());
driver_chains.push(DriverDspChain {
name: format!("{}_{}", group_name, i + 1),
index: i,
plugins: sub_plugins,
initial_curve: driver_curve,
});
}
let mut combined_plugins = Vec::new();
if !eq_filters.is_empty() {
combined_plugins.push(create_eq_plugin(eq_filters));
}
ChannelDspChain {
channel: channel_name.to_string(),
plugins: combined_plugins,
drivers: Some(driver_chains),
initial_curve: initial_curve.map(|c| c.into()),
final_curve: final_curve.map(|c| c.into()),
eq_response: None,
pre_ir: None,
post_ir: None,
target_curve: None,
}
}
pub fn build_dba_dsp_chain(
channel_name: &str,
gains: &[f64],
delays: &[f64],
eq_filters: &[Biquad],
) -> ChannelDspChain {
build_dba_dsp_chain_with_curves(channel_name, gains, delays, eq_filters, None, None, None)
}
pub fn build_dba_dsp_chain_with_curves(
channel_name: &str,
gains: &[f64],
delays: &[f64],
eq_filters: &[Biquad],
initial_curve: Option<&crate::Curve>,
final_curve: Option<&crate::Curve>,
driver_initial_curves: Option<&[crate::Curve]>,
) -> ChannelDspChain {
let mut driver_chains = Vec::new();
let mut front_plugins = Vec::new();
if gains[0].abs() > 0.01 {
front_plugins.push(create_gain_plugin(gains[0]));
}
if delays[0].abs() > 0.001 {
front_plugins.push(create_delay_plugin(delays[0]));
}
let front_curve = driver_initial_curves
.and_then(|curves| curves.first())
.map(|c| c.into());
driver_chains.push(DriverDspChain {
name: "Front Array".to_string(),
index: 0,
plugins: front_plugins,
initial_curve: front_curve,
});
let mut rear_plugins = Vec::new();
rear_plugins.push(create_gain_plugin_with_invert(gains[1], true));
if delays[1].abs() > 0.001 {
rear_plugins.push(create_delay_plugin(delays[1]));
}
let rear_curve = driver_initial_curves
.and_then(|curves| curves.get(1))
.map(|c| c.into());
driver_chains.push(DriverDspChain {
name: "Rear Array".to_string(),
index: 1,
plugins: rear_plugins,
initial_curve: rear_curve,
});
let mut combined_plugins = Vec::new();
if !eq_filters.is_empty() {
combined_plugins.push(create_eq_plugin(eq_filters));
}
ChannelDspChain {
channel: channel_name.to_string(),
plugins: combined_plugins,
drivers: Some(driver_chains),
initial_curve: initial_curve.map(|c| c.into()),
final_curve: final_curve.map(|c| c.into()),
eq_response: None,
pre_ir: None,
post_ir: None,
target_curve: None,
}
}
pub fn build_cardioid_dsp_chain_with_curves(
channel_name: &str,
gains: &[f64],
delays: &[f64],
eq_filters: &[Biquad],
initial_curve: Option<&crate::Curve>,
final_curve: Option<&crate::Curve>,
driver_initial_curves: Option<&[crate::Curve]>,
) -> ChannelDspChain {
let mut driver_chains = Vec::new();
let mut front_plugins = Vec::new();
if gains[0].abs() > 0.01 {
front_plugins.push(create_gain_plugin(gains[0]));
}
if delays[0].abs() > 0.001 {
front_plugins.push(create_delay_plugin(delays[0]));
}
let front_curve = driver_initial_curves
.and_then(|curves| curves.first())
.map(|c| c.into());
driver_chains.push(DriverDspChain {
name: "Front Sub".to_string(),
index: 0,
plugins: front_plugins,
initial_curve: front_curve,
});
let mut rear_plugins = Vec::new();
rear_plugins.push(create_gain_plugin_with_invert(gains[1], true));
if delays[1].abs() > 0.001 {
rear_plugins.push(create_delay_plugin(delays[1]));
}
let rear_curve = driver_initial_curves
.and_then(|curves| curves.get(1))
.map(|c| c.into());
driver_chains.push(DriverDspChain {
name: "Rear Sub".to_string(),
index: 1,
plugins: rear_plugins,
initial_curve: rear_curve,
});
let mut combined_plugins = Vec::new();
if !eq_filters.is_empty() {
combined_plugins.push(create_eq_plugin(eq_filters));
}
ChannelDspChain {
channel: channel_name.to_string(),
plugins: combined_plugins,
drivers: Some(driver_chains),
initial_curve: initial_curve.map(|c| c.into()),
final_curve: final_curve.map(|c| c.into()),
eq_response: None,
pre_ir: None,
post_ir: None,
target_curve: None,
}
}
pub fn create_dsp_chain_output(
channels: HashMap<String, ChannelDspChain>,
metadata: Option<OptimizationMetadata>,
) -> DspChainOutput {
DspChainOutput {
version: super::types::default_config_version(),
channels,
metadata,
}
}
pub fn compute_eq_response(
initial: &super::types::CurveData,
final_curve: &super::types::CurveData,
) -> super::types::CurveData {
let spl: Vec<f64> = final_curve
.spl
.iter()
.zip(initial.spl.iter())
.map(|(&f, &i)| f - i)
.collect();
super::types::CurveData {
freq: initial.freq.clone(),
spl,
phase: None,
norm_range: None,
}
}
pub fn save_dsp_chain(
output: &DspChainOutput,
path: &std::path::Path,
) -> Result<(), Box<dyn Error>> {
let json = serde_json::to_string_pretty(output)?;
std::fs::write(path, json)?;
Ok(())
}
pub fn add_delay_plugin(chain: &mut ChannelDspChain, delay_ms: f64) {
let plugin = create_delay_plugin(delay_ms);
chain.plugins.insert(0, plugin);
}
pub fn create_band_split_plugin(frequency: f64, crossover_type: &str) -> PluginConfigWrapper {
PluginConfigWrapper {
plugin_type: "band_split".to_string(),
parameters: json!({
"frequency": frequency,
"type": crossover_type
}),
}
}
pub fn create_band_merge_plugin(bands: usize) -> PluginConfigWrapper {
PluginConfigWrapper {
plugin_type: "band_merge".to_string(),
parameters: json!({
"bands": bands
}),
}
}
pub fn build_mixed_mode_crossover_chain(
channel_name: &str,
mixed_config: &MixedModeConfig,
eq_filters: &[Biquad],
fir_wav_path: &str,
fir_uses_low: bool,
initial_curve: Option<&crate::Curve>,
) -> ChannelDspChain {
let mut plugins = Vec::new();
plugins.push(create_band_split_plugin(
mixed_config.crossover_freq,
&mixed_config.crossover_type,
));
let fir_plugin = PluginConfigWrapper {
plugin_type: "convolution".to_string(),
parameters: json!({
"ir_file": fir_wav_path,
"channels": if fir_uses_low { [0, 1] } else { [2, 3] }
}),
};
plugins.push(fir_plugin);
if !eq_filters.is_empty() {
let filter_configs: Vec<serde_json::Value> = eq_filters
.iter()
.map(|biquad| {
json!({
"filter_type": biquad.filter_type.long_name().to_lowercase(),
"freq": biquad.freq,
"q": biquad.q,
"db_gain": biquad.db_gain,
})
})
.collect();
let eq_plugin = PluginConfigWrapper {
plugin_type: "eq".to_string(),
parameters: json!({
"filters": filter_configs,
"channels": if fir_uses_low { [2, 3] } else { [0, 1] }
}),
};
plugins.push(eq_plugin);
}
plugins.push(create_band_merge_plugin(2));
ChannelDspChain {
channel: channel_name.to_string(),
plugins,
drivers: None,
initial_curve: initial_curve.map(|c| c.into()),
final_curve: None, eq_response: None,
pre_ir: None,
post_ir: None,
target_curve: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use math_audio_iir_fir::BiquadFilterType;
#[test]
fn test_create_gain_plugin() {
let plugin = create_gain_plugin(-3.5);
assert_eq!(plugin.plugin_type, "gain");
assert_eq!(
plugin.parameters.get("gain_db").unwrap().as_f64().unwrap(),
-3.5
);
}
#[test]
fn test_create_gain_plugin_with_invert() {
let plugin = create_gain_plugin_with_invert(-2.0, true);
assert_eq!(plugin.plugin_type, "gain");
assert_eq!(
plugin.parameters.get("gain_db").unwrap().as_f64().unwrap(),
-2.0
);
assert!(plugin.parameters.get("invert").unwrap().as_bool().unwrap());
let plugin_no_invert = create_gain_plugin_with_invert(1.5, false);
assert!(
!plugin_no_invert
.parameters
.get("invert")
.unwrap()
.as_bool()
.unwrap()
);
}
#[test]
fn test_create_eq_plugin() {
let sample_rate = 48000.0;
let filters = vec![
Biquad::new(BiquadFilterType::Peak, 1000.0, sample_rate, 2.0, -3.0),
Biquad::new(BiquadFilterType::Peak, 4000.0, sample_rate, 1.5, 2.0),
];
let plugin = create_eq_plugin(&filters);
assert_eq!(plugin.plugin_type, "eq");
let filters_arr = plugin
.parameters
.get("filters")
.unwrap()
.as_array()
.unwrap();
assert_eq!(filters_arr.len(), 2);
let first_filter = &filters_arr[0];
assert_eq!(first_filter.get("freq").unwrap().as_f64().unwrap(), 1000.0);
assert_eq!(first_filter.get("q").unwrap().as_f64().unwrap(), 2.0);
assert_eq!(first_filter.get("db_gain").unwrap().as_f64().unwrap(), -3.0);
}
#[test]
fn test_create_crossover_plugin() {
let plugin = create_crossover_plugin("LR24", 2500.0, "low");
assert_eq!(plugin.plugin_type, "crossover");
assert_eq!(
plugin.parameters.get("type").unwrap().as_str().unwrap(),
"LR24"
);
assert_eq!(
plugin
.parameters
.get("frequency")
.unwrap()
.as_f64()
.unwrap(),
2500.0
);
assert_eq!(
plugin.parameters.get("output").unwrap().as_str().unwrap(),
"low"
);
}
#[test]
fn test_create_delay_plugin() {
let plugin = create_delay_plugin(15.5);
assert_eq!(plugin.plugin_type, "delay");
assert_eq!(
plugin.parameters.get("delay_ms").unwrap().as_f64().unwrap(),
15.5
);
}
#[test]
fn test_create_convolution_plugin() {
let plugin = create_convolution_plugin("left_fir.wav");
assert_eq!(plugin.plugin_type, "convolution");
assert_eq!(
plugin.parameters.get("ir_file").unwrap().as_str().unwrap(),
"left_fir.wav"
);
}
#[test]
fn test_build_channel_dsp_chain_with_gain_and_eq() {
let sample_rate = 48000.0;
let filters = vec![Biquad::new(
BiquadFilterType::Peak,
1000.0,
sample_rate,
2.0,
-3.0,
)];
let chain = build_channel_dsp_chain("left", Some(-2.5), Vec::new(), &filters);
assert_eq!(chain.channel, "left");
assert_eq!(chain.plugins.len(), 2); assert_eq!(chain.plugins[0].plugin_type, "gain");
assert_eq!(chain.plugins[1].plugin_type, "eq");
assert!(chain.drivers.is_none());
}
#[test]
fn test_build_channel_dsp_chain_zero_gain_not_added() {
let chain = build_channel_dsp_chain("test", Some(0.0), Vec::new(), &[]);
assert!(!chain.plugins.iter().any(|p| p.plugin_type == "gain"));
}
#[test]
fn test_build_channel_dsp_chain_small_gain_not_added() {
let chain = build_channel_dsp_chain("test", Some(0.005), Vec::new(), &[]);
assert!(!chain.plugins.iter().any(|p| p.plugin_type == "gain"));
}
#[test]
fn test_build_multidriver_dsp_chain_2way() {
let gains = vec![-3.0, 0.0];
let delays = vec![2.5, 0.0];
let crossover_freqs = vec![2500.0];
let chain = build_multidriver_dsp_chain(
"left",
&gains,
&delays,
None,
&crossover_freqs,
"LR24",
&[],
None,
);
assert_eq!(chain.channel, "left");
assert!(chain.drivers.is_some());
let drivers = chain.drivers.as_ref().unwrap();
assert_eq!(drivers.len(), 2);
let woofer = &drivers[0];
assert_eq!(woofer.name, "woofer");
assert_eq!(woofer.index, 0);
assert!(woofer.plugins.iter().any(|p| p.plugin_type == "gain"));
assert!(woofer.plugins.iter().any(|p| p.plugin_type == "delay"));
assert!(woofer.plugins.iter().any(|p| {
p.plugin_type == "crossover"
&& p.parameters.get("output").unwrap().as_str().unwrap() == "low"
}));
let tweeter = &drivers[1];
assert_eq!(tweeter.name, "tweeter");
assert_eq!(tweeter.index, 1);
assert!(tweeter.plugins.iter().any(|p| {
p.plugin_type == "crossover"
&& p.parameters.get("output").unwrap().as_str().unwrap() == "high"
}));
}
#[test]
fn test_build_multidriver_dsp_chain_3way() {
let gains = vec![0.0, -2.0, 1.0];
let delays = vec![0.0, 1.0, 2.0];
let crossover_freqs = vec![500.0, 3000.0];
let chain = build_multidriver_dsp_chain(
"center",
&gains,
&delays,
None,
&crossover_freqs,
"LR24",
&[],
None,
);
let drivers = chain.drivers.as_ref().unwrap();
assert_eq!(drivers.len(), 3);
assert_eq!(drivers[0].name, "woofer");
assert_eq!(drivers[1].name, "midrange");
assert_eq!(drivers[2].name, "tweeter");
let midrange = &drivers[1];
let has_highpass = midrange.plugins.iter().any(|p| {
p.plugin_type == "crossover"
&& p.parameters.get("output").unwrap().as_str().unwrap() == "high"
});
let has_lowpass = midrange.plugins.iter().any(|p| {
p.plugin_type == "crossover"
&& p.parameters.get("output").unwrap().as_str().unwrap() == "low"
});
assert!(has_highpass, "Midrange should have highpass crossover");
assert!(has_lowpass, "Midrange should have lowpass crossover");
}
#[test]
fn test_build_multisub_dsp_chain() {
let gains = vec![-2.0, 0.0, 1.0];
let delays = vec![0.0, 5.0, 10.0];
let chain = build_multisub_dsp_chain("lfe", "subs", 3, &gains, &delays, &[]);
assert_eq!(chain.channel, "lfe");
assert!(chain.drivers.is_some());
let drivers = chain.drivers.as_ref().unwrap();
assert_eq!(drivers.len(), 3);
assert_eq!(drivers[0].name, "subs_1");
assert_eq!(drivers[1].name, "subs_2");
assert_eq!(drivers[2].name, "subs_3");
assert!(drivers[1].plugins.iter().any(|p| p.plugin_type == "delay"));
}
#[test]
fn test_build_dba_dsp_chain() {
let gains = vec![0.0, -3.0];
let delays = vec![0.0, 5.0];
let chain = build_dba_dsp_chain("dba", &gains, &delays, &[]);
assert_eq!(chain.channel, "dba");
assert!(chain.drivers.is_some());
let drivers = chain.drivers.as_ref().unwrap();
assert_eq!(drivers.len(), 2);
let front = &drivers[0];
assert_eq!(front.name, "Front Array");
assert_eq!(front.index, 0);
let rear = &drivers[1];
assert_eq!(rear.name, "Rear Array");
assert_eq!(rear.index, 1);
let rear_gain = rear
.plugins
.iter()
.find(|p| p.plugin_type == "gain")
.expect("Rear should have gain plugin");
assert!(
rear_gain
.parameters
.get("invert")
.unwrap()
.as_bool()
.unwrap(),
"Rear should be inverted"
);
assert!(rear.plugins.iter().any(|p| p.plugin_type == "delay"));
}
#[test]
fn test_add_delay_plugin() {
let mut chain = ChannelDspChain {
channel: "test".to_string(),
plugins: vec![create_gain_plugin(-3.0)],
drivers: None,
initial_curve: None,
final_curve: None,
eq_response: None,
pre_ir: None,
post_ir: None,
target_curve: None,
};
add_delay_plugin(&mut chain, 10.0);
assert_eq!(chain.plugins.len(), 2);
assert_eq!(chain.plugins[0].plugin_type, "delay");
assert_eq!(chain.plugins[1].plugin_type, "gain");
}
#[test]
fn test_create_dsp_chain_output() {
let mut channels = HashMap::new();
channels.insert(
"left".to_string(),
build_channel_dsp_chain("left", Some(-2.0), Vec::new(), &[]),
);
let metadata = OptimizationMetadata {
pre_score: 5.0,
post_score: 2.0,
algorithm: "cobyla".to_string(),
iterations: 1000,
timestamp: "2025-01-01T00:00:00Z".to_string(),
inter_channel_deviation: None,
};
let output = create_dsp_chain_output(channels, Some(metadata));
assert!(output.channels.contains_key("left"));
assert!(output.metadata.is_some());
let meta = output.metadata.unwrap();
assert_eq!(meta.pre_score, 5.0);
assert_eq!(meta.post_score, 2.0);
}
#[test]
fn test_get_driver_name() {
assert_eq!(get_driver_name(0, 2), "woofer");
assert_eq!(get_driver_name(1, 2), "tweeter");
assert_eq!(get_driver_name(0, 3), "woofer");
assert_eq!(get_driver_name(1, 3), "midrange");
assert_eq!(get_driver_name(2, 3), "tweeter");
assert_eq!(get_driver_name(0, 4), "woofer");
assert_eq!(get_driver_name(1, 4), "lower_midrange");
assert_eq!(get_driver_name(2, 4), "upper_midrange");
assert_eq!(get_driver_name(3, 4), "tweeter");
assert_eq!(get_driver_name(5, 8), "driver_5");
}
#[test]
fn test_extend_curve_to_full_range_already_full() {
let curve = crate::Curve {
freq: Array1::from(vec![20.0, 100.0, 1000.0, 10000.0, 20000.0]),
spl: Array1::from(vec![0.0, 1.0, 2.0, 1.0, 0.0]),
phase: None,
};
let extended = extend_curve_to_full_range(&curve);
assert_eq!(extended.freq.len(), curve.freq.len());
}
#[test]
fn test_extend_curve_to_full_range_narrow() {
let curve = crate::Curve {
freq: Array1::from(vec![100.0, 200.0, 300.0, 400.0, 500.0]),
spl: Array1::from(vec![-5.0, -3.0, 0.0, -2.0, -4.0]),
phase: None,
};
let extended = extend_curve_to_full_range(&curve);
assert!(extended.freq.len() > curve.freq.len());
assert!(extended.freq[0] < 25.0);
assert!(extended.freq[0] >= 20.0);
let last = *extended.freq.last().unwrap();
assert!(last > 19000.0);
assert!(last <= 20000.0);
assert_eq!(extended.spl[0], -5.0);
assert_eq!(*extended.spl.last().unwrap(), -4.0);
let orig_start = extended.freq.iter().position(|&f| f == 100.0).unwrap();
assert_eq!(extended.spl[orig_start], -5.0);
}
#[test]
fn test_extend_curve_to_full_range_empty() {
let curve = crate::Curve {
freq: Array1::from(vec![]),
spl: Array1::from(vec![]),
phase: None,
};
let extended = extend_curve_to_full_range(&curve);
assert!(extended.freq.is_empty());
}
#[test]
fn test_multisub_allpass_chain_has_eq_plugin_per_sub() {
let chain = build_multisub_dsp_chain_with_allpass(
"LFE",
"subs",
2,
&[0.0, -3.0],
&[0.0, 2.0],
&[],
None,
None,
None,
Some(&[(60.0, 1.5), (80.0, 2.0)]),
96000.0,
);
let drivers = chain.drivers.unwrap();
assert_eq!(drivers.len(), 2);
assert_eq!(
drivers[0].plugins.len(),
1,
"Sub 0 should have 1 plugin (allpass), got {}",
drivers[0].plugins.len()
);
assert_eq!(drivers[0].plugins[0].plugin_type, "eq");
assert_eq!(
drivers[1].plugins.len(),
3,
"Sub 1 should have 3 plugins (gain+delay+allpass), got {}",
drivers[1].plugins.len()
);
}
}