Compare commits

..

2 Commits

Author SHA1 Message Date
24d9fc7972 Fix audio_fft outputing phase instead of magnitude 2024-05-12 15:17:59 -04:00
162220aa72 Add debug plots 2024-05-12 15:17:06 -04:00
5 changed files with 140 additions and 6 deletions

10
Cargo.lock generated
View File

@ -1013,6 +1013,15 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "egui_plot"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7854b86dc1c2d352c5270db3d600011daa913d6b554141a03939761323288a1"
dependencies = [
"egui",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.11.0" version = "1.11.0"
@ -2828,6 +2837,7 @@ dependencies = [
"cpal", "cpal",
"eframe", "eframe",
"egui", "egui",
"egui_plot",
"env_logger", "env_logger",
"log", "log",
"realfft", "realfft",

View File

@ -10,6 +10,7 @@ edition = "2021"
anyhow = "1.0.83" anyhow = "1.0.83"
cpal = "0.15.3" cpal = "0.15.3"
egui = "0.27.0" egui = "0.27.0"
egui_plot = "0.27.2"
log = "0.4.21" log = "0.4.21"
realfft = "3.3.0" realfft = "3.3.0"

View File

@ -2,6 +2,8 @@ use eframe::{egui_glow, glow};
use egui::mutex::Mutex; use egui::mutex::Mutex;
use std::sync::Arc; use std::sync::Arc;
pub mod debug_plot;
use debug_plot::DebugPlots;
mod waterfall; mod waterfall;
use waterfall::Waterfall; use waterfall::Waterfall;
mod audio_fft; mod audio_fft;
@ -10,8 +12,8 @@ pub mod turbo_colormap;
const FFT_SIZE: usize = 1024; const FFT_SIZE: usize = 1024;
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
pub struct TemplateApp { pub struct TemplateApp {
plots: DebugPlots,
// Example stuff: // Example stuff:
label: String, label: String,
value: f32, value: f32,
@ -29,7 +31,8 @@ impl TemplateApp {
// Load previous app state (if any). // Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work. // Note that you must enable the `persistence` feature for this to work.
let (stream, rx) = AudioFFT::new(FFT_SIZE).unwrap(); let plots = DebugPlots::new();
let (stream, rx) = AudioFFT::new(FFT_SIZE, plots.get_sender()).unwrap();
let wf_size = stream.output_len; let wf_size = stream.output_len;
let gl = cc let gl = cc
.gl .gl
@ -37,6 +40,7 @@ impl TemplateApp {
.expect("Could not get gl context from glow backend"); .expect("Could not get gl context from glow backend");
Self { Self {
plots,
// Example stuff: // Example stuff:
label: "Hello World!".to_owned(), label: "Hello World!".to_owned(),
value: 2.7, value: 2.7,
@ -64,6 +68,7 @@ impl eframe::App for TemplateApp {
// For inspiration and more examples, go to https://emilk.github.io/egui // For inspiration and more examples, go to https://emilk.github.io/egui
ctx.request_repaint(); ctx.request_repaint();
self.plots.update_plots();
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
// The top panel is often a good place for a menu bar: // The top panel is often a good place for a menu bar:
@ -77,13 +82,16 @@ impl eframe::App for TemplateApp {
ctx.send_viewport_cmd(egui::ViewportCommand::Close); ctx.send_viewport_cmd(egui::ViewportCommand::Close);
} }
}); });
ui.add_space(16.0);
} }
self.plots.render_menu_buttons(ui);
ui.add_space(16.0);
egui::widgets::global_dark_light_mode_buttons(ui); egui::widgets::global_dark_light_mode_buttons(ui);
}); });
}); });
self.plots.render_plot_windows(ctx);
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
// The central panel the region left after adding TopPanel's and SidePanel's // The central panel the region left after adding TopPanel's and SidePanel's
ui.heading("eframe template"); ui.heading("eframe template");

View File

