Selectable log plot support! :D
This commit is contained in:
parent
9c195f7b96
commit
6028d5ca05
@ -122,7 +122,10 @@ impl<T: Num + ToPrimitive, U: FftNum + Float + AddAssign, const NUM_CHANNELS: us
|
||||
"Peak Amplitude"
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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_channels(&mut self, channels: usize);
|
||||
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)]
|
||||
@ -218,6 +221,8 @@ pub struct FftAnalyzer<T, U: FftNum, const NUM_CHANNELS: usize> {
|
||||
|
||||
audio_buf: Arc<Mutex<AudioBuf<T>>>,
|
||||
|
||||
log_plot: bool,
|
||||
|
||||
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(),
|
||||
bin_freqs,
|
||||
|
||||
log_plot: true,
|
||||
|
||||
processor: planner.plan_fft_forward(fft_length),
|
||||
planner,
|
||||
window: HanningWindow::new(fft_length),
|
||||
@ -372,4 +379,10 @@ impl<
|
||||
fn add_measurement(&mut self, measurement: impl FftMeasurement<T, U, NUM_CHANNELS> + '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
|
||||
}
|
||||
}
|
||||
|
153
src/main.rs
153
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<U> {
|
||||
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)]
|
||||
struct MyAppUiSelections {
|
||||
audio_device: usize,
|
||||
sample_rate: usize,
|
||||
fft_length: usize,
|
||||
x_log: bool,
|
||||
}
|
||||
|
||||
struct MyApp {
|
||||
@ -148,6 +124,7 @@ impl<T: MeasurementValueType> Widget for Measurement<T> {
|
||||
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<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 {
|
||||
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<FloatType>; NUM_CHANNELS])) {
|
||||
fn meas_box(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
meas: (&str, [FftMeasurementValue<FloatType>; 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<Vec<[f64; 2]>> =
|
||||
analyzer
|
||||
.lock()
|
||||
.unwrap()
|
||||
.last_analysis
|
||||
if lock.log_plot() {
|
||||
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)]
|
||||
[
|
||||
(*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_max = plot_max(&charts);
|
||||
|
Loading…
Reference in New Issue
Block a user