Selectable log plot support! :D

This commit is contained in:
Keenan Tims 2023-11-13 13:38:18 -08:00
parent 9c195f7b96
commit 6028d5ca05
Signed by: ktims
GPG Key ID: 11230674D69038D4
2 changed files with 137 additions and 47 deletions

View File

@ -122,7 +122,10 @@ impl<T: Num + ToPrimitive, U: FftNum + Float + AddAssign, const NUM_CHANNELS: us
"Peak Amplitude" "Peak Amplitude"
} }
fn value(&self, channel: usize) -> FftMeasurementValue<U> { fn value(&self, channel: usize) -> FftMeasurementValue<U> {
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<T: Num + ToPrimitive, U: FftNum + Float + AddAssign, const NUM_CHANNELS: us
self.cur_peaks[channel] = bin self.cur_peaks[channel] = bin
} }
} }
fn accum_td_sample(&mut self, sample: T, channel: usize) { fn accum_td_sample(&mut self, sample: T, channel: usize) {}
}
fn finalize(&mut self, channel: usize) { fn finalize(&mut self, channel: usize) {
self.last_peaks[channel] = self.cur_peaks[channel] self.last_peaks[channel] = self.cur_peaks[channel]
} }
@ -149,6 +150,8 @@ pub trait Analyzer<T: Num + Send, U: FftNum + Float + Send, const NUM_CHANNELS:
fn set_length(&mut self, length: usize); fn set_length(&mut self, length: usize);
fn set_channels(&mut self, channels: usize); fn set_channels(&mut self, channels: usize);
fn add_measurement(&mut self, measurement: impl FftMeasurement<T, U, NUM_CHANNELS> + 'static); fn add_measurement(&mut self, measurement: impl FftMeasurement<T, U, NUM_CHANNELS> + 'static);
fn log_plot(&self) -> bool;
fn set_log_plot(&mut self, log_plot: bool);
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@ -218,6 +221,8 @@ pub struct FftAnalyzer<T, U: FftNum, const NUM_CHANNELS: usize> {
audio_buf: Arc<Mutex<AudioBuf<T>>>, audio_buf: Arc<Mutex<AudioBuf<T>>>,
log_plot: bool,
pub measurements: Vec<Box<dyn FftMeasurement<T, U, NUM_CHANNELS>>>, pub measurements: Vec<Box<dyn FftMeasurement<T, U, NUM_CHANNELS>>>,
} }
@ -238,6 +243,8 @@ impl<T: Num, U: FftNum, const NUM_CHANNELS: usize> FftAnalyzer<T, U, NUM_CHANNEL
norm_factor: U::from_f64(1.0 / (fft_length as f64).sqrt()).unwrap(), norm_factor: U::from_f64(1.0 / (fft_length as f64).sqrt()).unwrap(),
bin_freqs, bin_freqs,
log_plot: true,
processor: planner.plan_fft_forward(fft_length), processor: planner.plan_fft_forward(fft_length),
planner, planner,
window: HanningWindow::new(fft_length), window: HanningWindow::new(fft_length),
@ -372,4 +379,10 @@ impl<
fn add_measurement(&mut self, measurement: impl FftMeasurement<T, U, NUM_CHANNELS> + 'static) { fn add_measurement(&mut self, measurement: impl FftMeasurement<T, U, NUM_CHANNELS> + 'static) {
self.measurements.push(Box::new(measurement)); 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
}
} }

View File

