From 162220aa72b7fbb9d4ac7fbc38d143a788bf188d Mon Sep 17 00:00:00 2001 From: Lucas Schumacher Date: Sun, 12 May 2024 15:17:06 -0400 Subject: [PATCH] Add debug plots --- Cargo.lock | 10 +++++ Cargo.toml | 1 + src/app.rs | 14 ++++-- src/app/audio_fft.rs | 15 ++++++- src/app/debug_plot.rs | 99 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 src/app/debug_plot.rs diff --git a/Cargo.lock b/Cargo.lock index ff4764b..c8c1897 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1013,6 +1013,15 @@ dependencies = [ "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]] name = "either" version = "1.11.0" @@ -2828,6 +2837,7 @@ dependencies = [ "cpal", "eframe", "egui", + "egui_plot", "env_logger", "log", "realfft", diff --git a/Cargo.toml b/Cargo.toml index 9cba2f8..8a30f90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ edition = "2021" anyhow = "1.0.83" cpal = "0.15.3" egui = "0.27.0" +egui_plot = "0.27.2" log = "0.4.21" realfft = "3.3.0" diff --git a/src/app.rs b/src/app.rs index 35768fa..a5a2883 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,6 +2,8 @@ use eframe::{egui_glow, glow}; use egui::mutex::Mutex; use std::sync::Arc; +pub mod debug_plot; +use debug_plot::DebugPlots; mod waterfall; use waterfall::Waterfall; mod audio_fft; @@ -10,8 +12,8 @@ pub mod turbo_colormap; const FFT_SIZE: usize = 1024; -/// We derive Deserialize/Serialize so we can persist app state on shutdown. pub struct TemplateApp { + plots: DebugPlots, // Example stuff: label: String, value: f32, @@ -29,7 +31,8 @@ impl TemplateApp { // Load previous app state (if any). // 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 gl = cc .gl @@ -37,6 +40,7 @@ impl TemplateApp { .expect("Could not get gl context from glow backend"); Self { + plots, // Example stuff: label: "Hello World!".to_owned(), value: 2.7, @@ -64,6 +68,7 @@ impl eframe::App for TemplateApp { // For inspiration and more examples, go to https://emilk.github.io/egui ctx.request_repaint(); + self.plots.update_plots(); egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { // 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); } }); - ui.add_space(16.0); } + self.plots.render_menu_buttons(ui); + ui.add_space(16.0); egui::widgets::global_dark_light_mode_buttons(ui); }); }); + self.plots.render_plot_windows(ctx); + egui::CentralPanel::default().show(ctx, |ui| { // The central panel the region left after adding TopPanel's and SidePanel's ui.heading("eframe template"); diff --git a/src/app/audio_fft.rs b/src/app/audio_fft.rs index 172f2eb..7df784f 100644 --- a/src/app/audio_fft.rs +++ b/src/app/audio_fft.rs @@ -5,7 +5,9 @@ use cpal::{ BufferSize, StreamConfig, }; use realfft::RealFftPlanner; -use std::sync::mpsc; +use std::sync::mpsc::{self, Sender}; + +use super::debug_plot::PlotData; pub struct AudioFFT { pub stream: cpal::Stream, @@ -13,7 +15,10 @@ pub struct AudioFFT { } impl AudioFFT { - pub fn new(size: usize) -> Result<(Self, mpsc::Receiver>)> { + pub fn new( + size: usize, + plot_tx: Sender<(&'static str, PlotData)>, + ) -> Result<(Self, mpsc::Receiver>)> { let output_len = size / 2 + 1; // Create mpsc queue @@ -45,9 +50,15 @@ impl AudioFFT { assert_eq!(size, fft_in.len()); fft.process_with_scratch(&mut fft_in, &mut fft_out, &mut fft_scratch) .unwrap(); + plot_tx + .send(("FFT Output", PlotData::Bode32(fft_out.clone()))) + .unwrap(); fft_in.clear(); let output: Vec = fft_out.iter().map(|c| (c.arg() * 255.0) as u8).collect(); assert_eq!(output_len, output.len()); + plot_tx + .send(("FFT Processed Output", PlotData::U8(output.clone()))) + .unwrap(); tx.send(output).unwrap(); } }, diff --git a/src/app/debug_plot.rs b/src/app/debug_plot.rs new file mode 100644 index 0000000..14331dc --- /dev/null +++ b/src/app/debug_plot.rs @@ -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), + //F32(Vec), + Bode32(Vec), +} + +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"); + } + }; + }); + } +}