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"
|
"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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
153
src/main.rs
153
src/main.rs
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user