@ -2,7 +2,7 @@ use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use egui::epaint::Hsva; use egui::epaint::Hsva;
use egui::Widget; 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::mem::swap;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
@ -14,7 +14,8 @@ use log::debug;
mod analyzer; mod analyzer;
use 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; use crate::analyzer::RmsAmplitudeMeasurement;
@ -60,37 +61,12 @@ struct FftResult<U> {
plot_data: Arc<Mutex<FftPlotData>>, plot_data: Arc<Mutex<FftPlotData>>,
} }
fn ui_plot(ui: &mut egui::Ui, last_fft_result: FftResult<FloatType>) {
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)] #[derive(Default)]
struct MyAppUiSelections { struct MyAppUiSelections {
audio_device: usize, audio_device: usize,
sample_rate: usize, sample_rate: usize,
fft_length: usize, fft_length: usize,
x_log: bool,
} }
struct MyApp { struct MyApp {
@ -148,6 +124,7 @@ impl<T: MeasurementValueType> Widget for Measurement<T> {
impl eframe::App for MyApp { impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::SidePanel::left("left_panel").show(ctx, |ui| { egui::SidePanel::left("left_panel").show(ctx, |ui| {
ui.label("Capture Options");
egui::ComboBox::from_label("Source") egui::ComboBox::from_label("Source")
.selected_text(self.audio_devices[self.ui_selections.audio_device].clone()) .selected_text(self.audio_devices[self.ui_selections.audio_device].clone())
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
@ -161,6 +138,10 @@ impl eframe::App for MyApp {
self.sample_rate_box(ui); self.sample_rate_box(ui);
self.fft_length_box(ui); self.fft_length_box(ui);
ui.separator();
ui.label("Plot Options");
self.x_log_plot_check(ui);
ui.separator(); ui.separator();
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
self.fft_progress(ui) self.fft_progress(ui)
@ -186,12 +167,75 @@ impl eframe::App for MyApp {
self.meas_box(ui, meas) self.meas_box(ui, meas)
} }
}); });
ui_plot(ui, self.last_result.clone()); self.plot_spectrum(ui);
}); });
} }
} }
fn real_log_grid_spacer(gi: GridInput) -> Vec<GridMark> {
// 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 { 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) { fn sample_rate_box(&mut self, ui: &mut egui::Ui) {
let sample_rate_box = egui::ComboBox::from_label("Sample Rate") let sample_rate_box = egui::ComboBox::from_label("Sample Rate")
.selected_text(self.sample_rates[self.ui_selections.sample_rate].to_string()); .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]); 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) { 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 percent = self.fft_progress.load(Ordering::Relaxed) as f32 / self.fft_length as f32;
let fft_progress = egui::ProgressBar::new(percent); let fft_progress = egui::ProgressBar::new(percent);
@ -237,7 +289,11 @@ impl MyApp {
self.fft_progress.store(0, Ordering::Relaxed); self.fft_progress.store(0, Ordering::Relaxed);
} }
} }
fn meas_box(&mut self, ui: &mut egui::Ui, meas: (&str, [FftMeasurementValue<FloatType>; NUM_CHANNELS])) { fn meas_box(
&mut self,
ui: &mut egui::Ui,
meas: (&str, [FftMeasurementValue<FloatType>; NUM_CHANNELS]),
) {
ui.group(|ui| { ui.group(|ui| {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.label(meas.0); ui.label(meas.0);
@ -321,6 +377,7 @@ impl MyApp {
.unwrap_or(0), .unwrap_or(0),
sample_rate: sample_rate_idx, sample_rate: sample_rate_idx,
fft_length: DEFAULT_FFT_LENGTH, fft_length: DEFAULT_FFT_LENGTH,
x_log: true,
}, },
sample_rates: supported_sample_rates, sample_rates: supported_sample_rates,
audio_device: default_device, audio_device: default_device,
@ -425,7 +482,13 @@ impl MyApp {
thread::Builder::new() thread::Builder::new()
.name("fft".into()) .name("fft".into())
.spawn(move || { .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() .unwrap()
} }
@ -469,20 +532,34 @@ fn fft_thread_impl(
return; return;
} }
if let Ok(buf) = data_pipe.recv_timeout(std::time::Duration::from_secs_f64(0.1)) { 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 // Prepare the data for plotting
let lock = analyzer.lock().unwrap();
let charts: Vec<Vec<[f64; 2]>> = let charts: Vec<Vec<[f64; 2]>> =
analyzer if lock.log_plot() {
.lock() lock.last_analysis
.unwrap()
.last_analysis
.iter() .iter()
.map(|analysis| { .map(|analysis| {
Vec::from_iter(analysis.fft.iter().map(|(freq, amp, _energy)| { Vec::from_iter(analysis.fft.iter().map(|(freq, amp, _energy)| {
[(*freq as f64), lin_to_db(*amp as f64)] [
(*freq as f64).log10().clamp(0.0, f64::INFINITY),
lin_to_db(*amp as f64),
]
})) }))
}) })
.collect(); .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_min = plot_min(&charts);
let plot_max = plot_max(&charts); let plot_max = plot_max(&charts);