diff --git a/Cargo.lock b/Cargo.lock index babff57..7d74969 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,8 @@ dependencies = [ "eframe", "egui", "egui_plot", + "env_logger", + "log", "num", "realfft", "ringbuf", @@ -68,7 +70,7 @@ checksum = "e084cb5168790c0c112626175412dc5ad127083441a8248ae49ddf6725519e83" dependencies = [ "accesskit", "accesskit_consumer", - "async-channel", + "async-channel 1.9.0", "atspi", "futures-lite 1.13.0", "serde", @@ -237,6 +239,19 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-channel" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37875bd9915b7d67c2f117ea2c30a0989874d0b2cb694fe25403c85763c0c9e" +dependencies = [ + "concurrent-queue", + "event-listener 3.1.0", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-executor" version = "1.6.0" @@ -289,7 +304,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41ed9d5715c2d329bf1b4da8d60455b99b187f27ba726df2883799af9af60997" dependencies = [ - "async-lock 3.0.0", + "async-lock 3.1.0", "cfg-if", "concurrent-queue", "futures-io", @@ -314,11 +329,11 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e900cdcd39bb94a14487d3f7ef92ca222162e6c7c3fe7cb3550ea75fb486ed" +checksum = "deb2ab2aa8a746e221ab826c73f48bc6ba41be6763f0855cb249eb6d154cf1d7" dependencies = [ - "event-listener 3.0.1", + "event-listener 3.1.0", "event-listener-strategy", "pin-project-lite", ] @@ -334,7 +349,7 @@ dependencies = [ "async-signal", "blocking", "cfg-if", - "event-listener 3.0.1", + "event-listener 3.1.0", "futures-lite 1.13.0", "rustix 0.38.21", "windows-sys 0.48.0", @@ -493,16 +508,16 @@ dependencies = [ [[package]] name = "blocking" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ - "async-channel", - "async-lock 2.8.0", + "async-channel 2.1.0", + "async-lock 3.1.0", "async-task", "fastrand 2.0.1", "futures-io", - "futures-lite 1.13.0", + "futures-lite 2.0.1", "piper", "tracing", ] @@ -561,11 +576,10 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" dependencies = [ - "jobserver", "libc", ] @@ -976,6 +990,19 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "env_logger" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "epaint" version = "0.23.0" @@ -1026,9 +1053,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cec0252c2afff729ee6f00e903d479fba81784c8e2bd77447673471fdfaea1" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" dependencies = [ "concurrent-queue", "parking", @@ -1041,7 +1068,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d96b852f1345da36d551b9473fa1e2b1eb5c5195585c6c018118bc92a8d91160" dependencies = [ - "event-listener 3.0.1", + "event-listener 3.1.0", "pin-project-lite", ] @@ -1319,6 +1346,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "idna" version = "0.4.0" @@ -1376,6 +1409,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix 0.38.21", + "windows-sys 0.48.0", +] + [[package]] name = "jni" version = "0.19.0" @@ -1426,15 +1470,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" -[[package]] -name = "jobserver" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" -dependencies = [ - "libc", -] - [[package]] name = "js-sys" version = "0.3.65" @@ -2472,6 +2507,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "termcolor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.50" diff --git a/Cargo.toml b/Cargo.toml index 62cc2c1..7c75b92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,8 @@ cpal = { version = "0.15.2" } eframe = { version = "0.23.0" } egui = { version = "0.23.0", features = ["puffin"] } egui_plot = "0.23.0" +env_logger = "0.10.1" +log = "0.4.20" num = "0.4.1" realfft = "3.3.0" ringbuf = "0.3.3" diff --git a/src/analyzer.rs b/src/analyzer.rs index ff961ef..f98d77c 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -1,60 +1,175 @@ use num::complex::ComplexFloat; -use num::integer::Roots; use num::traits::real::Real; -use num::{Float, FromPrimitive, Signed}; +use num::traits::AsPrimitive; +use num::{Float, FromPrimitive, Num, Signed, ToPrimitive}; use realfft::{RealFftPlanner, RealToComplex}; use ringbuf::{HeapRb, Rb}; -use rustfft::Fft; -use rustfft::{num_complex::Complex}; +use rustfft::num_complex::Complex; +use rustfft::{Fft, FftNum}; -use std::iter::{repeat}; +use std::fmt::{Debug, Display}; +use std::iter::repeat; +use std::ops::{AddAssign, Mul}; +use std::sync::{Arc, Mutex}; +pub struct DbMeasurement(U); +pub struct FrequencyMeasurement(U); +pub enum FftMeasurementValue { + Db(DbMeasurement), + Peak(FrequencyMeasurement, DbMeasurement), +} -use std::{ - fmt::Debug, - sync::{Arc, Mutex}, -}; +impl std::fmt::Display for FftMeasurementValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Db(v) => v.fmt(f), + Self::Peak(hz, v) => f.write_fmt(format_args!("{:.0} @ {:.0}", v, hz)), + } + } +} -use std::f64::consts::SQRT_2; +impl Display for DbMeasurement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{:.2}dB", lin_to_db(self.0))) + } +} -type FloatType = f32; -type SampleType = f32; +impl Display for FrequencyMeasurement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{:.0}Hz", self.0)) + } +} -pub trait Analyzer { +pub trait FftMeasurement: + FftMeasurementImpl + Send +{ + fn value(&self, channel: usize) -> FftMeasurementValue; + fn name(&self) -> &'static str; +} + +pub trait FftMeasurementImpl { + fn accum_td_sample(&mut self, sample: T, channel: usize); + fn accum_fd_sample(&mut self, bin: (U, U, U), channel: usize); // Slice of tuples of (freq, amplitude) + fn finalize(&mut self, channel: usize); +} + +pub struct RmsAmplitudeMeasurement { + accums: [U; NUM_CHANNELS], + n_samples: [usize; NUM_CHANNELS], + last_results: [U; NUM_CHANNELS], +} + +impl Default for RmsAmplitudeMeasurement { + fn default() -> Self { + Self { + accums: [U::zero(); NUM_CHANNELS], + n_samples: [0; NUM_CHANNELS], + last_results: [U::zero(); NUM_CHANNELS], + } + } +} + +impl + FftMeasurementImpl for RmsAmplitudeMeasurement +{ + fn accum_fd_sample(&mut self, _: (U, U, U), _: usize) {} + fn accum_td_sample(&mut self, sample: T, channel: usize) { + // 'square' + let f_sample = U::from(sample).unwrap_or(U::zero()); + self.accums[channel] += f_sample * f_sample; + self.n_samples[channel] += 1; + } + fn finalize(&mut self, channel: usize) { + // 'root mean' + self.last_results[channel] = + self.accums[channel] / U::from(self.n_samples[channel]).unwrap_or(U::one()); + + self.accums[channel] = U::zero(); + self.n_samples[channel] = 0; + } +} + +impl + FftMeasurement for RmsAmplitudeMeasurement +{ + fn value(&self, channel: usize) -> FftMeasurementValue { + FftMeasurementValue::Db(DbMeasurement(self.last_results[channel])) + } + fn name(&self) -> &'static str { + "RMS Amplitude" + } +} + +pub struct PeakFreqAmplitude { + cur_peaks: [(U, U, U); NUM_CHANNELS], + last_peaks: [(U, U, U); NUM_CHANNELS], +} + +impl Default for PeakFreqAmplitude { + fn default() -> Self { + Self { + cur_peaks: [(U::zero(), U::zero(), U::zero()); NUM_CHANNELS], + last_peaks: [(U::zero(), U::zero(), U::zero()); NUM_CHANNELS], + } + } +} + +impl + FftMeasurement for PeakFreqAmplitude +{ + fn name(&self) -> &'static str { + "Peak Amplitude" + } + fn value(&self, channel: usize) -> FftMeasurementValue { + FftMeasurementValue::Peak(FrequencyMeasurement(self.last_peaks[channel].0), DbMeasurement(self.last_peaks[channel].1)) + } +} + +impl + FftMeasurementImpl for PeakFreqAmplitude +{ + fn accum_fd_sample(&mut self, bin: (U, U, U), channel: usize) { + if bin.1 > self.cur_peaks[channel].1 { + self.cur_peaks[channel] = bin + } + } + fn accum_td_sample(&mut self, sample: T, channel: usize) { + + } + fn finalize(&mut self, channel: usize) { + self.last_peaks[channel] = self.cur_peaks[channel] + } +} + +pub trait Analyzer { /// Process some data, and return whether the analysis was updated as a result - fn process_data(&mut self, data: &[SampleType]) -> bool; - fn set_samplerate(&mut self, rate: FloatType); + fn process_data(&mut self, data: &[T]) -> bool; + fn set_samplerate(&mut self, rate: U); fn set_length(&mut self, length: usize); fn set_channels(&mut self, channels: usize); + fn add_measurement(&mut self, measurement: impl FftMeasurement + 'static); } #[derive(Debug, Clone, Default)] -pub struct FftAnalysis { - pub signal_frequency: T, - pub signal_power: T, - pub total_power: T, - pub noise_power: T, - pub thd_plus_noise: T, - pub max: T, - pub min: T, - pub fft: Vec<(T, T, T)>, // Frequency, Magnitude, Energy +pub struct FftAnalysis { + pub fft: Vec<(U, U, U)>, // Frequency, Magnitude, Energy } struct AudioBuf { - samples: Vec>, + samples: Vec>, // TODO: Improve data layout read_since_last_update: usize, } -pub trait WindowFunction { +pub trait WindowFunction { fn amplitude_correction_factor(&self) -> T; fn energy_correction_factor(&self) -> T; fn apply(&self, buf: &mut [T]); } -struct HanningWindow(Vec); +struct HanningWindow(Vec); -impl HanningWindow { +impl HanningWindow { fn new(fft_length: usize) -> Self { Self( apodize::hanning_iter(fft_length) @@ -64,7 +179,7 @@ impl HanningWindow { } } -impl WindowFunction for HanningWindow { +impl WindowFunction for HanningWindow { fn amplitude_correction_factor(&self) -> T { T::from_f64(2.0).unwrap() } @@ -78,109 +193,124 @@ impl WindowFunction for HanningWindow { } } -pub fn lin_to_db(lin: f64) -> f64 { - 20.0 * lin.log10() +pub fn lin_to_db(lin: T) -> T { + T::from_f64(20.0).unwrap_or(T::zero()) * lin.log10() } -pub fn lin_to_dbfs(lin: f64) -> f64 { - 20.0 * (lin * SQRT_2).log10() +pub fn lin_to_dbfs(lin: T) -> T { + T::from_f64(20.0).unwrap_or(T::zero()) + * (lin * T::from_f64(std::f64::consts::SQRT_2).unwrap_or(T::zero())).log10() } -pub struct FftAnalyzer { - pub last_analysis: Arc>>>, +// These bounds are really annoying but required to hold RealFftPlanner +pub struct FftAnalyzer { + pub last_analysis: Vec>, fft_length: usize, - sample_rate: FloatType, + sample_rate: U, channels: usize, - norm_factor: FloatType, - bin_freqs: Vec, + norm_factor: U, + bin_freqs: Vec, - planner: RealFftPlanner, - processor: Arc>, - window: HanningWindow, + planner: RealFftPlanner, + processor: Arc>, + window: HanningWindow, - audio_buf: Arc>>, + audio_buf: Arc>>, + + pub measurements: Vec>>, } -impl FftAnalyzer { - pub fn new(fft_length: usize, sample_rate: FloatType, channels: usize) -> Self { +impl FftAnalyzer { + pub fn new(fft_length: usize, sample_rate: U, channels: usize) -> Self { let mut planner = RealFftPlanner::new(); let bin_freqs = (0..fft_length) .map(|x| { - FloatType::from_f64(x as f64).unwrap() * sample_rate - / FloatType::from_f64(fft_length as f64).unwrap() + U::from_f64(x as f64).unwrap() * sample_rate + / U::from_f64(fft_length as f64).unwrap() }) .collect(); Self { - last_analysis: Arc::new(Mutex::new(Vec::with_capacity(channels))), + last_analysis: Vec::with_capacity(channels), fft_length, sample_rate, channels, - norm_factor: FloatType::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, processor: planner.plan_fft_forward(fft_length), planner, window: HanningWindow::new(fft_length), - audio_buf: Arc::new(Mutex::new(AudioBuf:: { + audio_buf: Arc::new(Mutex::new(AudioBuf:: { samples: repeat(0) .take(channels) .map(|_| HeapRb::new(fft_length)) .collect(), read_since_last_update: 0, })), + measurements: Vec::new(), } } - fn time_domain_process(&mut self, buf: &Vec) -> (FloatType, FloatType, FloatType) { - let mut rms_accumulator: FloatType = 0.0; - let mut local_max: FloatType = 0.0; - let mut local_min: FloatType = 0.0; - - for sample in buf { - rms_accumulator += sample * sample; - if *sample > local_max { - local_max = *sample - } - if *sample < local_min { - local_min = *sample + fn time_domain_process<'a>(&mut self, samples: impl Iterator, channel: usize) + where + T: Num + Copy + 'static, + U: Float + Mul + AddAssign + PartialOrd, + { + for sample in samples { + for meas in &mut self.measurements { + meas.accum_td_sample(*sample, channel); } } - - let total_power = (rms_accumulator / buf.len() as f32).sqrt(); - - (local_min, local_max, total_power) + for meas in &mut self.measurements { + meas.finalize(channel); + } } - fn process_frame(&mut self) { - let _channels_float = FloatType::from(self.channels as f32); + fn process_frame(&mut self) + where + T: Copy + Clone + Num + ToPrimitive + 'static, + U: Float + Mul + AddAssign + PartialOrd, + { + let _channels_float = U::from_usize(self.channels).unwrap_or(U::one()); let mut result = Vec::with_capacity(self.channels); + // Do time domain processing + for chan in 0..self.channels { - let mut in_buf: Vec = self.audio_buf.lock().unwrap().samples[chan] + let raw_in_buf: Vec<_> = self.audio_buf.lock().unwrap().samples[chan] .iter() .map(|x| *x) .collect(); + self.time_domain_process(raw_in_buf.iter(), chan); - let td_result = self.time_domain_process(&in_buf); + // Transform to float + let mut in_buf: Vec = raw_in_buf + .iter() + .map(|x| U::from(*x).unwrap_or(U::zero())) + .collect(); // apply window self.window.apply(&mut in_buf); - let mut out_buf = - Vec::from_iter(repeat(Complex { re: 0.0, im: 0.0 }).take(self.fft_length / 2 + 1)); + let mut out_buf = Vec::from_iter( + repeat(Complex { + re: U::zero(), + im: U::zero(), + }) + .take(self.fft_length / 2 + 1), + ); // do fft self.processor .process(&mut in_buf, out_buf.as_mut_slice()) .expect("fft failed?"); - let analysis_buf = out_buf + let analysis_buf: Vec<_> = out_buf .iter() .enumerate() .map(|(bin, complex)| { // get absolute value and normalize based on length - let raw_mag = (complex - / FloatType::from_f64(self.fft_length as f64 / 2.0).unwrap()) - .abs(); + let raw_mag = + (complex / U::from_f64(self.fft_length as f64 / 2.0).unwrap()).norm(); ( self.bin_freqs[bin], raw_mag * self.window.amplitude_correction_factor(), @@ -188,37 +318,40 @@ impl FftAnalyzer { ) }) .collect(); - result.push(FftAnalysis { - signal_frequency: 0.0, - signal_power: 0.0, - total_power: td_result.2, - noise_power: 0.0, - thd_plus_noise: 0.0, - max: td_result.1, - min: td_result.0, - fft: analysis_buf, - }); + for sample in analysis_buf.iter() { + for meas in &mut self.measurements { + meas.accum_fd_sample(*sample, chan); + } + } + result.push(FftAnalysis { fft: analysis_buf }); } - let mut lock = self.last_analysis.lock().unwrap(); - *lock = result; + self.last_analysis = result; } fn progress(&self) -> f32 { self.audio_buf.lock().unwrap().read_since_last_update as f32 / self.fft_length as f32 } } -impl Analyzer for FftAnalyzer { - fn process_data(&mut self, data: &[SampleType]) -> bool { - let _channels_float = FloatType::from(self.channels as f32); +impl< + T: Copy + Num + Clone + ToPrimitive + Send + 'static, + U: Float + FftNum + std::ops::AddAssign + Send, + const NUM_CHANNELS: usize, + > Analyzer for FftAnalyzer +{ + fn process_data(&mut self, data: &[T]) -> bool { + let _channels_float = U::from(self.channels as f32); let mut buf = self.audio_buf.lock().unwrap(); for chan in 0..self.channels { - buf.samples[chan].push_iter_overwrite(data.iter().skip(chan).step_by(self.channels).map(|x| *x)); + buf.samples[chan] + .push_iter_overwrite(data.iter().skip(chan).step_by(self.channels).map(|x| *x)); } buf.read_since_last_update += data.len() / self.channels; - if buf.samples[0].len() >= self.fft_length && buf.read_since_last_update >= self.fft_length / 4 { + if buf.samples[0].len() >= self.fft_length + && buf.read_since_last_update >= self.fft_length / 4 + { buf.read_since_last_update = 0; drop(buf); self.process_frame(); @@ -233,7 +366,10 @@ impl Analyzer for FftAnalyzer { fn set_length(&mut self, _length: usize) { unimplemented!(); // Need to rebuild the window and plan } - fn set_samplerate(&mut self, rate: FloatType) { + fn set_samplerate(&mut self, rate: U) { self.sample_rate = rate; } + fn add_measurement(&mut self, measurement: impl FftMeasurement + 'static) { + self.measurements.push(Box::new(measurement)); + } } diff --git a/src/main.rs b/src/main.rs index 925cdf8..dd7cca8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,7 @@ use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use cpal::SampleFormat; use egui::epaint::Hsva; -use egui::{Color32, Label, Widget}; +use egui::Widget; use egui_plot::{Legend, Line, Plot}; use std::mem::swap; @@ -11,9 +10,16 @@ use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::{Arc, Mutex}; use std::thread::{self, JoinHandle}; -mod analyzer; -use analyzer::{lin_to_db, Analyzer, FftAnalysis, FftAnalyzer}; +use log::debug; +mod analyzer; +use analyzer::{ + lin_to_db, Analyzer, FftAnalysis, FftAnalyzer, FftMeasurement, FftMeasurementValue, PeakFreqAmplitude, +}; + +use crate::analyzer::RmsAmplitudeMeasurement; + +const NUM_CHANNELS: usize = 2; const SAMPLE_RATES: &[u32] = &[ 11025, 22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000, ]; @@ -37,24 +43,26 @@ const DEFAULT_SAMPLE_RATE: u32 = 48000; const AUDIO_FRAME_SIZE: cpal::BufferSize = cpal::BufferSize::Fixed(512); type SampleType = f32; +type FloatType = f64; const FFT_LENGTHS: &[usize] = &[512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072]; const DEFAULT_FFT_LENGTH: usize = 1; #[derive(Default)] struct FftPlotData { - plot_points: Vec>, + plot_points: Vec>, plot_min: f64, plot_max: f64, } #[derive(Clone, Default)] -struct FftResult { - fft_analyses: Arc>>>, +struct FftResult { + fft_analyses: Arc>>>, plot_data: Arc>, } -fn ui_plot(ui: &mut egui::Ui, last_fft_result: FftResult) { +fn ui_plot(ui: &mut egui::Ui, last_fft_result: FftResult) { 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") @@ -89,6 +97,7 @@ struct MyApp { egui_ctx: egui::Context, fft_thread: JoinHandle<()>, + fft_analyzer: Arc>>, fft_close_channel: Sender<()>, fft_length: usize, fft_progress: Arc, @@ -100,7 +109,7 @@ struct MyApp { sample_rates: Vec, audio_stream: Option, sample_channel: Sender>, // Store this here so we can restart the audio stream - last_result: FftResult, + last_result: FftResult, ui_selections: MyAppUiSelections, } @@ -131,23 +140,13 @@ impl Widget for Measurement { ui.colored_label(CHANNEL_COLOURS[chan], value.label_format()); } }) - }).response + }) + .response } } impl eframe::App for MyApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - let measurements = vec![Measurement:: { - name: &"RMS", - values: self - .last_result - .fft_analyses - .lock() - .unwrap() - .iter() - .map(|x| Db(lin_to_db(x.total_power as f64))) - .collect(), - }]; egui::SidePanel::left("left_panel").show(ctx, |ui| { egui::ComboBox::from_label("Source") .selected_text(self.audio_devices[self.ui_selections.audio_device].clone()) @@ -169,8 +168,22 @@ impl eframe::App for MyApp { }); egui::CentralPanel::default().show(ctx, |ui| { ui.horizontal(|ui| { - for meas in measurements { - self.meas_box(ui, meas); + let lock = self.fft_analyzer.lock().unwrap(); + // Yikes + let values = lock + .measurements + .iter() + .map(|x| { + ( + x.name(), + // FIXME: we need to support more channels someday + [x.value(0), x.value(1)], + ) + }) + .collect::>(); + drop(lock); + for meas in values { + self.meas_box(ui, meas) } }); ui_plot(ui, self.last_result.clone()); @@ -224,8 +237,15 @@ impl MyApp { self.fft_progress.store(0, Ordering::Relaxed); } } - fn meas_box(&mut self, ui: &mut egui::Ui, meas: Measurement) { - ui.add(meas); + fn meas_box(&mut self, ui: &mut egui::Ui, meas: (&str, [FftMeasurementValue; NUM_CHANNELS])) { + ui.group(|ui| { + ui.vertical(|ui| { + ui.label(meas.0); + for chan in 0..NUM_CHANNELS { + ui.colored_label(CHANNEL_COLOURS[chan], meas.1[chan].to_string()); + } + }) + }); } fn new(cc: &eframe::CreationContext) -> Self { let host = cpal::default_host(); @@ -255,22 +275,26 @@ impl MyApp { .unwrap_or(0); let audio_config = cpal::StreamConfig { - channels: 2, + channels: NUM_CHANNELS as u16, sample_rate: cpal::SampleRate(supported_sample_rates[sample_rate_idx]), buffer_size: AUDIO_FRAME_SIZE, }; let fft_thread_ctx = cc.egui_ctx.clone(); - let last_result = FftResult::::default(); + let last_result = FftResult::::default(); let fft_thread_result = last_result.clone(); let fft_close_channel = channel::<()>(); - let fft_analyzer = FftAnalyzer::new( + let mut fft_analyzer = FftAnalyzer::::new( FFT_LENGTHS[DEFAULT_FFT_LENGTH], - supported_sample_rates[sample_rate_idx] as f32, - 2, + supported_sample_rates[sample_rate_idx] as f64, + NUM_CHANNELS, ); + fft_analyzer.add_measurement(RmsAmplitudeMeasurement::default()); + + let fft_analyzer = Arc::new(Mutex::new(fft_analyzer)); + let fft_thread_analyzer_ref = fft_analyzer.clone(); Self { egui_ctx: cc.egui_ctx.clone(), @@ -278,7 +302,7 @@ impl MyApp { .name("fft".into()) .spawn(move || { fft_thread_impl( - fft_analyzer, + fft_thread_analyzer_ref, fft_thread_ctx, sample_rx, fft_close_channel.1, @@ -286,6 +310,7 @@ impl MyApp { ) }) .unwrap(), + fft_analyzer, fft_length: FFT_LENGTHS[DEFAULT_FFT_LENGTH], fft_progress: Arc::new(AtomicUsize::new(0)), fft_close_channel: fft_close_channel.0, @@ -383,17 +408,24 @@ impl MyApp { data_pipe: Receiver>, close_pipe: Receiver<()>, ) -> JoinHandle<()> { - let fft_analyzer = FftAnalyzer::new( - FFT_LENGTHS[self.ui_selections.fft_length], - self.sample_rates[self.ui_selections.sample_rate] as f32, - 2, - ); let result_ref = self.last_result.clone(); let ctx_ref = self.egui_ctx.clone(); + let mut fft_analyzer = FftAnalyzer::::new( + FFT_LENGTHS[self.ui_selections.fft_length], + self.sample_rates[self.ui_selections.sample_rate] as f64, + NUM_CHANNELS, + ); + fft_analyzer.add_measurement(RmsAmplitudeMeasurement::default()); + fft_analyzer.add_measurement(PeakFreqAmplitude::default()); + + *self.fft_analyzer.lock().unwrap() = fft_analyzer; + + let fft_thread_analyzer_ref = self.fft_analyzer.clone(); + thread::Builder::new() .name("fft".into()) .spawn(move || { - fft_thread_impl(fft_analyzer, ctx_ref, data_pipe, close_pipe, result_ref) + fft_thread_impl(fft_thread_analyzer_ref, ctx_ref, data_pipe, close_pipe, result_ref) }) .unwrap() } @@ -424,11 +456,11 @@ fn plot_max(data: &Vec>) -> f64 { } fn fft_thread_impl( - mut analyzer: FftAnalyzer, + analyzer: Arc>>, ctx: egui::Context, data_pipe: Receiver>, close_pipe: Receiver<()>, - last_result: FftResult, + last_result: FftResult, ) { println!("FFT thread initialized"); loop { @@ -437,13 +469,13 @@ fn fft_thread_impl( return; } if let Ok(buf) = data_pipe.recv_timeout(std::time::Duration::from_secs_f64(0.1)) { - if analyzer.process_data(&buf) { + if analyzer.lock().unwrap().process_data(&buf) { // Prepare the data for plotting let charts: Vec> = analyzer - .last_analysis .lock() .unwrap() + .last_analysis .iter() .map(|analysis| { Vec::from_iter(analysis.fft.iter().map(|(freq, amp, _energy)| { @@ -463,12 +495,10 @@ fn fft_thread_impl( }; // while holding the plot_lock which will block the gui thread, update the rest *last_result.fft_analyses.lock().unwrap() = - analyzer.last_analysis.lock().unwrap().clone(); + analyzer.lock().unwrap().last_analysis.clone(); drop(plot_data); ctx.request_repaint(); } - } else { - println!("Didn't receive any data, looping around"); } } } @@ -479,7 +509,6 @@ fn handle_audio_frame( fft_progress: Arc, ) { // Copy and send to processor thread - // FIXME: Fails at fft lengths > 16384. Are we too slow collecting data? chan.send(data.to_vec()).unwrap_or_default(); fft_progress.store( fft_progress.load(Ordering::Relaxed) + data.len(), @@ -488,8 +517,9 @@ fn handle_audio_frame( } fn main() { + env_logger::init(); let options = eframe::NativeOptions { - initial_window_size: Some(egui::vec2(640.0, 480.0)), + initial_window_size: Some(egui::vec2(1024.0, 768.0)), ..Default::default() };