1use crate::signal::{FreqSignal, math::gain_to_db};
2use eframe::egui;
3use egui_plot::{GridInput, GridMark, 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 FreqValue {
12 Magnitude,
13 MagnitudeDb,
14 PhaseRadians,
15 PhaseDegrees,
16}
17
18#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct FreqPlotOptions {
20 pub log_freq: bool,
21 pub value: FreqValue,
22}
23
24impl Default for FreqPlotOptions {
25 fn default() -> Self {
26 Self {
27 log_freq: true,
28 value: FreqValue::MagnitudeDb,
29 }
30 }
31}
32
33impl FreqValue {
34 fn value(&self, magnitude: f64, phase_radians: f64) -> f64 {
35 match self {
36 FreqValue::MagnitudeDb => gain_to_db(magnitude).max(DB_FLOOR),
37 FreqValue::Magnitude => magnitude,
38 FreqValue::PhaseRadians => phase_radians,
39 FreqValue::PhaseDegrees => phase_radians.to_degrees(),
40 }
41 }
42
43 fn y_label(&self) -> &'static str {
44 match self {
45 FreqValue::MagnitudeDb => "Magnitude (dB)",
46 FreqValue::Magnitude => "Magnitude",
47 FreqValue::PhaseRadians => "Phase (rad)",
48 FreqValue::PhaseDegrees => "Phase (deg)",
49 }
50 }
51
52 fn format_y(&self, y: f64) -> String {
53 match self {
54 FreqValue::MagnitudeDb => format!("{y:.1} dB"),
55 FreqValue::Magnitude => format!("{y:.4}"),
56 FreqValue::PhaseRadians => format!("{y:.4} rad"),
57 FreqValue::PhaseDegrees => format!("{y:.2} deg"),
58 }
59 }
60}
61
62fn spectrum_grid_spacer(input: GridInput) -> Vec<GridMark> {
68 let (lo, hi) = (input.bounds.0, input.bounds.1);
69 let mut marks = Vec::new();
70
71 let start_exp = lo.floor() as i32 - 1;
72 let end_exp = hi.ceil() as i32 + 1;
73
74 for exp in start_exp..=end_exp {
75 let decade = 10f64.powi(exp);
76 for mult in 1..10 {
77 let hz = decade * mult as f64;
78 let log_pos = hz.log10();
79 if log_pos < lo || log_pos > hi {
80 continue;
81 }
82 let step_size = match mult {
83 1 => 1.0,
84 2 | 5 => 0.3,
85 _ => 0.1,
86 };
87 marks.push(GridMark {
88 value: log_pos,
89 step_size,
90 });
91 }
92 }
93 marks
94}
95
96pub(crate) struct FreqSignalPlot {
97 series: Vec<(String, Vec<PlotPoint>)>,
98 options: FreqPlotOptions,
99 save: SavePlotState,
100}
101
102impl FreqSignalPlot {
103 pub(crate) fn new(signals: &[&FreqSignal], title: &str, options: FreqPlotOptions) -> Self {
104 let mut series = Vec::new();
105 for (signal_index, signal) in signals.iter().enumerate() {
106 let freq_bins = signal.freq_bins();
107 let derived = match options.value {
108 FreqValue::Magnitude => Some(signal.to_magnitude()),
109 FreqValue::MagnitudeDb => Some(signal.to_magnitude_db()),
110 FreqValue::PhaseRadians | FreqValue::PhaseDegrees => Some(signal.to_phase()),
111 };
112 let num_channels = signal.num_channels();
113 for (channel_index, channel) in signal.channel_iter().enumerate() {
114 let derived_channel = derived.as_ref().map(|data| data.channel(channel_index));
115 let label = crate::plot::legend::build_channel_label(
116 signal.channel_label(channel_index),
117 signal.comment(),
118 signal_index,
119 channel_index,
120 num_channels,
121 );
122 let points = freq_bins
123 .iter()
124 .zip(channel.iter().enumerate())
125 .filter(|&(&f, _)| !options.log_freq || f > 0.0)
126 .map(|(&f, (bin_index, c))| {
127 let x = if options.log_freq { f.log10() } else { f };
128 let y = if let Some(derived_channel) = &derived_channel {
129 let value = derived_channel[bin_index];
130 if matches!(options.value, FreqValue::PhaseDegrees) {
131 value.to_degrees()
132 } else {
133 value
134 }
135 } else {
136 options.value.value(c.norm(), c.arg())
137 };
138 PlotPoint::new(x, y)
139 })
140 .collect();
141 series.push((label, points));
142 }
143 }
144 Self {
145 series,
146 options,
147 save: SavePlotState::new(title),
148 }
149 }
150}
151
152impl eframe::App for FreqSignalPlot {
153 fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
154 egui::Panel::top("controls").show(ui, |ui| {
155 self.save.show_panel(ui);
156 });
157 egui::CentralPanel::default().show(ui, |ui| {
158 let mut plot = Plot::new("freq_signal")
159 .x_axis_label("Frequency (Hz)")
160 .y_axis_label(self.options.value.y_label())
161 .legend(Legend::default());
162
163 if self.options.log_freq {
164 let value = self.options.value;
165 plot = plot
166 .x_grid_spacer(spectrum_grid_spacer)
167 .x_axis_formatter(|mark, _range| {
168 let hz = 10f64.powf(mark.value);
169 if hz >= 1000.0 {
170 format!("{:.0} kHz", hz / 1000.0)
171 } else {
172 format!("{hz:.0} Hz")
173 }
174 })
175 .label_formatter(move |pos| {
176 let (name, point) = match pos {
177 egui_plot::HoverPosition::NearDataPoint {
178 plot_name,
179 position,
180 ..
181 } => (*plot_name, position),
182 egui_plot::HoverPosition::Elsewhere { position } => ("", position),
183 };
184 let hz = 10f64.powf(point.x);
185 let freq_str = if hz >= 1000.0 {
186 format!("{:.1} kHz", hz / 1000.0)
187 } else {
188 format!("{hz:.1} Hz")
189 };
190 let y_str = value.format_y(point.y);
191 Some(if name.is_empty() {
192 format!("{freq_str}\n{y_str}")
193 } else {
194 format!("{name}\n{freq_str}\n{y_str}")
195 })
196 });
197 }
198
199 plot.show(ui, |plot_ui| {
200 for (label, points) in &self.series {
201 plot_ui.line(Line::new(label.clone(), PlotPoints::Borrowed(points)));
202 }
203 });
204 });
205 self.save.handle_screenshot(ui.ctx());
206 }
207}
208
209pub fn show_freq_with_options(
210 title: &str,
211 signals: &[&FreqSignal],
212 options: FreqPlotOptions,
213) -> Result<(), crate::plot::Error> {
214 Ok(eframe::run_native(
215 title,
216 native_options_any_thread(),
217 Box::new(|_cc| Ok(Box::new(FreqSignalPlot::new(signals, title, options)))),
218 )?)
219}
220
221pub fn show_freq(title: &str, signals: &[&FreqSignal]) -> Result<(), crate::plot::Error> {
222 show_freq_with_options(title, signals, Default::default())
223}
224
225#[cfg(test)]
226mod tests {
227 use crate::signal::{TimeSignal, ndarray::arr2};
228
229 use super::*;
230
231 fn make_signal() -> FreqSignal {
232 let sample_rate = 44100.0;
233 TimeSignal::new(
234 arr2(&[
235 std::array::from_fn::<f64, 4096, _>(|i| {
236 (i as f64 / sample_rate * 440.0 * std::f64::consts::TAU).sin()
237 }),
238 std::array::from_fn::<f64, 4096, _>(|i| {
239 (i as f64 / sample_rate * 880.0 * std::f64::consts::TAU).sin()
240 }),
241 ]),
242 sample_rate,
243 )
244 .unwrap()
245 .into_freq()
246 }
247
248 #[test]
249 #[ignore]
250 fn test_show_freq_signal_log_db() {
251 show_freq("Freq Signal (Log, dB)", &[&make_signal()]).unwrap();
252 }
253
254 #[test]
255 #[ignore]
256 fn test_show_freq_signal_phase() {
257 show_freq_with_options(
258 "Freq Signal (Phase)",
259 &[&make_signal()],
260 FreqPlotOptions {
261 log_freq: true,
262 value: FreqValue::PhaseDegrees,
263 },
264 )
265 .unwrap();
266 }
267
268 #[test]
269 #[ignore]
270 fn test_show_freq_signal_phase_degrees() {
271 show_freq_with_options(
272 "Freq Signal (Phase, Degrees)",
273 &[&make_signal()],
274 FreqPlotOptions {
275 log_freq: true,
276 value: FreqValue::PhaseDegrees,
277 },
278 )
279 .unwrap();
280 }
281}