@ -5,7 +5,9 @@ use cpal::{
BufferSize, StreamConfig, BufferSize, StreamConfig,
}; };
use realfft::RealFftPlanner; use realfft::RealFftPlanner;
use std::sync::mpsc; use std::sync::mpsc::{self, Sender};
use super::debug_plot::PlotData;
pub struct AudioFFT { pub struct AudioFFT {
pub stream: cpal::Stream, pub stream: cpal::Stream,
@ -13,7 +15,10 @@ pub struct AudioFFT {
} }
impl AudioFFT { impl AudioFFT {
pub fn new(size: usize) -> Result<(Self, mpsc::Receiver<Vec<u8>>)> { pub fn new(
size: usize,
plot_tx: Sender<(&'static str, PlotData)>,
) -> Result<(Self, mpsc::Receiver<Vec<u8>>)> {
let output_len = size / 2 + 1; let output_len = size / 2 + 1;
// Create mpsc queue // Create mpsc queue
@ -45,9 +50,20 @@ impl AudioFFT {
assert_eq!(size, fft_in.len()); assert_eq!(size, fft_in.len());
fft.process_with_scratch(&mut fft_in, &mut fft_out, &mut fft_scratch) fft.process_with_scratch(&mut fft_in, &mut fft_out, &mut fft_scratch)
.unwrap(); .unwrap();
plot_tx
.send(("FFT Output", PlotData::Bode32(fft_out.clone())))
.unwrap();
fft_in.clear(); fft_in.clear();
let output: Vec<u8> = fft_out.iter().map(|c| (c.arg() * 255.0) as u8).collect(); let output: Vec<u8> = fft_out
.iter()
.map(|c| {
(((c.re * c.re) + (c.im * c.im)).sqrt() / size as f32 * 255.0) as u8
})
.collect();
assert_eq!(output_len, output.len()); assert_eq!(output_len, output.len());
plot_tx
.send(("FFT Processed Output", PlotData::U8(output.clone())))
.unwrap();
tx.send(output).unwrap(); tx.send(output).unwrap();
} }
}, },

99
src/app/debug_plot.rs Normal file
View File

@ -0,0 +1,99 @@
use std::collections::HashMap;
use std::sync::mpsc::{self, Sender};
use egui::{Context, Ui};
use egui_plot::{Line, Plot, PlotBounds, PlotPoints};
use realfft::num_complex::Complex32;
pub enum PlotData {
U8(Vec<u8>),
//F32(Vec<f32>),
Bode32(Vec<Complex32>),
}
pub struct DebugPlots {
plots: HashMap<&'static str, PlotData>,
plot_en: HashMap<&'static str, bool>,
rx: mpsc::Receiver<(&'static str, PlotData)>,
tx: mpsc::Sender<(&'static str, PlotData)>,
}
impl DebugPlots {
pub fn new() -> Self {
let (tx, rx) = mpsc::channel();
DebugPlots {
plots: HashMap::new(),
plot_en: HashMap::new(),
rx,
tx,
}
}
pub fn get_sender(&self) -> Sender<(&'static str, PlotData)> {
self.tx.clone()
}
pub fn update_plots(&mut self) {
while let Ok((key, plot)) = self.rx.try_recv() {
if self.plots.insert(key, plot).is_none() {
self.plot_en.insert(key, false);
}
}
}
pub fn render_menu_buttons(&mut self, ui: &mut Ui) {
ui.menu_button("Debug Plots", |ui| {
for &k in self.plots.keys() {
if !self.plot_en.contains_key(k) {
self.plot_en.insert(k, false);
}
let enabled = self.plot_en.get_mut(k).unwrap();
ui.checkbox(enabled, k);
}
});
}
pub fn render_plot_windows(&mut self, ctx: &Context) {
for (key, plot) in self.plots.iter() {
let enabled = self.plot_en.get_mut(key).unwrap();
if *enabled {
DebugPlots::render_window(ctx, key, plot, enabled);
}
}
}
fn render_window(ctx: &Context, title: &'static str, plot: &PlotData, open: &mut bool) {
egui::Window::new(title).open(open).show(ctx, |ui| {
ui.heading(title);
match plot {
PlotData::U8(v) => {
ui.heading("u8 Plot");
let line = Line::new(PlotPoints::from_iter(
v.iter().enumerate().map(|(i, y)| [i as f64, *y as f64]),
));
let plot = Plot::new(title);
plot.show(ui, |plot_ui| {
plot_ui.line(line);
plot_ui.set_plot_bounds(PlotBounds::from_min_max(
[-1.0, -1.0],
[(v.len() + 1) as f64, 256.0],
));
});
}
PlotData::Bode32(v) => {
ui.heading("Bode Plot");
let mag_line =
Line::new(PlotPoints::from_iter(v.iter().enumerate().map(|(i, c)| {
[i as f64, ((c.re * c.re) + (c.im * c.im)).sqrt() as f64]
})));
let phase_line = Line::new(PlotPoints::from_iter(
v.iter()
.enumerate()
.map(|(i, c)| [i as f64, c.arg() as f64]),
));
let plot = Plot::new(title);
plot.show(ui, |plot_ui| {
plot_ui.line(mag_line);
plot_ui.line(phase_line);
});
ui.heading("TODO");
}
};
});
}
}