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}