use super::*;
use crate::MeasurementSource;
use crate::roomeq::optimize::optimize_room;
use crate::roomeq::types::{
BassManagementConfig, CrossoverConfig, MultiSeatConfig, MultiSeatStrategy, OptimizerConfig,
RoomConfig, SpeakerConfig, SubwooferSystemConfig, SystemConfig, SystemModel,
};
use ndarray::array;
use std::collections::HashMap;
fn phase_curve(phase_deg: f64) -> Curve {
Curve {
freq: array![40.0, 80.0, 160.0],
spl: array![0.0, 0.0, 0.0],
phase: Some(array![phase_deg, phase_deg, phase_deg]),
..Default::default()
}
}
fn bass_management_workflow_curve(
base_level: f64,
acoustic_delay_ms: f64,
with_phase: bool,
) -> Curve {
let n = 96;
let freq: Vec<f64> = (0..n)
.map(|i| 20.0 * (1000.0f64).powf(i as f64 / (n - 1) as f64))
.collect();
let spl: Vec<f64> = freq
.iter()
.map(|&f| {
let bass_shelf = if f < 80.0 {
-3.0 * (80.0 / f).log2().min(2.0)
} else {
0.0
};
base_level + bass_shelf
})
.collect();
let phase = with_phase.then(|| {
ndarray::Array1::from_vec(
freq.iter()
.map(|&f| -360.0 * f * acoustic_delay_ms / 1000.0)
.collect(),
)
});
Curve {
freq: ndarray::Array1::from_vec(freq),
spl: ndarray::Array1::from_vec(spl),
phase,
..Default::default()
}
}
fn bass_management_workflow_config(with_phase: bool, max_sub_boost_db: f64) -> RoomConfig {
let mut speakers = HashMap::new();
speakers.insert(
"left".to_string(),
SpeakerConfig::Single(MeasurementSource::InMemory(bass_management_workflow_curve(
76.0, 0.0, with_phase,
))),
);
speakers.insert(
"right".to_string(),
SpeakerConfig::Single(MeasurementSource::InMemory(bass_management_workflow_curve(
76.5, 0.1, with_phase,
))),
);
speakers.insert(
"sub".to_string(),
SpeakerConfig::Single(MeasurementSource::InMemory(bass_management_workflow_curve(
62.0, 3.0, with_phase,
))),
);
let mut system_speakers = HashMap::new();
system_speakers.insert("L".to_string(), "left".to_string());
system_speakers.insert("R".to_string(), "right".to_string());
system_speakers.insert("LFE".to_string(), "sub".to_string());
let mut crossovers = HashMap::new();
crossovers.insert(
"bass".to_string(),
CrossoverConfig {
crossover_type: "LR24".to_string(),
frequency: Some(80.0),
frequencies: None,
frequency_range: None,
},
);
RoomConfig {
version: "test".to_string(),
system: Some(SystemConfig {
model: SystemModel::HomeCinema,
speakers: system_speakers,
subwoofers: Some(SubwooferSystemConfig {
config: super::SubwooferStrategy::Single,
crossover: Some("bass".to_string()),
mapping: HashMap::new(),
}),
bass_management: Some(BassManagementConfig {
enabled: true,
redirect_bass: true,
max_sub_boost_db,
..BassManagementConfig::default()
}),
}),
speakers,
crossovers: Some(crossovers),
target_curve: None,
optimizer: OptimizerConfig {
num_filters: 1,
max_iter: 4,
population: 4,
seed: Some(7),
refine: false,
allow_delay: Some(false),
decomposed_correction: None,
min_freq: 20.0,
max_freq: 20_000.0,
..OptimizerConfig::default()
},
recording_config: None,
cea2034_cache: None,
}
}
fn home_cinema_multiseat_guardrail_config() -> RoomConfig {
let primary = bass_management_workflow_curve(76.0, 0.0, true);
let mut null_seat = bass_management_workflow_curve(76.0, 0.0, true);
for (freq, spl) in null_seat.freq.iter().zip(null_seat.spl.iter_mut()) {
if *freq >= 70.0 && *freq <= 140.0 {
*spl -= 24.0;
}
}
let mut speakers = HashMap::new();
speakers.insert(
"left".to_string(),
SpeakerConfig::Single(MeasurementSource::InMemoryMultiple(vec![
primary.clone(),
null_seat.clone(),
])),
);
speakers.insert(
"right".to_string(),
SpeakerConfig::Single(MeasurementSource::InMemoryMultiple(vec![
primary, null_seat,
])),
);
let mut system_speakers = HashMap::new();
system_speakers.insert("L".to_string(), "left".to_string());
system_speakers.insert("R".to_string(), "right".to_string());
RoomConfig {
version: "test".to_string(),
system: Some(SystemConfig {
model: SystemModel::HomeCinema,
speakers: system_speakers,
subwoofers: None,
bass_management: None,
}),
speakers,
crossovers: None,
target_curve: None,
optimizer: OptimizerConfig {
num_filters: 1,
max_iter: 3,
population: 4,
seed: Some(19),
refine: false,
allow_delay: Some(false),
decomposed_correction: None,
multi_seat: Some(MultiSeatConfig {
enabled: false,
strategy: MultiSeatStrategy::PrimaryWithConstraints,
primary_seat: 0,
max_deviation_db: 6.0,
..Default::default()
}),
min_freq: 40.0,
max_freq: 500.0,
..OptimizerConfig::default()
},
recording_config: None,
cea2034_cache: None,
}
}
fn total_delay_ms(chain: &ChannelDspChain) -> f64 {
chain
.plugins
.iter()
.filter(|p| p.plugin_type == "delay")
.map(|p| p.parameters["delay_ms"].as_f64().unwrap_or(0.0))
.sum()
}
fn has_crossover_plugin(chain: &ChannelDspChain, mode: &str) -> bool {
chain
.plugins
.iter()
.any(|p| p.plugin_type == "crossover" && p.parameters["output"].as_str() == Some(mode))
}
fn crossover_plugin_frequency(chain: &ChannelDspChain, mode: &str) -> Option<f64> {
chain.plugins.iter().find_map(|p| {
(p.plugin_type == "crossover" && p.parameters["output"].as_str() == Some(mode))
.then(|| p.parameters["frequency"].as_f64())
.flatten()
})
}
#[test]
fn home_cinema_bass_management_delays_are_normalized() {
let (main, sub) = normalize_crossover_delays(-1.5, 0.25);
assert_eq!(main, 0.0);
assert!((sub - 1.75).abs() < 1e-9);
let (main, sub) = normalize_crossover_delays(2.0, 5.0);
assert_eq!(main, 0.0);
assert_eq!(sub, 3.0);
}
#[test]
fn home_cinema_bass_management_delay_and_polarity_update_phase() {
let curve = phase_curve(0.0);
let adjusted = apply_delay_and_polarity_to_curve(&curve, 1.0, true);
let phase = adjusted.phase.expect("phase");
assert!((phase[0] - 165.6).abs() < 1e-6);
assert!((phase[1] - 151.2).abs() < 1e-6);
assert!((phase[2] - 122.4).abs() < 1e-6);
}
#[test]
fn home_cinema_bass_management_prediction_requires_phase() {
let main = phase_curve(0.0);
let mut sub = phase_curve(0.0);
sub.phase = None;
let predicted = predict_bass_management_sum(
&main, &sub, "LR24", 80.0, 48_000.0, 0.0, 0.0, 0.0, 0.0, false,
);
assert!(predicted.is_none());
}
#[test]
fn home_cinema_bass_management_alignment_requires_matching_grids() {
let main = phase_curve(0.0);
let mut shifted = phase_curve(0.0);
shifted.freq = array![41.0, 82.0, 164.0];
assert!(all_curves_have_usable_phase(&[&main, &shifted]));
assert!(!all_curves_share_frequency_grid(&[&main, &shifted]));
}
#[test]
fn cardioid_preprocess_rejects_mismatched_frequency_grids() {
let front = phase_curve(0.0);
let mut rear = phase_curve(0.0);
rear.freq = array![41.0, 82.0, 164.0];
let cardioid = CardioidConfig {
name: "cardioid".to_string(),
speaker_name: None,
front: MeasurementSource::InMemory(front),
rear: MeasurementSource::InMemory(rear),
separation_meters: 0.5,
};
match preprocess_cardioid(&cardioid) {
Ok(_) => panic!("cardioid preprocessing should reject mismatched frequency grids"),
Err(err) => assert!(
err.to_string().contains("same frequency grid"),
"unexpected error: {err}"
),
}
}
#[test]
fn home_cinema_bass_management_prediction_has_phase_when_valid() {
let main = phase_curve(0.0);
let sub = phase_curve(0.0);
let predicted = predict_bass_management_sum(
&main, &sub, "LR24", 80.0, 48_000.0, 0.0, 0.0, 0.0, 1.0, false,
)
.expect("prediction");
assert_eq!(predicted.freq.len(), main.freq.len());
assert!(predicted.phase.is_some());
assert!(bass_management_objective(Some(&predicted), 80.0).is_some());
}
#[test]
fn home_cinema_bass_management_sub_curve_is_predicted_from_routes() {
let sub = phase_curve(0.0);
let graph = crate::roomeq::home_cinema::BassManagementRoutingGraph {
physical_sub_output: "Sub".to_string(),
input_channels: vec!["L".to_string(), "R".to_string(), "Sub".to_string()],
output_channels: vec!["L".to_string(), "R".to_string(), "Sub".to_string()],
routes: vec![
crate::roomeq::home_cinema::BassManagementRoute {
group_id: Some("lcr".to_string()),
source_channel: "L".to_string(),
source_index: 0,
destination: "Sub".to_string(),
destination_index: 2,
pre_chain_channel: Some("Sub".to_string()),
post_chain_channel: Some("Sub".to_string()),
route_kind: "redirected_bass_lowpass_to_sub".to_string(),
crossover_type: "LR24".to_string(),
high_pass_hz: None,
low_pass_hz: Some(80.0),
gain_db: 0.0,
gain_linear: 1.0,
matrix_gain: 1.0,
delay_ms: 0.0,
polarity_inverted: false,
},
crate::roomeq::home_cinema::BassManagementRoute {
group_id: Some("surround".to_string()),
source_channel: "R".to_string(),
source_index: 1,
destination: "Sub".to_string(),
destination_index: 2,
pre_chain_channel: Some("Sub".to_string()),
post_chain_channel: Some("Sub".to_string()),
route_kind: "redirected_bass_lowpass_to_sub".to_string(),
crossover_type: "BW12".to_string(),
high_pass_hz: None,
low_pass_hz: Some(120.0),
gain_db: -6.0,
gain_linear: 10.0_f64.powf(-6.0 / 20.0),
matrix_gain: 1.0,
delay_ms: 2.0,
polarity_inverted: true,
},
],
matrix: None,
advisories: vec!["ok".to_string()],
};
let predicted = predict_bass_output_curve_from_routes(&sub, &graph, "Sub", 48_000.0)
.expect("route-predicted sub curve");
let single_route_graph = crate::roomeq::home_cinema::BassManagementRoutingGraph {
routes: vec![graph.routes[0].clone()],
..graph
};
let single = predict_bass_output_curve_from_routes(&sub, &single_route_graph, "Sub", 48_000.0)
.expect("single route curve");
assert_eq!(predicted.freq.len(), sub.freq.len());
assert!(predicted.phase.is_some());
assert!(
predicted
.spl
.iter()
.zip(single.spl.iter())
.any(|(a, b)| (a - b).abs() > 1e-6),
"sub final curve prediction must include every route branch, not just a representative low-pass"
);
}
#[test]
fn home_cinema_bass_bus_curve_is_predicted_across_multiple_sub_outputs() {
let reference = phase_curve(0.0);
let mut sub_a = phase_curve(0.0);
sub_a.spl = array![0.0, 0.0, 0.0];
let mut sub_b = phase_curve(0.0);
sub_b.spl = array![-6.0, -6.0, -6.0];
let graph = crate::roomeq::home_cinema::BassManagementRoutingGraph {
physical_sub_output: "LFE".to_string(),
input_channels: vec![
"L".to_string(),
"R".to_string(),
"subs_1".to_string(),
"subs_2".to_string(),
],
output_channels: vec![
"L".to_string(),
"R".to_string(),
"subs_1".to_string(),
"subs_2".to_string(),
],
routes: vec![
crate::roomeq::home_cinema::BassManagementRoute {
group_id: Some("lcr".to_string()),
source_channel: "L".to_string(),
source_index: 0,
destination: "subs_1".to_string(),
destination_index: 2,
pre_chain_channel: Some("LFE".to_string()),
post_chain_channel: Some("subs_1".to_string()),
route_kind: "redirected_bass_lowpass_to_sub".to_string(),
crossover_type: "LR24".to_string(),
high_pass_hz: None,
low_pass_hz: Some(80.0),
gain_db: 0.0,
gain_linear: 1.0,
matrix_gain: 1.0,
delay_ms: 0.0,
polarity_inverted: false,
},
crate::roomeq::home_cinema::BassManagementRoute {
group_id: Some("surround".to_string()),
source_channel: "R".to_string(),
source_index: 1,
destination: "subs_2".to_string(),
destination_index: 3,
pre_chain_channel: Some("LFE".to_string()),
post_chain_channel: Some("subs_2".to_string()),
route_kind: "redirected_bass_lowpass_to_sub".to_string(),
crossover_type: "BW12".to_string(),
high_pass_hz: None,
low_pass_hz: Some(120.0),
gain_db: 0.0,
gain_linear: 1.0,
matrix_gain: 1.0,
delay_ms: 1.0,
polarity_inverted: false,
},
],
matrix: None,
advisories: vec!["ok".to_string()],
};
let output_base_curves = HashMap::from([
("subs_1".to_string(), sub_a.clone()),
("subs_2".to_string(), sub_b.clone()),
]);
let predicted = predict_bass_bus_curve_from_routes(
&reference,
&graph,
&output_base_curves,
&reference,
48_000.0,
)
.expect("route-predicted bass bus curve");
let single_graph = crate::roomeq::home_cinema::BassManagementRoutingGraph {
routes: vec![graph.routes[0].clone()],
..graph
};
let single = predict_bass_bus_curve_from_routes(
&reference,
&single_graph,
&output_base_curves,
&reference,
48_000.0,
)
.expect("single-output route-predicted bus curve");
assert_eq!(predicted.freq.len(), reference.freq.len());
assert!(predicted.phase.is_some());
assert!(
predicted
.spl
.iter()
.zip(single.spl.iter())
.any(|(a, b)| (a - b).abs() > 1e-6),
"bass bus prediction must include every routed physical sub output"
);
}
#[test]
fn apply_curve_delta_to_reference_curve_preserves_driver_delta_and_phase() {
let reference = Curve {
freq: array![40.0, 80.0],
spl: array![1.0, -2.0],
phase: Some(array![10.0, 20.0]),
..Default::default()
};
let initial = Curve {
freq: array![40.0, 80.0],
spl: array![0.0, 0.0],
phase: Some(array![0.0, 5.0]),
..Default::default()
};
let final_curve = Curve {
freq: array![40.0, 80.0],
spl: array![3.0, 4.0],
phase: Some(array![30.0, 45.0]),
..Default::default()
};
let corrected = apply_curve_delta_to_reference_curve(&reference, &initial, &final_curve);
assert_eq!(corrected.spl, array![4.0, 2.0]);
assert_eq!(corrected.phase.unwrap(), array![40.0, 60.0]);
}
#[test]
fn representative_bass_route_signature_uses_emitted_route_shape() {
let graph = crate::roomeq::home_cinema::BassManagementRoutingGraph {
physical_sub_output: "LFE".to_string(),
input_channels: vec!["L".to_string(), "R".to_string(), "LFE".to_string()],
output_channels: vec!["L".to_string(), "R".to_string(), "LFE".to_string()],
routes: vec![
crate::roomeq::home_cinema::BassManagementRoute {
group_id: Some("lcr".to_string()),
source_channel: "L".to_string(),
source_index: 0,
destination: "LFE".to_string(),
destination_index: 2,
pre_chain_channel: Some("LFE".to_string()),
post_chain_channel: Some("LFE".to_string()),
route_kind: "redirected_bass_lowpass_to_sub".to_string(),
crossover_type: "LR24".to_string(),
high_pass_hz: None,
low_pass_hz: Some(80.0),
gain_db: 0.0,
gain_linear: 1.0,
matrix_gain: 1.0,
delay_ms: 0.0,
polarity_inverted: false,
},
crate::roomeq::home_cinema::BassManagementRoute {
group_id: Some("height".to_string()),
source_channel: "R".to_string(),
source_index: 1,
destination: "LFE".to_string(),
destination_index: 2,
pre_chain_channel: Some("LFE".to_string()),
post_chain_channel: Some("LFE".to_string()),
route_kind: "redirected_bass_lowpass_to_sub".to_string(),
crossover_type: "BW12".to_string(),
high_pass_hz: None,
low_pass_hz: Some(120.0),
gain_db: 0.0,
gain_linear: 1.0,
matrix_gain: 1.0,
delay_ms: 0.0,
polarity_inverted: false,
},
],
matrix: None,
advisories: vec!["ok".to_string()],
};
let (crossover_type, crossover_hz) =
representative_bass_route_signature(Some(&graph), "LR24", 80.0);
assert_eq!(crossover_type, "BW12");
assert_eq!(crossover_hz, 120.0);
}
#[test]
fn home_cinema_bass_management_workflow_reports_exported_dsp() {
let config = bass_management_workflow_config(true, 6.0);
let result = optimize_room(&config, 48_000.0, None, None).expect("room optimization");
let report = result
.metadata
.bass_management
.as_ref()
.expect("bass management report");
let optimization = report.optimization.as_ref().expect("optimization report");
assert!(optimization.applied);
assert!(optimization.phase_available);
assert!(
optimization.advisories.contains(&"ok".to_string())
|| optimization
.advisories
.contains(&"joint_route_de_optimized".to_string())
|| optimization
.advisories
.contains(&"joint_optimizer_no_improvement".to_string())
);
assert!(optimization.objective_before.is_some());
assert!(optimization.objective_after.is_some());
assert!(optimization.main_delay_ms >= -1e-9);
assert!(optimization.sub_delay_ms >= -1e-9);
assert_eq!(
report.applied_sub_gain_db,
Some(optimization.applied_sub_gain_db)
);
assert!(optimization.estimated_bass_bus_peak_gain_db.is_some());
let routing = report.routing_graph.as_ref().expect("routing graph");
assert_eq!(routing.physical_sub_output, "LFE");
assert!(routing.routes.iter().any(|route| {
route.route_kind == "redirected_bass_lowpass_to_sub"
&& route.source_channel == "L"
&& route.destination == "LFE"
}));
assert!(routing.routes.iter().any(|route| {
route.route_kind == "lfe_lowpass_to_sub"
&& route.source_channel == "LFE"
&& (route.gain_db - (10.0 + optimization.applied_sub_gain_db)).abs() < 1e-9
}));
let routing_matrix = routing.matrix.as_ref().expect("routing matrix");
assert_eq!(routing_matrix.output_channel_map, vec![2]);
assert_eq!(routing_matrix.route_count, 3);
for role in ["L", "R"] {
let chain = result.channels.get(role).expect("main chain");
assert!(has_crossover_plugin(chain, "high"));
let group = optimization
.group_results
.iter()
.find(|group| group.group_id == "lcr")
.expect("lcr group result");
assert!((total_delay_ms(chain) - group.main_delay_ms).abs() < 1e-6);
let chain_phase = chain
.final_curve
.as_ref()
.and_then(|c| c.phase.as_ref())
.expect("chain final phase");
let result_phase = result.channel_results[role]
.final_curve
.phase
.as_ref()
.expect("result final phase");
assert_eq!(chain_phase.len(), result_phase.len());
}
let sub_chain = result.channels.get("LFE").expect("sub chain");
let dsp_output = result.to_dsp_chain_output();
let matrix_plugin = dsp_output
.global_plugins
.iter()
.find(|plugin| plugin.plugin_type == "matrix")
.expect("bass-management global matrix plugin");
assert_eq!(
matrix_plugin.parameters["label"].as_str(),
Some("home_cinema_bass_management")
);
assert_eq!(
matrix_plugin.parameters["input_channel_map"]
.as_array()
.expect("input map")
.len(),
routing_matrix.input_channel_map.len()
);
assert!(has_crossover_plugin(sub_chain, "low"));
assert!((total_delay_ms(sub_chain) - optimization.sub_delay_ms).abs() < 1e-6);
assert!(
sub_chain
.final_curve
.as_ref()
.and_then(|c| c.phase.as_ref())
.is_some()
);
}
#[test]
fn home_cinema_all_channel_multiseat_guardrail_reruns_and_reports_rejection() {
let config = home_cinema_multiseat_guardrail_config();
let result = optimize_room(&config, 48_000.0, None, None).expect("room optimization");
let report = result
.metadata
.multi_seat_correction
.as_ref()
.expect("multi-seat correction report");
assert!(report.enabled);
assert!(!report.applied);
assert!(
report
.advisories
.contains(&"all_channel_corrections_rejected_by_guardrails".to_string()),
"expected guardrail rejection advisory, got {:?}",
report.advisories
);
for role in ["L", "R"] {
let channel = report
.channels
.iter()
.find(|channel| channel.channel == role)
.expect("channel report");
assert_eq!(channel.status, "rejected_guardrails");
assert!(
channel
.advisories
.iter()
.any(|advisory| advisory == "non_primary_seat_constraint_failed"
|| advisory == "weighted_target_fit_collapsed"),
"expected rejection reason for {role}, got {:?}",
channel.advisories
);
assert!(channel.seats.is_empty());
}
}
#[test]
fn home_cinema_bass_management_workflow_skips_alignment_without_phase() {
let config = bass_management_workflow_config(false, 6.0);
let result = optimize_room(&config, 48_000.0, None, None).expect("room optimization");
let optimization = result
.metadata
.bass_management
.as_ref()
.and_then(|r| r.optimization.as_ref())
.expect("optimization report");
assert!(!optimization.applied);
assert!(!optimization.phase_available);
assert_eq!(optimization.main_delay_ms, 0.0);
assert_eq!(optimization.sub_delay_ms, 0.0);
assert!(!optimization.sub_polarity_inverted);
assert!(optimization.objective_before.is_none());
assert!(optimization.objective_after.is_none());
assert!(
optimization
.advisories
.contains(&"missing_phase_crossover_alignment_skipped".to_string())
);
}
#[test]
fn home_cinema_bass_management_workflow_limits_sub_boost_for_headroom() {
let config = bass_management_workflow_config(true, 0.0);
let result = optimize_room(&config, 48_000.0, None, None).expect("room optimization");
let report = result
.metadata
.bass_management
.as_ref()
.expect("bass management report");
let optimization = report.optimization.as_ref().expect("optimization report");
assert!(optimization.applied_sub_gain_db <= 1e-9);
assert!(
optimization
.sub_output_results
.iter()
.all(|output| output.gain_db <= 1e-9),
"physical sub outputs must respect the configured max_sub_boost cap"
);
assert_eq!(
report.applied_sub_gain_db,
Some(optimization.applied_sub_gain_db)
);
assert!(report.gain_limited == optimization.gain_limited);
if optimization.gain_limited {
assert!(
optimization
.advisories
.contains(&"sub_gain_limited_for_headroom".to_string())
);
}
}
#[test]
fn home_cinema_bass_management_workflow_selects_auto_crossover_type() {
let mut config = bass_management_workflow_config(true, 6.0);
config
.crossovers
.as_mut()
.and_then(|crossovers| crossovers.get_mut("bass"))
.expect("bass crossover")
.crossover_type = "auto".to_string();
let result = optimize_room(&config, 48_000.0, None, None).expect("room optimization");
let routing = result
.metadata
.bass_management
.as_ref()
.and_then(|report| report.routing_graph.as_ref())
.expect("routing graph");
let optimization = result
.metadata
.bass_management
.as_ref()
.and_then(|report| report.optimization.as_ref())
.expect("optimization report");
assert_ne!(optimization.crossover_type, "auto");
assert!(["LR24", "LR48", "BW12", "BW24"].contains(&optimization.crossover_type.as_str()));
assert!(
routing
.routes
.iter()
.all(|route| route.crossover_type == optimization.crossover_type)
);
}
#[test]
fn home_cinema_bass_management_workflow_applies_configured_group_crossovers_when_optimization_disabled()
{
let mut config = bass_management_workflow_config(true, 6.0);
let mut speakers = HashMap::new();
for role in ["L", "R", "SL", "SR", "TFL", "TFR"] {
speakers.insert(
role.to_string(),
SpeakerConfig::Single(MeasurementSource::InMemory(bass_management_workflow_curve(
76.0, 0.0, true,
))),
);
}
speakers.insert(
"sub".to_string(),
SpeakerConfig::Single(MeasurementSource::InMemory(bass_management_workflow_curve(
62.0, 3.0, true,
))),
);
config.speakers = speakers;
let system = config.system.as_mut().expect("system");
system.speakers = HashMap::from([
("L".to_string(), "L".to_string()),
("R".to_string(), "R".to_string()),
("SL".to_string(), "SL".to_string()),
("SR".to_string(), "SR".to_string()),
("TFL".to_string(), "TFL".to_string()),
("TFR".to_string(), "TFR".to_string()),
("LFE".to_string(), "sub".to_string()),
]);
system.bass_management = Some(BassManagementConfig {
enabled: true,
redirect_bass: true,
optimize_groups: false,
group_crossovers: HashMap::from([
("surround".to_string(), "surround".to_string()),
("height".to_string(), "height".to_string()),
]),
..BassManagementConfig::default()
});
config.crossovers.as_mut().expect("crossovers").extend([
(
"surround".to_string(),
CrossoverConfig {
crossover_type: "BW12".to_string(),
frequency: Some(100.0),
frequencies: None,
frequency_range: None,
},
),
(
"height".to_string(),
CrossoverConfig {
crossover_type: "LR48".to_string(),
frequency: Some(140.0),
frequencies: None,
frequency_range: None,
},
),
]);
let result = optimize_room(&config, 48_000.0, None, None).expect("room optimization");
assert_eq!(
crossover_plugin_frequency(result.channels.get("L").expect("L chain"), "high"),
Some(80.0)
);
assert_eq!(
crossover_plugin_frequency(result.channels.get("SL").expect("SL chain"), "high"),
Some(100.0)
);
assert_eq!(
crossover_plugin_frequency(result.channels.get("TFL").expect("TFL chain"), "high"),
Some(140.0)
);
let report = result
.metadata
.bass_management
.as_ref()
.expect("bass management report");
let routing = report.routing_graph.as_ref().expect("routing graph");
assert!(routing.routes.iter().any(|route| {
route.source_channel == "SL"
&& route.route_kind == "main_highpass_to_self"
&& route.crossover_type == "BW12"
&& route.high_pass_hz == Some(100.0)
}));
assert!(routing.routes.iter().any(|route| {
route.source_channel == "TFL"
&& route.route_kind == "main_highpass_to_self"
&& route.crossover_type == "LR48"
&& route.high_pass_hz == Some(140.0)
}));
let optimization = report.optimization.as_ref().expect("optimization");
assert!(optimization.group_results.iter().any(|group| {
group.group_id == "surround"
&& group.crossover_type == "BW12"
&& group.selected_crossover_hz == Some(100.0)
&& group
.advisories
.contains(&"group_optimization_disabled".to_string())
}));
}
#[test]
fn home_cinema_bass_management_routes_carry_shared_sub_gain() {
let outputs = bass_management_sub_output_results("LFE", None, 4.5, &SubwooferStrategy::Single);
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].output_role, "LFE");
assert!((outputs[0].gain_db - 4.5).abs() < 1e-9);
assert!((outputs[0].headroom_contribution_db - 4.5).abs() < 1e-9);
let driver_outputs = bass_management_sub_output_results(
"LFE",
Some(&[
SubDriverInfo {
name: "Sub A".to_string(),
gain: -2.0,
delay: 1.0,
inverted: false,
initial_curve: None,
},
SubDriverInfo {
name: "Sub B".to_string(),
gain: -4.0,
delay: 2.0,
inverted: true,
initial_curve: None,
},
]),
3.0,
&SubwooferStrategy::Mso,
);
assert_eq!(driver_outputs.len(), 2);
assert!((driver_outputs[0].gain_db - 1.0).abs() < 1e-9);
assert!((driver_outputs[1].gain_db + 1.0).abs() < 1e-9);
}
#[test]
fn home_cinema_bass_management_sub_output_refinement_requires_phase() {
let config = bass_management_workflow_config(true, 6.0);
let mut group_results = BTreeMap::new();
group_results.insert(
"lcr".to_string(),
crate::roomeq::home_cinema::BassManagementGroupReport {
group_id: "lcr".to_string(),
roles: vec!["L".to_string(), "R".to_string()],
crossover_type: "LR24".to_string(),
selected_crossover_hz: Some(80.0),
configured_crossover_hz: Some(80.0),
main_delay_ms: 0.0,
bass_route_delay_ms: 0.0,
polarity_inverted: false,
trim_db: 0.0,
objective_before: None,
objective_after: None,
advisories: vec!["ok".to_string()],
},
);
let mut outputs = vec![crate::roomeq::home_cinema::BassManagementSubOutputReport {
output_role: "Sub A".to_string(),
gain_db: 0.0,
delay_ms: 0.0,
polarity_inverted: false,
strategy_source: "mso".to_string(),
headroom_contribution_db: 0.0,
}];
let mut driver_curve = phase_curve(0.0);
driver_curve.phase = None;
let drivers = vec![SubDriverInfo {
name: "Sub A".to_string(),
gain: 0.0,
delay: 0.0,
inverted: false,
initial_curve: Some(driver_curve),
}];
let mut aligned = HashMap::new();
aligned.insert("L".to_string(), phase_curve(0.0));
aligned.insert("R".to_string(), phase_curve(0.0));
let advisories = refine_bass_management_sub_outputs(
&config,
&["L".to_string(), "R".to_string()],
&aligned,
&mut group_results,
&mut outputs,
Some(&drivers),
48_000.0,
);
assert_eq!(
advisories,
vec!["joint_sub_output_skipped_missing_phase".to_string()]
);
}
#[test]
fn home_cinema_bass_management_sub_output_refinement_updates_physical_outputs() {
let mut config = bass_management_workflow_config(true, 6.0);
config.optimizer.max_iter = 320;
config.optimizer.population = 28;
config.optimizer.seed = Some(99);
let mut group_results = BTreeMap::new();
group_results.insert(
"lcr".to_string(),
crate::roomeq::home_cinema::BassManagementGroupReport {
group_id: "lcr".to_string(),
roles: vec!["L".to_string(), "R".to_string()],
crossover_type: "LR24".to_string(),
selected_crossover_hz: Some(80.0),
configured_crossover_hz: Some(80.0),
main_delay_ms: 0.0,
bass_route_delay_ms: 0.0,
polarity_inverted: false,
trim_db: 0.0,
objective_before: None,
objective_after: None,
advisories: vec!["ok".to_string()],
},
);
let mut outputs = vec![
crate::roomeq::home_cinema::BassManagementSubOutputReport {
output_role: "Sub A".to_string(),
gain_db: 12.0,
delay_ms: 0.0,
polarity_inverted: false,
strategy_source: "mso".to_string(),
headroom_contribution_db: 12.0,
},
crate::roomeq::home_cinema::BassManagementSubOutputReport {
output_role: "Sub B".to_string(),
gain_db: 12.0,
delay_ms: 0.0,
polarity_inverted: false,
strategy_source: "mso".to_string(),
headroom_contribution_db: 12.0,
},
];
let mut loud_sub_a = phase_curve(0.0);
loud_sub_a.spl = array![12.0, 12.0, 12.0];
let mut loud_sub_b = phase_curve(0.0);
loud_sub_b.spl = array![12.0, 12.0, 12.0];
let drivers = vec![
SubDriverInfo {
name: "Sub A".to_string(),
gain: 12.0,
delay: 0.0,
inverted: false,
initial_curve: Some(loud_sub_a),
},
SubDriverInfo {
name: "Sub B".to_string(),
gain: 12.0,
delay: 0.0,
inverted: false,
initial_curve: Some(loud_sub_b),
},
];
let mut aligned = HashMap::new();
aligned.insert("L".to_string(), phase_curve(0.0));
aligned.insert("R".to_string(), phase_curve(0.0));
let advisories = refine_bass_management_sub_outputs(
&config,
&["L".to_string(), "R".to_string()],
&aligned,
&mut group_results,
&mut outputs,
Some(&drivers),
48_000.0,
);
assert!(
advisories.contains(&"joint_sub_output_de_optimized".to_string()),
"expected physical sub output DE to improve, got {advisories:?}"
);
assert!(outputs.iter().all(|output| output.gain_db < 12.0));
assert!(outputs.iter().all(|output| output.delay_ms >= 0.0));
assert!(
group_results["lcr"]
.advisories
.contains(&"joint_sub_output_de_optimized".to_string())
);
}
#[test]
fn bass_management_joint_de_minimizer_improves_seed_solution() {
let lower = [-10.0, -10.0];
let upper = [10.0, 10.0];
let initial = [8.0, -7.0];
let objective = |x: &[f64]| (x[0] - 1.25).powi(2) + (x[1] + 2.5).powi(2);
let (best, score) =
differential_evolution_minimize(&lower, &upper, &initial, &objective, 24, 300, 42);
assert!(score < objective(&initial));
assert!((best[0] - 1.25).abs() < 1.0);
assert!((best[1] + 2.5).abs() < 1.0);
}