Skip to main content

audio_lab/plot/
time.rs

1use crate::signal::{TimeSignal, math::gain_to_db};
2use eframe::egui;
3use egui_plot::{Legend, Line, Plot, PlotPoint, PlotPoints};
4
5use crate::plot::native_options_any_thread;
6use crate::plot::save::SavePlotState;
7
8const DB_FLOOR: f64 = -120.0;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum TimeValue {
12    Amplitude,
13    AmplitudeDb,
14}
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub struct TimePlotOptions {
18    pub value: TimeValue,
19}
20
21impl Default for TimePlotOptions {
22    fn default() -> Self {
23        Self {
24            value: TimeValue::Amplitude,
25        }
26    }
27}
28
29impl TimeValue {
30    fn value(&self, sample: f64) -> f64 {
31        match self {
32            Self::Amplitude => sample,
33            Self::AmplitudeDb => gain_to_db(sample.abs()).max(DB_FLOOR),
34        }
35    }
36
37    fn y_label(&self) -> &'static str {
38        match self {
39            Self::Amplitude => "Amplitude",
40            Self::AmplitudeDb => "Amplitude (dB)",
41        }
42    }
43}
44
45pub(crate) struct TimeSignalPlot {
46    series: Vec<(String, Vec<PlotPoint>)>,
47    options: TimePlotOptions,
48    save: SavePlotState,
49}
50
51impl TimeSignalPlot {
52    pub(crate) fn new(signals: &[&TimeSignal], title: &str, options: TimePlotOptions) -> Self {
53        let mut series = Vec::new();
54        for (signal_index, signal) in signals.iter().enumerate() {
55            let time_steps = signal.time_steps();
56            let num_channels = signal.num_channels();
57            for (channel_index, channel) in signal.channel_iter().enumerate() {
58                let label = crate::plot::legend::build_channel_label(
59                    signal.channel_label(channel_index),
60                    signal.comment(),
61                    signal_index,
62                    channel_index,
63                    num_channels,
64                );
65                let points = time_steps
66                    .iter()
67                    .zip(channel.iter())
68                    .map(|(&t, &y)| PlotPoint::new(t, options.value.value(y)))
69                    .collect();
70                series.push((label, points));
71            }
72        }
73        Self {
74            series,
75            options,
76            save: SavePlotState::new(title),
77        }
78    }
79}
80
81impl eframe::App for TimeSignalPlot {
82    fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
83        egui::Panel::top("controls").show(ui, |ui| {
84            self.save.show_panel(ui);
85        });
86        egui::CentralPanel::default().show(ui, |ui| {
87            Plot::new("time_signal")
88                .x_axis_label("Time (s)")
89                .y_axis_label(self.options.value.y_label())
90                .legend(Legend::default())
91                .show(ui, |plot_ui| {
92                    for (label, points) in &self.series {
93                        plot_ui.line(Line::new(label.clone(), PlotPoints::Borrowed(points)));
94                    }
95                });
96        });
97        self.save.handle_screenshot(ui.ctx());
98    }
99}
100
101pub fn show_time_with_options(
102    title: &str,
103    signals: &[&TimeSignal],
104    options: TimePlotOptions,
105) -> Result<(), crate::plot::Error> {
106    Ok(eframe::run_native(
107        title,
108        native_options_any_thread(),
109        Box::new(|_cc| Ok(Box::new(TimeSignalPlot::new(signals, title, options)))),
110    )?)
111}
112
113pub fn show_time(title: &str, signals: &[&TimeSignal]) -> Result<(), crate::plot::Error> {
114    show_time_with_options(title, signals, Default::default())
115}
116
117#[cfg(test)]
118mod tests {
119    use crate::signal::TimeSignal;
120
121    use super::*;
122
123    #[test]
124    #[ignore]
125    fn test_show_time_signal() {
126        use crate::signal::ndarray::arr2;
127
128        let sample_rate = 44100.0;
129        let signal = TimeSignal::new(
130            arr2(&[
131                std::array::from_fn::<f64, 1000, _>(|i| {
132                    (i as f64 / sample_rate * 440.0 * std::f64::consts::TAU).sin()
133                }),
134                std::array::from_fn::<f64, 1000, _>(|i| {
135                    (i as f64 / sample_rate * 880.0 * std::f64::consts::TAU).sin()
136                }),
137            ]),
138            sample_rate,
139        )
140        .unwrap();
141        show_time("Time Signal Test", &[&signal]).unwrap();
142    }
143
144    #[test]
145    #[ignore]
146    fn test_show_time_signal_db() {
147        use crate::signal::ndarray::arr2;
148
149        let sample_rate = 44100.0;
150        let signal = TimeSignal::new(
151            arr2(&[
152                std::array::from_fn::<f64, 1000, _>(|i| {
153                    (i as f64 / sample_rate * 440.0 * std::f64::consts::TAU).sin()
154                }),
155                std::array::from_fn::<f64, 1000, _>(|i| {
156                    (i as f64 / sample_rate * 880.0 * std::f64::consts::TAU).sin()
157                }),
158            ]),
159            sample_rate,
160        )
161        .unwrap();
162        show_time_with_options(
163            "Time Signal Test (dB)",
164            &[&signal],
165            TimePlotOptions {
166                value: TimeValue::AmplitudeDb,
167            },
168        )
169        .unwrap();
170    }
171}