diff --git a/src/analyzer.rs b/src/analyzer.rs index f98d77c..f0c4410 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -122,7 +122,10 @@ impl FftMeasurementValue { - FftMeasurementValue::Peak(FrequencyMeasurement(self.last_peaks[channel].0), DbMeasurement(self.last_peaks[channel].1)) + FftMeasurementValue::Peak( + FrequencyMeasurement(self.last_peaks[channel].0), + DbMeasurement(self.last_peaks[channel].1), + ) } } @@ -134,9 +137,7 @@ impl + 'static); + fn log_plot(&self) -> bool; + fn set_log_plot(&mut self, log_plot: bool); } #[derive(Debug, Clone, Default)] @@ -218,6 +221,8 @@ pub struct FftAnalyzer { audio_buf: Arc>>, + log_plot: bool, + pub measurements: Vec>>, } @@ -238,6 +243,8 @@ impl FftAnalyzer + 'static) { self.measurements.push(Box::new(measurement)); } + fn set_log_plot(&mut self, log_plot: bool) { + self.log_plot = log_plot; + } + fn log_plot(&self) -> bool { + self.log_plot + } } diff --git a/src/main.rs b/src/main.rs index dd7cca8..eac2b43 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use egui::epaint::Hsva; use egui::Widget; -use egui_plot::{Legend, Line, Plot}; +use egui_plot::{log_grid_spacer, GridInput, GridMark, Legend, Line, Plot}; use std::mem::swap; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -14,7 +14,8 @@ use log::debug; mod analyzer; use analyzer::{ - lin_to_db, Analyzer, FftAnalysis, FftAnalyzer, FftMeasurement, FftMeasurementValue, PeakFreqAmplitude, + lin_to_db, Analyzer, FftAnalysis, FftAnalyzer, FftMeasurement, FftMeasurementValue, + PeakFreqAmplitude, }; use crate::analyzer::RmsAmplitudeMeasurement; @@ -60,37 +61,12 @@ struct FftResult { plot_data: Arc>, } -fn ui_plot(ui: &mut egui::Ui, last_fft_result: FftResult) { - let plot_data = last_fft_result.plot_data.lock().unwrap(); - debug!("Plot data: {:?}", plot_data.plot_points); - let plot_min = plot_data.plot_min.min(-120.0); - let plot_max = plot_data.plot_max.max(0.0); - let _plot = Plot::new("FFT") - .allow_drag(false) - .include_y(plot_min) - .include_y(plot_max) - .set_margin_fraction(egui::vec2(0.0, 0.0)) - .x_axis_label("Frequency (KHz)") - .y_axis_label("Magnitude (dBFS)") - .x_axis_formatter(|x, _n, _r| format!("{:.0}", x / 1000.0)) - .legend(Legend::default()) - .show(ui, |plot_ui| { - for (chan, chart) in plot_data.plot_points.iter().enumerate() { - plot_ui.line( - Line::new(chart.to_vec()) - .fill(plot_min as f32) - .name(CHANNEL_NAMES[chan]) - .color(CHANNEL_COLOURS[chan]), - ); - } - }); -} - #[derive(Default)] struct MyAppUiSelections { audio_device: usize, sample_rate: usize, fft_length: usize, + x_log: bool, } struct MyApp { @@ -148,6 +124,7 @@ impl Widget for Measurement { impl eframe::App for MyApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { egui::SidePanel::left("left_panel").show(ctx, |ui| { + ui.label("Capture Options"); egui::ComboBox::from_label("Source") .selected_text(self.audio_devices[self.ui_selections.audio_device].clone()) .show_ui(ui, |ui| { @@ -161,6 +138,10 @@ impl eframe::App for MyApp { self.sample_rate_box(ui); self.fft_length_box(ui); + ui.separator(); + ui.label("Plot Options"); + self.x_log_plot_check(ui); + ui.separator(); ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { self.fft_progress(ui) @@ -186,12 +167,75 @@ impl eframe::App for MyApp { self.meas_box(ui, meas) } }); - ui_plot(ui, self.last_result.clone()); + self.plot_spectrum(ui); }); } } +fn real_log_grid_spacer(gi: GridInput) -> Vec { + // we want a major tick at every order of magnitude, integers in the range + let mut out = Vec::new(); + for i in gi.bounds.0.ceil() as usize..gi.bounds.1.floor() as usize + 1 { + out.push(GridMark { + value: i as f64, + step_size: 1.0, + }); + // Go from 10^i to 10^(i+1) in steps of (10^i+1 / 10) + let freq_base = 10.0_f64.powi(i as i32); + let frac_step = 10.0_f64.powi((i + 1) as i32) / 10.0; + for frac in 1..10 { + out.push(GridMark { + value: (freq_base + frac_step * frac as f64).log10(), + step_size: 0.1, + }); + } + } + + out +} + impl MyApp { + fn plot_spectrum(&self, ui: &mut egui::Ui) { + let last_fft_result = self.last_result.clone(); + let plot_data = last_fft_result.plot_data.lock().unwrap(); + let plot_min = plot_data.plot_min.min(-120.0); + let plot_max = plot_data.plot_max.max(0.0); + let mut plot = Plot::new("FFT") + .allow_drag(false) + .include_y(plot_min) + .include_y(plot_max) + .set_margin_fraction(egui::vec2(0.0, 0.0)) + .x_axis_label("Frequency (KHz)") + .y_axis_label("Magnitude (dBFS)") + .include_x(0.0) + .legend(Legend::default()); + + plot = if self.ui_selections.x_log { + plot.include_x((self.sample_rates[self.ui_selections.sample_rate] as f64 / 2.0).log10()) + .x_grid_spacer(real_log_grid_spacer) + .x_axis_formatter(|x, _n, _r| { + if x.floor() == x { + format!("{:.0}", 10.0_f64.powf(x)) + } else { + String::default() + } + }) + } else { + plot.include_x(self.sample_rates[self.ui_selections.sample_rate] as f64 / 2.0) + .x_axis_formatter(|x, _n, _r| format!("{:.2}", x)) + }; + + plot.show(ui, |plot_ui| { + for (chan, chart) in plot_data.plot_points.iter().enumerate() { + plot_ui.line( + Line::new(chart.to_vec()) + .fill(plot_min as f32) + .name(CHANNEL_NAMES[chan]) + .color(CHANNEL_COLOURS[chan]), + ); + } + }); + } fn sample_rate_box(&mut self, ui: &mut egui::Ui) { let sample_rate_box = egui::ComboBox::from_label("Sample Rate") .selected_text(self.sample_rates[self.ui_selections.sample_rate].to_string()); @@ -229,6 +273,14 @@ impl MyApp { self.set_fft_length(FFT_LENGTHS[self.ui_selections.fft_length]); } } + fn x_log_plot_check(&mut self, ui: &mut egui::Ui) { + let mut selection = self.ui_selections.x_log; + ui.checkbox(&mut selection, "X Log"); + if selection != self.ui_selections.x_log { + self.ui_selections.x_log = selection; + self.fft_analyzer.lock().unwrap().set_log_plot(selection); + } + } fn fft_progress(&mut self, ui: &mut egui::Ui) { let percent = self.fft_progress.load(Ordering::Relaxed) as f32 / self.fft_length as f32; let fft_progress = egui::ProgressBar::new(percent); @@ -237,7 +289,11 @@ impl MyApp { self.fft_progress.store(0, Ordering::Relaxed); } } - fn meas_box(&mut self, ui: &mut egui::Ui, meas: (&str, [FftMeasurementValue; NUM_CHANNELS])) { + fn meas_box( + &mut self, + ui: &mut egui::Ui, + meas: (&str, [FftMeasurementValue; NUM_CHANNELS]), + ) { ui.group(|ui| { ui.vertical(|ui| { ui.label(meas.0); @@ -321,6 +377,7 @@ impl MyApp { .unwrap_or(0), sample_rate: sample_rate_idx, fft_length: DEFAULT_FFT_LENGTH, + x_log: true, }, sample_rates: supported_sample_rates, audio_device: default_device, @@ -425,7 +482,13 @@ impl MyApp { thread::Builder::new() .name("fft".into()) .spawn(move || { - fft_thread_impl(fft_thread_analyzer_ref, ctx_ref, data_pipe, close_pipe, result_ref) + fft_thread_impl( + fft_thread_analyzer_ref, + ctx_ref, + data_pipe, + close_pipe, + result_ref, + ) }) .unwrap() } @@ -469,20 +532,34 @@ fn fft_thread_impl( return; } if let Ok(buf) = data_pipe.recv_timeout(std::time::Duration::from_secs_f64(0.1)) { - if analyzer.lock().unwrap().process_data(&buf) { + let new_result = analyzer.lock().unwrap().process_data(&buf); + if new_result { // Prepare the data for plotting + let lock = analyzer.lock().unwrap(); let charts: Vec> = - analyzer - .lock() - .unwrap() - .last_analysis - .iter() - .map(|analysis| { - Vec::from_iter(analysis.fft.iter().map(|(freq, amp, _energy)| { - [(*freq as f64), lin_to_db(*amp as f64)] - })) - }) - .collect(); + if lock.log_plot() { + lock.last_analysis + .iter() + .map(|analysis| { + Vec::from_iter(analysis.fft.iter().map(|(freq, amp, _energy)| { + [ + (*freq as f64).log10().clamp(0.0, f64::INFINITY), + lin_to_db(*amp as f64), + ] + })) + }) + .collect() + } else { + lock.last_analysis + .iter() + .map(|analysis| { + Vec::from_iter(analysis.fft.iter().map(|(freq, amp, _energy)| { + [*freq as f64, lin_to_db(*amp as f64)] + })) + }) + .collect() + }; + drop(lock); let plot_min = plot_min(&charts); let plot_max = plot_max(&charts);