use crate::signal::{TimeSignal, math::gain_to_db};
use eframe::egui;
use egui_plot::{Legend, Line, Plot, PlotPoint, PlotPoints};
use crate::plot::native_options_any_thread;
use crate::plot::save::SavePlotState;
const DB_FLOOR: f64 = -120.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TimeValue {
Amplitude,
AmplitudeDb,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TimePlotOptions {
pub value: TimeValue,
}
impl Default for TimePlotOptions {
fn default() -> Self {
Self {
value: TimeValue::Amplitude,
}
}
}
impl TimeValue {
fn value(&self, sample: f64) -> f64 {
match self {
Self::Amplitude => sample,
Self::AmplitudeDb => gain_to_db(sample.abs()).max(DB_FLOOR),
}
}
fn y_label(&self) -> &'static str {
match self {
Self::Amplitude => "Amplitude",
Self::AmplitudeDb => "Amplitude (dB)",
}
}
}
pub(crate) struct TimeSignalPlot {
series: Vec<(String, Vec<PlotPoint>)>,
options: TimePlotOptions,
save: SavePlotState,
}
impl TimeSignalPlot {
pub(crate) fn new(signals: &[&TimeSignal], title: &str, options: TimePlotOptions) -> Self {
let mut series = Vec::new();
for (signal_index, signal) in signals.iter().enumerate() {
let time_steps = signal.time_steps();
let num_channels = signal.num_channels();
for (channel_index, channel) in signal.channel_iter().enumerate() {
let label = crate::plot::legend::build_channel_label(
signal.channel_label(channel_index),
signal.comment(),
signal_index,
channel_index,
num_channels,
);
let points = time_steps
.iter()
.zip(channel.iter())
.map(|(&t, &y)| PlotPoint::new(t, options.value.value(y)))
.collect();
series.push((label, points));
}
}
Self {
series,
options,
save: SavePlotState::new(title),
}
}
}
impl eframe::App for TimeSignalPlot {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
egui::Panel::top("controls").show(ui, |ui| {
self.save.show_panel(ui);
});
egui::CentralPanel::default().show(ui, |ui| {
Plot::new("time_signal")
.x_axis_label("Time (s)")
.y_axis_label(self.options.value.y_label())
.legend(Legend::default())
.show(ui, |plot_ui| {
for (label, points) in &self.series {
plot_ui.line(Line::new(label.clone(), PlotPoints::Borrowed(points)));
}
});
});
self.save.handle_screenshot(ui.ctx());
}
}
pub fn show_time_with_options(
title: &str,
signals: &[&TimeSignal],
options: TimePlotOptions,
) -> Result<(), crate::plot::Error> {
Ok(eframe::run_native(
title,
native_options_any_thread(),
Box::new(|_cc| Ok(Box::new(TimeSignalPlot::new(signals, title, options)))),
)?)
}
pub fn show_time(title: &str, signals: &[&TimeSignal]) -> Result<(), crate::plot::Error> {
show_time_with_options(title, signals, Default::default())
}
#[cfg(test)]
mod tests {
use crate::signal::TimeSignal;
use super::*;
#[test]
#[ignore]
fn test_show_time_signal() {
use crate::signal::ndarray::arr2;
let sample_rate = 44100.0;
let signal = TimeSignal::new(
arr2(&[
std::array::from_fn::<f64, 1000, _>(|i| {
(i as f64 / sample_rate * 440.0 * std::f64::consts::TAU).sin()
}),
std::array::from_fn::<f64, 1000, _>(|i| {
(i as f64 / sample_rate * 880.0 * std::f64::consts::TAU).sin()
}),
]),
sample_rate,
)
.unwrap();
show_time("Time Signal Test", &[&signal]).unwrap();
}
#[test]
#[ignore]
fn test_show_time_signal_db() {
use crate::signal::ndarray::arr2;
let sample_rate = 44100.0;
let signal = TimeSignal::new(
arr2(&[
std::array::from_fn::<f64, 1000, _>(|i| {
(i as f64 / sample_rate * 440.0 * std::f64::consts::TAU).sin()
}),
std::array::from_fn::<f64, 1000, _>(|i| {
(i as f64 / sample_rate * 880.0 * std::f64::consts::TAU).sin()
}),
]),
sample_rate,
)
.unwrap();
show_time_with_options(
"Time Signal Test (dB)",
&[&signal],
TimePlotOptions {
value: TimeValue::AmplitudeDb,
},
)
.unwrap();
}
}