Compare commits

..

3 Commits

4 changed files with 496 additions and 206 deletions

96
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -1,60 +1,175 @@
use num::complex::ComplexFloat;
use num::integer::Roots;
use num::traits::real::Real;
use num::{Float, FromPrimitive, Signed};
use num::{Float, FromPrimitive, Num, ToPrimitive};
use realfft::{RealFftPlanner, RealToComplex};
use ringbuf::{HeapRb, Rb};
use rustfft::Fft;
use rustfft::{num_complex::Complex};
use rustfft::num_complex::Complex;
use rustfft::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: Float + Sized>(U);
pub struct FrequencyMeasurement<U: Float + Sized>(U);
pub enum FftMeasurementValue<U: Float + Sized> {
Db(DbMeasurement<U>),
Peak(FrequencyMeasurement<U>, DbMeasurement<U>),
}
use std::{
fmt::Debug,
sync::{Arc, Mutex},
};
impl<U: Float + FromPrimitive + Display> std::fmt::Display for FftMeasurementValue<U> {
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<U: Float + FromPrimitive + Display> Display for DbMeasurement<U> {
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<U: Float + Display> Display for FrequencyMeasurement<U> {
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<T: Num, U: FftNum + Float, const NUM_CHANNELS: usize>:
FftMeasurementImpl<T, U> + Send
{
fn value(&self, channel: usize) -> FftMeasurementValue<U>;
fn name(&self) -> &'static str;
}
pub trait FftMeasurementImpl<T: Num, U: FftNum + Float> {
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<U: FftNum, const NUM_CHANNELS: usize> {
accums: [U; NUM_CHANNELS],
n_samples: [usize; NUM_CHANNELS],
last_results: [U; NUM_CHANNELS],
}
impl<U: FftNum, const NUM_CHANNELS: usize> Default for RmsAmplitudeMeasurement<U, NUM_CHANNELS> {
fn default() -> Self {
Self {
accums: [U::zero(); NUM_CHANNELS],
n_samples: [0; NUM_CHANNELS],
last_results: [U::zero(); NUM_CHANNELS],
}
}
}
impl<T: Num + ToPrimitive, U: FftNum + Float + AddAssign, const NUM_CHANNELS: usize>
FftMeasurementImpl<T, U> for RmsAmplitudeMeasurement<U, NUM_CHANNELS>
{
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<T: Num + ToPrimitive, U: FftNum + Float + AddAssign, const NUM_CHANNELS: usize>
FftMeasurement<T, U, NUM_CHANNELS> for RmsAmplitudeMeasurement<U, NUM_CHANNELS>
{
fn value(&self, channel: usize) -> FftMeasurementValue<U> {
FftMeasurementValue::Db(DbMeasurement(self.last_results[channel]))
}
fn name(&self) -> &'static str {
"RMS Amplitude"
}
}
pub struct PeakFreqAmplitude<U: FftNum, const NUM_CHANNELS: usize> {
cur_peaks: [(U, U, U); NUM_CHANNELS],
last_peaks: [(U, U, U); NUM_CHANNELS],
}
impl<U: FftNum, const NUM_CHANNELS: usize> Default for PeakFreqAmplitude<U, NUM_CHANNELS> {
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<T: Num + ToPrimitive, U: FftNum + Float + AddAssign, const NUM_CHANNELS: usize>
FftMeasurement<T, U, NUM_CHANNELS> for PeakFreqAmplitude<U, NUM_CHANNELS>
{
fn name(&self) -> &'static str {
"Peak Amplitude"
}
fn value(&self, channel: usize) -> FftMeasurementValue<U> {
FftMeasurementValue::Peak(
FrequencyMeasurement(self.last_peaks[channel].0),
DbMeasurement(self.last_peaks[channel].1),
)
}
}
impl<T: Num + ToPrimitive, U: FftNum + Float + AddAssign, const NUM_CHANNELS: usize>
FftMeasurementImpl<T, U> for PeakFreqAmplitude<U, NUM_CHANNELS>
{
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<T: Num + Send, U: FftNum + Float + Send, const NUM_CHANNELS: usize> {
/// 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<T, U, NUM_CHANNELS> + 'static);
fn log_plot(&self) -> bool;
fn set_log_plot(&mut self, log_plot: bool);
}
#[derive(Debug, Clone, Default)]
pub struct FftAnalysis<T> {
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<U> {
pub fft: Vec<(U, U, U)>, // Frequency, Magnitude, Energy
}
struct AudioBuf<T> {
samples: Vec<HeapRb<T>>,
samples: Vec<HeapRb<T>>, // TODO: Improve data layout
read_since_last_update: usize,
}
pub trait WindowFunction<T: Real> {
pub trait WindowFunction<T: FftNum> {
fn amplitude_correction_factor(&self) -> T;
fn energy_correction_factor(&self) -> T;
fn apply(&self, buf: &mut [T]);
}
struct HanningWindow<T: Real>(Vec<T>);
struct HanningWindow<T>(Vec<T>);
impl<T: Real + FromPrimitive> HanningWindow<T> {
impl<T: FftNum> HanningWindow<T> {
fn new(fft_length: usize) -> Self {
Self(
apodize::hanning_iter(fft_length)
@ -64,7 +179,7 @@ impl<T: Real + FromPrimitive> HanningWindow<T> {
}
}
impl<T: Real + FromPrimitive> WindowFunction<T> for HanningWindow<T> {
impl<T: FftNum> WindowFunction<T> for HanningWindow<T> {
fn amplitude_correction_factor(&self) -> T {
T::from_f64(2.0).unwrap()
}
@ -78,109 +193,124 @@ impl<T: Real + FromPrimitive> WindowFunction<T> for HanningWindow<T> {
}
}
pub fn lin_to_db(lin: f64) -> f64 {
20.0 * lin.log10()
pub fn lin_to_db<T: Float + FromPrimitive>(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<T: Float + FromPrimitive>(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<Mutex<Vec<FftAnalysis<SampleType>>>>,
// These bounds are really annoying but required to hold RealFftPlanner
pub struct FftAnalyzer<T, U: FftNum, const NUM_CHANNELS: usize> {
pub last_analysis: Vec<FftAnalysis<U>>,
fft_length: usize,
sample_rate: FloatType,
sample_rate: U,
channels: usize,
norm_factor: FloatType,
bin_freqs: Vec<FloatType>,
bin_freqs: Vec<U>,
planner: RealFftPlanner<FloatType>,
processor: Arc<dyn RealToComplex<FloatType>>,
window: HanningWindow<FloatType>,
processor: Arc<dyn RealToComplex<U>>,
window: HanningWindow<U>,
audio_buf: Arc<Mutex<AudioBuf<FloatType>>>,
audio_buf: Arc<Mutex<AudioBuf<T>>>,
log_plot: bool,
pub measurements: Vec<Box<dyn FftMeasurement<T, U, NUM_CHANNELS>>>,
}
impl FftAnalyzer {
pub fn new(fft_length: usize, sample_rate: FloatType, channels: usize) -> Self {
impl<T: Num, U: FftNum, const NUM_CHANNELS: usize> FftAnalyzer<T, U, NUM_CHANNELS> {
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(),
bin_freqs,
log_plot: true,
processor: planner.plan_fft_forward(fft_length),
planner,
window: HanningWindow::new(fft_length),
audio_buf: Arc::new(Mutex::new(AudioBuf::<SampleType> {
audio_buf: Arc::new(Mutex::new(AudioBuf::<T> {
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, 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<Item = &'a T>, 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<FloatType> = 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<U> = 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,37 @@ 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;
}
fn progress(&self) -> f32 {
self.audio_buf.lock().unwrap().read_since_last_update as f32 / self.fft_length as f32
self.last_analysis = result;
}
}
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<T, U, NUM_CHANNELS> for FftAnalyzer<T, U, NUM_CHANNELS>
{
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 +363,16 @@ 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<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
}
}

View File

@ -1,9 +1,8 @@
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::SampleFormat;
use egui::epaint::Hsva;
use egui::{Color32, Label, Widget};
use egui_plot::{Legend, Line, Plot};
use egui::Widget;
use egui_plot::{GridInput, GridMark, Legend, Line, Plot};
use std::mem::swap;
use std::sync::atomic::{AtomicUsize, Ordering};
@ -12,8 +11,13 @@ use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
mod analyzer;
use analyzer::{lin_to_db, Analyzer, FftAnalysis, FftAnalyzer};
use analyzer::{
lin_to_db, Analyzer, FftAnalysis, FftAnalyzer, 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,58 +41,36 @@ 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<Vec<[f64; 2]>>,
plot_points: Vec<Vec<[f64; NUM_CHANNELS]>>,
plot_min: f64,
plot_max: f64,
}
#[derive(Clone, Default)]
struct FftResult<T> {
fft_analyses: Arc<Mutex<Vec<FftAnalysis<T>>>>,
struct FftResult<U> {
fft_analyses: Arc<Mutex<Vec<FftAnalysis<U>>>>,
plot_data: Arc<Mutex<FftPlotData>>,
}
fn ui_plot(ui: &mut egui::Ui, last_fft_result: FftResult<f32>) {
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 _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 {
egui_ctx: egui::Context,
fft_thread: JoinHandle<()>,
fft_analyzer: Arc<Mutex<FftAnalyzer<SampleType, FloatType, NUM_CHANNELS>>>,
fft_close_channel: Sender<()>,
fft_length: usize,
fft_progress: Arc<AtomicUsize>,
@ -100,7 +82,7 @@ struct MyApp {
sample_rates: Vec<u32>,
audio_stream: Option<cpal::Stream>,
sample_channel: Sender<Vec<SampleType>>, // Store this here so we can restart the audio stream
last_result: FftResult<f32>,
last_result: FftResult<FloatType>,
ui_selections: MyAppUiSelections,
}
@ -131,24 +113,15 @@ impl<T: MeasurementValueType> Widget for Measurement<T> {
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::<Db> {
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| {
ui.label("Capture Options");
egui::ComboBox::from_label("Source")
.selected_text(self.audio_devices[self.ui_selections.audio_device].clone())
.show_ui(ui, |ui| {
@ -162,6 +135,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)
@ -169,16 +146,94 @@ 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::<Vec<_>>();
drop(lock);
for meas in values {
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);
// TODO: Need coordinates_formatter for log and linear plots
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());
@ -216,6 +271,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);
@ -224,8 +287,19 @@ impl MyApp {
self.fft_progress.store(0, Ordering::Relaxed);
}
}
fn meas_box<T: MeasurementValueType>(&mut self, ui: &mut egui::Ui, meas: Measurement<T>) {
ui.add(meas);
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);
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 +329,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::<f32>::default();
let last_result = FftResult::<FloatType>::default();
let fft_thread_result = last_result.clone();
let fft_close_channel = channel::<()>();
let fft_analyzer = FftAnalyzer::new(
let mut fft_analyzer = FftAnalyzer::<SampleType, FloatType, NUM_CHANNELS>::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 +356,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 +364,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,
@ -296,6 +375,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,
@ -383,17 +463,30 @@ impl MyApp {
data_pipe: Receiver<Vec<SampleType>>,
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::<SampleType, FloatType, NUM_CHANNELS>::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 +517,11 @@ fn plot_max(data: &Vec<Vec<[f64; 2]>>) -> f64 {
}
fn fft_thread_impl(
mut analyzer: FftAnalyzer,
analyzer: Arc<Mutex<FftAnalyzer<SampleType, FloatType, NUM_CHANNELS>>>,
ctx: egui::Context,
data_pipe: Receiver<Vec<SampleType>>,
close_pipe: Receiver<()>,
last_result: FftResult<f32>,
last_result: FftResult<FloatType>,
) {
println!("FFT thread initialized");
loop {
@ -437,20 +530,34 @@ 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) {
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
.last_analysis
.lock()
.unwrap()
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);
@ -463,12 +570,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 +584,6 @@ fn handle_audio_frame(
fft_progress: Arc<AtomicUsize>,
) {
// 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 +592,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()
};