use plotly::common::Mode;
use plotly::layout::{Axis, AxisType};
use plotly::{Layout, Plot, Scatter};
use crate::loss::{DriversLossData, compute_drivers_combined_response};
pub fn plot_drivers(
drivers_data: &DriversLossData,
gains: &[f64],
crossover_freqs: &[f64],
delays: Option<&[f64]>,
sample_rate: f64,
) -> Plot {
let mut plot = Plot::new();
let freq_grid = &drivers_data.freq_grid;
let combined_response = compute_drivers_combined_response(
drivers_data,
gains,
crossover_freqs,
delays,
sample_rate,
);
let combined_mean = combined_response.mean().unwrap_or(0.0);
for (i, driver) in drivers_data.drivers.iter().enumerate() {
let interpolated = crate::read::normalize_and_interpolate_response(
freq_grid,
&crate::Curve {
freq: driver.freq.clone(),
spl: driver.spl.clone(),
phase: driver.phase.clone(),
},
);
let color = match i {
0 => "rgb(31, 119, 180)", 1 => "rgb(255, 127, 14)", 2 => "rgb(44, 160, 44)", 3 => "rgb(214, 39, 40)", _ => "rgb(128, 128, 128)", };
let trace = Scatter::new(freq_grid.to_vec(), interpolated.spl.to_vec())
.mode(Mode::Lines)
.name(format!("Driver {} (raw)", i + 1))
.line(
plotly::common::Line::new()
.color(color)
.width(1.5)
.dash(plotly::common::DashType::Dash),
);
plot.add_trace(trace);
}
for (i, driver) in drivers_data.drivers.iter().enumerate() {
let driver_freq_grid = &driver.freq;
let mut response = &driver.spl + gains[i];
if let crate::loss::CrossoverType::None = drivers_data.crossover_type {
} else {
if i > 0 {
let xover_freq = crossover_freqs[i - 1];
let hp_filter = match drivers_data.crossover_type {
crate::loss::CrossoverType::Butterworth2 => {
crate::iir::peq_butterworth_highpass(2, xover_freq, sample_rate)
}
crate::loss::CrossoverType::LinkwitzRiley2 => {
crate::iir::peq_linkwitzriley_highpass(2, xover_freq, sample_rate)
}
crate::loss::CrossoverType::LinkwitzRiley4 => {
crate::iir::peq_linkwitzriley_highpass(4, xover_freq, sample_rate)
}
crate::loss::CrossoverType::LinkwitzRiley8 => {
crate::iir::peq_linkwitzriley_highpass(8, xover_freq, sample_rate)
}
crate::loss::CrossoverType::None => vec![],
};
let hp_response =
crate::iir::compute_peq_response(driver_freq_grid, &hp_filter, sample_rate);
response = response + hp_response;
}
if i < drivers_data.drivers.len() - 1 {
let xover_freq = crossover_freqs[i];
let lp_filter = match drivers_data.crossover_type {
crate::loss::CrossoverType::Butterworth2 => {
crate::iir::peq_butterworth_lowpass(2, xover_freq, sample_rate)
}
crate::loss::CrossoverType::LinkwitzRiley2 => {
crate::iir::peq_linkwitzriley_lowpass(2, xover_freq, sample_rate)
}
crate::loss::CrossoverType::LinkwitzRiley4 => {
crate::iir::peq_linkwitzriley_lowpass(4, xover_freq, sample_rate)
}
crate::loss::CrossoverType::LinkwitzRiley8 => {
crate::iir::peq_linkwitzriley_lowpass(8, xover_freq, sample_rate)
}
crate::loss::CrossoverType::None => vec![],
};
let lp_response =
crate::iir::compute_peq_response(driver_freq_grid, &lp_filter, sample_rate);
response = response + lp_response;
}
}
let color = match i {
0 => "rgb(31, 119, 180)", 1 => "rgb(255, 127, 14)", 2 => "rgb(44, 160, 44)", 3 => "rgb(214, 39, 40)", _ => "rgb(128, 128, 128)", };
let trace = Scatter::new(driver_freq_grid.to_vec(), response.to_vec())
.mode(Mode::Lines)
.name(format!("Driver {} ({:+.1} dB)", i + 1, gains[i]))
.line(plotly::common::Line::new().color(color).width(2.0));
plot.add_trace(trace);
}
let combined_response_normalized = &combined_response - combined_mean;
let trace_combined = Scatter::new(freq_grid.to_vec(), combined_response_normalized.to_vec())
.mode(Mode::Lines)
.name("Combined Response")
.line(plotly::common::Line::new().color("rgb(0, 0, 0)").width(3.0));
plot.add_trace(trace_combined);
let mut shapes = Vec::new();
let mut annotations = Vec::new();
for (i, &xover_freq) in crossover_freqs.iter().enumerate() {
let shape = plotly::layout::Shape::new()
.shape_type(plotly::layout::ShapeType::Line)
.x_ref("x")
.y_ref("paper")
.x0(xover_freq)
.x1(xover_freq)
.y0(0.0)
.y1(1.0)
.line(
plotly::layout::ShapeLine::new()
.color("rgba(150, 150, 150, 0.6)")
.width(2.0)
.dash(plotly::common::DashType::Dot),
);
shapes.push(shape);
let annotation = plotly::layout::Annotation::new()
.x(xover_freq)
.y(1.02)
.x_ref("x")
.y_ref("paper")
.text(format!("Crossover {}: {:.0} Hz", i + 1, xover_freq))
.show_arrow(false)
.font(
plotly::common::Font::new()
.size(10)
.color("rgb(100, 100, 100)"),
)
.x_anchor(plotly::common::Anchor::Center)
.y_anchor(plotly::common::Anchor::Bottom);
annotations.push(annotation);
}
let crossover_type_str = drivers_data.crossover_type.display_name();
let layout = Layout::new()
.title(format!(
"Multi-Driver Crossover Optimization ({})",
crossover_type_str
))
.x_axis(
Axis::new()
.title("Frequency (Hz)".to_string())
.type_(AxisType::Log)
.range(vec![1.301, 4.301])
.grid_color("rgba(128, 128, 128, 0.2)"),
)
.y_axis(
Axis::new()
.title("SPL (dB)".to_string())
.range(vec![-30.0, 30.0])
.grid_color("rgba(128, 128, 128, 0.2)"),
)
.shapes(shapes)
.annotations(annotations)
.height(600)
.hover_mode(plotly::layout::HoverMode::X);
plot.set_layout(layout);
plot
}
pub fn plot_drivers_results(
drivers_data: &DriversLossData,
gains: &[f64],
crossover_freqs: &[f64],
delays: Option<&[f64]>,
sample_rate: f64,
output_path: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
use build_html::*;
use std::fs::File;
use std::io::Write;
let plot = plot_drivers(drivers_data, gains, crossover_freqs, delays, sample_rate);
let title_text = format!(
"{}-Way Speaker Crossover Optimization",
drivers_data.drivers.len()
);
let html = HtmlPage::new()
.with_title(&title_text)
.with_script_link("https://cdn.plot.ly/plotly-3.2.0.min.js")
.with_raw(plot.to_inline_html(Some("drivers")))
.to_html_string();
let html_output_path = output_path.with_extension("html");
if let Some(parent) = html_output_path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut file = File::create(&html_output_path)?;
file.write_all(html.as_bytes())?;
file.flush()?;
Ok(())
}