Compare commits
3 Commits
6fdcaa0f5b
...
2df27bec85
Author | SHA1 | Date | |
---|---|---|---|
2df27bec85 | |||
6028d5ca05 | |||
9c195f7b96 |
96
Cargo.lock
generated
96
Cargo.lock
generated
@ -11,6 +11,8 @@ dependencies = [
|
|||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui_plot",
|
"egui_plot",
|
||||||
|
"env_logger",
|
||||||
|
"log",
|
||||||
"num",
|
"num",
|
||||||
"realfft",
|
"realfft",
|
||||||
"ringbuf",
|
"ringbuf",
|
||||||
@ -68,7 +70,7 @@ checksum = "e084cb5168790c0c112626175412dc5ad127083441a8248ae49ddf6725519e83"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"accesskit_consumer",
|
"accesskit_consumer",
|
||||||
"async-channel",
|
"async-channel 1.9.0",
|
||||||
"atspi",
|
"atspi",
|
||||||
"futures-lite 1.13.0",
|
"futures-lite 1.13.0",
|
||||||
"serde",
|
"serde",
|
||||||
@ -237,6 +239,19 @@ dependencies = [
|
|||||||
"futures-core",
|
"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]]
|
[[package]]
|
||||||
name = "async-executor"
|
name = "async-executor"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@ -289,7 +304,7 @@ version = "2.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41ed9d5715c2d329bf1b4da8d60455b99b187f27ba726df2883799af9af60997"
|
checksum = "41ed9d5715c2d329bf1b4da8d60455b99b187f27ba726df2883799af9af60997"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-lock 3.0.0",
|
"async-lock 3.1.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
@ -314,11 +329,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-lock"
|
name = "async-lock"
|
||||||
version = "3.0.0"
|
version = "3.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "45e900cdcd39bb94a14487d3f7ef92ca222162e6c7c3fe7cb3550ea75fb486ed"
|
checksum = "deb2ab2aa8a746e221ab826c73f48bc6ba41be6763f0855cb249eb6d154cf1d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"event-listener 3.0.1",
|
"event-listener 3.1.0",
|
||||||
"event-listener-strategy",
|
"event-listener-strategy",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
@ -334,7 +349,7 @@ dependencies = [
|
|||||||
"async-signal",
|
"async-signal",
|
||||||
"blocking",
|
"blocking",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"event-listener 3.0.1",
|
"event-listener 3.1.0",
|
||||||
"futures-lite 1.13.0",
|
"futures-lite 1.13.0",
|
||||||
"rustix 0.38.21",
|
"rustix 0.38.21",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
@ -493,16 +508,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blocking"
|
name = "blocking"
|
||||||
version = "1.4.1"
|
version = "1.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c36a4d0d48574b3dd360b4b7d95cc651d2b6557b6402848a27d4b228a473e2a"
|
checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel",
|
"async-channel 2.1.0",
|
||||||
"async-lock 2.8.0",
|
"async-lock 3.1.0",
|
||||||
"async-task",
|
"async-task",
|
||||||
"fastrand 2.0.1",
|
"fastrand 2.0.1",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-lite 1.13.0",
|
"futures-lite 2.0.1",
|
||||||
"piper",
|
"piper",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
@ -561,11 +576,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.83"
|
version = "1.0.84"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
|
checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jobserver",
|
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -976,6 +990,19 @@ dependencies = [
|
|||||||
"syn 2.0.39",
|
"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]]
|
[[package]]
|
||||||
name = "epaint"
|
name = "epaint"
|
||||||
version = "0.23.0"
|
version = "0.23.0"
|
||||||
@ -1026,9 +1053,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "event-listener"
|
name = "event-listener"
|
||||||
version = "3.0.1"
|
version = "3.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "01cec0252c2afff729ee6f00e903d479fba81784c8e2bd77447673471fdfaea1"
|
checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"parking",
|
"parking",
|
||||||
@ -1041,7 +1068,7 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d96b852f1345da36d551b9473fa1e2b1eb5c5195585c6c018118bc92a8d91160"
|
checksum = "d96b852f1345da36d551b9473fa1e2b1eb5c5195585c6c018118bc92a8d91160"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"event-listener 3.0.1",
|
"event-listener 3.1.0",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1319,6 +1346,12 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"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]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -1376,6 +1409,17 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"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]]
|
[[package]]
|
||||||
name = "jni"
|
name = "jni"
|
||||||
version = "0.19.0"
|
version = "0.19.0"
|
||||||
@ -1426,15 +1470,6 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jobserver"
|
|
||||||
version = "0.1.27"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.65"
|
version = "0.3.65"
|
||||||
@ -2472,6 +2507,15 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"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]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.50"
|
version = "1.0.50"
|
||||||
|
@ -11,6 +11,8 @@ cpal = { version = "0.15.2" }
|
|||||||
eframe = { version = "0.23.0" }
|
eframe = { version = "0.23.0" }
|
||||||
egui = { version = "0.23.0", features = ["puffin"] }
|
egui = { version = "0.23.0", features = ["puffin"] }
|
||||||
egui_plot = "0.23.0"
|
egui_plot = "0.23.0"
|
||||||
|
env_logger = "0.10.1"
|
||||||
|
log = "0.4.20"
|
||||||
num = "0.4.1"
|
num = "0.4.1"
|
||||||
realfft = "3.3.0"
|
realfft = "3.3.0"
|
||||||
ringbuf = "0.3.3"
|
ringbuf = "0.3.3"
|
||||||
|
337
src/analyzer.rs
337
src/analyzer.rs
@ -1,60 +1,175 @@
|
|||||||
use num::complex::ComplexFloat;
|
use num::{Float, FromPrimitive, Num, ToPrimitive};
|
||||||
use num::integer::Roots;
|
|
||||||
use num::traits::real::Real;
|
|
||||||
use num::{Float, FromPrimitive, Signed};
|
|
||||||
use realfft::{RealFftPlanner, RealToComplex};
|
use realfft::{RealFftPlanner, RealToComplex};
|
||||||
use ringbuf::{HeapRb, Rb};
|
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::{
|
impl<U: Float + FromPrimitive + Display> std::fmt::Display for FftMeasurementValue<U> {
|
||||||
fmt::Debug,
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
sync::{Arc, Mutex},
|
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;
|
impl<U: Float + Display> Display for FrequencyMeasurement<U> {
|
||||||
type SampleType = f32;
|
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
|
/// Process some data, and return whether the analysis was updated as a result
|
||||||
fn process_data(&mut self, data: &[SampleType]) -> bool;
|
fn process_data(&mut self, data: &[T]) -> bool;
|
||||||
fn set_samplerate(&mut self, rate: FloatType);
|
fn set_samplerate(&mut self, rate: U);
|
||||||
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 log_plot(&self) -> bool;
|
||||||
|
fn set_log_plot(&mut self, log_plot: bool);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct FftAnalysis<T> {
|
pub struct FftAnalysis<U> {
|
||||||
pub signal_frequency: T,
|
pub fft: Vec<(U, U, U)>, // Frequency, Magnitude, Energy
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AudioBuf<T> {
|
struct AudioBuf<T> {
|
||||||
samples: Vec<HeapRb<T>>,
|
samples: Vec<HeapRb<T>>, // TODO: Improve data layout
|
||||||
read_since_last_update: usize,
|
read_since_last_update: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait WindowFunction<T: Real> {
|
pub trait WindowFunction<T: FftNum> {
|
||||||
fn amplitude_correction_factor(&self) -> T;
|
fn amplitude_correction_factor(&self) -> T;
|
||||||
fn energy_correction_factor(&self) -> T;
|
fn energy_correction_factor(&self) -> T;
|
||||||
fn apply(&self, buf: &mut [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 {
|
fn new(fft_length: usize) -> Self {
|
||||||
Self(
|
Self(
|
||||||
apodize::hanning_iter(fft_length)
|
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 {
|
fn amplitude_correction_factor(&self) -> T {
|
||||||
T::from_f64(2.0).unwrap()
|
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 {
|
pub fn lin_to_db<T: Float + FromPrimitive>(lin: T) -> T {
|
||||||
20.0 * lin.log10()
|
T::from_f64(20.0).unwrap_or(T::zero()) * lin.log10()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lin_to_dbfs(lin: f64) -> f64 {
|
pub fn lin_to_dbfs<T: Float + FromPrimitive>(lin: T) -> T {
|
||||||
20.0 * (lin * SQRT_2).log10()
|
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 {
|
// These bounds are really annoying but required to hold RealFftPlanner
|
||||||
pub last_analysis: Arc<Mutex<Vec<FftAnalysis<SampleType>>>>,
|
pub struct FftAnalyzer<T, U: FftNum, const NUM_CHANNELS: usize> {
|
||||||
|
pub last_analysis: Vec<FftAnalysis<U>>,
|
||||||
|
|
||||||
fft_length: usize,
|
fft_length: usize,
|
||||||
sample_rate: FloatType,
|
sample_rate: U,
|
||||||
channels: usize,
|
channels: usize,
|
||||||
norm_factor: FloatType,
|
bin_freqs: Vec<U>,
|
||||||
bin_freqs: Vec<FloatType>,
|
|
||||||
|
|
||||||
planner: RealFftPlanner<FloatType>,
|
processor: Arc<dyn RealToComplex<U>>,
|
||||||
processor: Arc<dyn RealToComplex<FloatType>>,
|
window: HanningWindow<U>,
|
||||||
window: HanningWindow<FloatType>,
|
|
||||||
|
|
||||||
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 {
|
impl<T: Num, U: FftNum, const NUM_CHANNELS: usize> FftAnalyzer<T, U, NUM_CHANNELS> {
|
||||||
pub fn new(fft_length: usize, sample_rate: FloatType, channels: usize) -> Self {
|
pub fn new(fft_length: usize, sample_rate: U, channels: usize) -> Self {
|
||||||
let mut planner = RealFftPlanner::new();
|
let mut planner = RealFftPlanner::new();
|
||||||
let bin_freqs = (0..fft_length)
|
let bin_freqs = (0..fft_length)
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
FloatType::from_f64(x as f64).unwrap() * sample_rate
|
U::from_f64(x as f64).unwrap() * sample_rate
|
||||||
/ FloatType::from_f64(fft_length as f64).unwrap()
|
/ U::from_f64(fft_length as f64).unwrap()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Self {
|
Self {
|
||||||
last_analysis: Arc::new(Mutex::new(Vec::with_capacity(channels))),
|
last_analysis: Vec::with_capacity(channels),
|
||||||
fft_length,
|
fft_length,
|
||||||
sample_rate,
|
sample_rate,
|
||||||
channels,
|
channels,
|
||||||
norm_factor: FloatType::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,
|
|
||||||
window: HanningWindow::new(fft_length),
|
window: HanningWindow::new(fft_length),
|
||||||
audio_buf: Arc::new(Mutex::new(AudioBuf::<SampleType> {
|
audio_buf: Arc::new(Mutex::new(AudioBuf::<T> {
|
||||||
samples: repeat(0)
|
samples: repeat(0)
|
||||||
.take(channels)
|
.take(channels)
|
||||||
.map(|_| HeapRb::new(fft_length))
|
.map(|_| HeapRb::new(fft_length))
|
||||||
.collect(),
|
.collect(),
|
||||||
read_since_last_update: 0,
|
read_since_last_update: 0,
|
||||||
})),
|
})),
|
||||||
|
measurements: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn time_domain_process(&mut self, buf: &Vec<FloatType>) -> (FloatType, FloatType, FloatType) {
|
fn time_domain_process<'a>(&mut self, samples: impl Iterator<Item = &'a T>, channel: usize)
|
||||||
let mut rms_accumulator: FloatType = 0.0;
|
where
|
||||||
let mut local_max: FloatType = 0.0;
|
T: Num + Copy + 'static,
|
||||||
let mut local_min: FloatType = 0.0;
|
U: Float + Mul + AddAssign + PartialOrd,
|
||||||
|
{
|
||||||
for sample in buf {
|
for sample in samples {
|
||||||
rms_accumulator += sample * sample;
|
for meas in &mut self.measurements {
|
||||||
if *sample > local_max {
|
meas.accum_td_sample(*sample, channel);
|
||||||
local_max = *sample
|
|
||||||
}
|
|
||||||
if *sample < local_min {
|
|
||||||
local_min = *sample
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for meas in &mut self.measurements {
|
||||||
let total_power = (rms_accumulator / buf.len() as f32).sqrt();
|
meas.finalize(channel);
|
||||||
|
|
||||||
(local_min, local_max, total_power)
|
|
||||||
}
|
}
|
||||||
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);
|
let mut result = Vec::with_capacity(self.channels);
|
||||||
|
|
||||||
|
// Do time domain processing
|
||||||
|
|
||||||
for chan in 0..self.channels {
|
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()
|
.iter()
|
||||||
.map(|x| *x)
|
.map(|x| *x)
|
||||||
.collect();
|
.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
|
// apply window
|
||||||
self.window.apply(&mut in_buf);
|
self.window.apply(&mut in_buf);
|
||||||
|
|
||||||
let mut out_buf =
|
let mut out_buf = Vec::from_iter(
|
||||||
Vec::from_iter(repeat(Complex { re: 0.0, im: 0.0 }).take(self.fft_length / 2 + 1));
|
repeat(Complex {
|
||||||
|
re: U::zero(),
|
||||||
|
im: U::zero(),
|
||||||
|
})
|
||||||
|
.take(self.fft_length / 2 + 1),
|
||||||
|
);
|
||||||
|
|
||||||
// do fft
|
// do fft
|
||||||
self.processor
|
self.processor
|
||||||
.process(&mut in_buf, out_buf.as_mut_slice())
|
.process(&mut in_buf, out_buf.as_mut_slice())
|
||||||
.expect("fft failed?");
|
.expect("fft failed?");
|
||||||
|
|
||||||
let analysis_buf = out_buf
|
let analysis_buf: Vec<_> = out_buf
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(bin, complex)| {
|
.map(|(bin, complex)| {
|
||||||
// get absolute value and normalize based on length
|
// get absolute value and normalize based on length
|
||||||
let raw_mag = (complex
|
let raw_mag =
|
||||||
/ FloatType::from_f64(self.fft_length as f64 / 2.0).unwrap())
|
(complex / U::from_f64(self.fft_length as f64 / 2.0).unwrap()).norm();
|
||||||
.abs();
|
|
||||||
(
|
(
|
||||||
self.bin_freqs[bin],
|
self.bin_freqs[bin],
|
||||||
raw_mag * self.window.amplitude_correction_factor(),
|
raw_mag * self.window.amplitude_correction_factor(),
|
||||||
@ -188,37 +318,37 @@ impl FftAnalyzer {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
result.push(FftAnalysis {
|
for sample in analysis_buf.iter() {
|
||||||
signal_frequency: 0.0,
|
for meas in &mut self.measurements {
|
||||||
signal_power: 0.0,
|
meas.accum_fd_sample(*sample, chan);
|
||||||
total_power: td_result.2,
|
}
|
||||||
noise_power: 0.0,
|
}
|
||||||
thd_plus_noise: 0.0,
|
result.push(FftAnalysis { fft: analysis_buf });
|
||||||
max: td_result.1,
|
|
||||||
min: td_result.0,
|
|
||||||
fft: analysis_buf,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut lock = self.last_analysis.lock().unwrap();
|
self.last_analysis = result;
|
||||||
*lock = 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 {
|
impl<
|
||||||
fn process_data(&mut self, data: &[SampleType]) -> bool {
|
T: Copy + Num + Clone + ToPrimitive + Send + 'static,
|
||||||
let _channels_float = FloatType::from(self.channels as f32);
|
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();
|
let mut buf = self.audio_buf.lock().unwrap();
|
||||||
for chan in 0..self.channels {
|
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;
|
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;
|
buf.read_since_last_update = 0;
|
||||||
drop(buf);
|
drop(buf);
|
||||||
self.process_frame();
|
self.process_frame();
|
||||||
@ -233,7 +363,16 @@ impl Analyzer for FftAnalyzer {
|
|||||||
fn set_length(&mut self, _length: usize) {
|
fn set_length(&mut self, _length: usize) {
|
||||||
unimplemented!(); // Need to rebuild the window and plan
|
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;
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
257
src/main.rs
257
src/main.rs
@ -1,9 +1,8 @@
|
|||||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||||
use cpal::SampleFormat;
|
|
||||||
|
|
||||||
use egui::epaint::Hsva;
|
use egui::epaint::Hsva;
|
||||||
use egui::{Color32, Label, Widget};
|
use egui::Widget;
|
||||||
use egui_plot::{Legend, Line, Plot};
|
use egui_plot::{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};
|
||||||
@ -12,8 +11,13 @@ use std::sync::{Arc, Mutex};
|
|||||||
use std::thread::{self, JoinHandle};
|
use std::thread::{self, JoinHandle};
|
||||||
|
|
||||||
mod analyzer;
|
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] = &[
|
const SAMPLE_RATES: &[u32] = &[
|
||||||
11025, 22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000,
|
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);
|
const AUDIO_FRAME_SIZE: cpal::BufferSize = cpal::BufferSize::Fixed(512);
|
||||||
|
|
||||||
type SampleType = f32;
|
type SampleType = f32;
|
||||||
|
type FloatType = f64;
|
||||||
const FFT_LENGTHS: &[usize] = &[512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072];
|
const FFT_LENGTHS: &[usize] = &[512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072];
|
||||||
const DEFAULT_FFT_LENGTH: usize = 1;
|
const DEFAULT_FFT_LENGTH: usize = 1;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct FftPlotData {
|
struct FftPlotData {
|
||||||
plot_points: Vec<Vec<[f64; 2]>>,
|
plot_points: Vec<Vec<[f64; NUM_CHANNELS]>>,
|
||||||
plot_min: f64,
|
plot_min: f64,
|
||||||
plot_max: f64,
|
plot_max: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
struct FftResult<T> {
|
struct FftResult<U> {
|
||||||
fft_analyses: Arc<Mutex<Vec<FftAnalysis<T>>>>,
|
fft_analyses: Arc<Mutex<Vec<FftAnalysis<U>>>>,
|
||||||
plot_data: Arc<Mutex<FftPlotData>>,
|
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)]
|
#[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 {
|
||||||
egui_ctx: egui::Context,
|
egui_ctx: egui::Context,
|
||||||
|
|
||||||
fft_thread: JoinHandle<()>,
|
fft_thread: JoinHandle<()>,
|
||||||
|
fft_analyzer: Arc<Mutex<FftAnalyzer<SampleType, FloatType, NUM_CHANNELS>>>,
|
||||||
fft_close_channel: Sender<()>,
|
fft_close_channel: Sender<()>,
|
||||||
fft_length: usize,
|
fft_length: usize,
|
||||||
fft_progress: Arc<AtomicUsize>,
|
fft_progress: Arc<AtomicUsize>,
|
||||||
@ -100,7 +82,7 @@ struct MyApp {
|
|||||||
sample_rates: Vec<u32>,
|
sample_rates: Vec<u32>,
|
||||||
audio_stream: Option<cpal::Stream>,
|
audio_stream: Option<cpal::Stream>,
|
||||||
sample_channel: Sender<Vec<SampleType>>, // Store this here so we can restart the audio 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,
|
ui_selections: MyAppUiSelections,
|
||||||
}
|
}
|
||||||
@ -131,24 +113,15 @@ impl<T: MeasurementValueType> Widget for Measurement<T> {
|
|||||||
ui.colored_label(CHANNEL_COLOURS[chan], value.label_format());
|
ui.colored_label(CHANNEL_COLOURS[chan], value.label_format());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}).response
|
})
|
||||||
|
.response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
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| {
|
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| {
|
||||||
@ -162,6 +135,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)
|
||||||
@ -169,16 +146,94 @@ impl eframe::App for MyApp {
|
|||||||
});
|
});
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
for meas in measurements {
|
let lock = self.fft_analyzer.lock().unwrap();
|
||||||
self.meas_box(ui, meas);
|
// 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 {
|
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) {
|
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());
|
||||||
@ -216,6 +271,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);
|
||||||
@ -224,8 +287,19 @@ impl MyApp {
|
|||||||
self.fft_progress.store(0, Ordering::Relaxed);
|
self.fft_progress.store(0, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn meas_box<T: MeasurementValueType>(&mut self, ui: &mut egui::Ui, meas: Measurement<T>) {
|
fn meas_box(
|
||||||
ui.add(meas);
|
&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 {
|
fn new(cc: &eframe::CreationContext) -> Self {
|
||||||
let host = cpal::default_host();
|
let host = cpal::default_host();
|
||||||
@ -255,22 +329,26 @@ impl MyApp {
|
|||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
let audio_config = cpal::StreamConfig {
|
let audio_config = cpal::StreamConfig {
|
||||||
channels: 2,
|
channels: NUM_CHANNELS as u16,
|
||||||
sample_rate: cpal::SampleRate(supported_sample_rates[sample_rate_idx]),
|
sample_rate: cpal::SampleRate(supported_sample_rates[sample_rate_idx]),
|
||||||
buffer_size: AUDIO_FRAME_SIZE,
|
buffer_size: AUDIO_FRAME_SIZE,
|
||||||
};
|
};
|
||||||
|
|
||||||
let fft_thread_ctx = cc.egui_ctx.clone();
|
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_thread_result = last_result.clone();
|
||||||
|
|
||||||
let fft_close_channel = channel::<()>();
|
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],
|
FFT_LENGTHS[DEFAULT_FFT_LENGTH],
|
||||||
supported_sample_rates[sample_rate_idx] as f32,
|
supported_sample_rates[sample_rate_idx] as f64,
|
||||||
2,
|
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 {
|
Self {
|
||||||
egui_ctx: cc.egui_ctx.clone(),
|
egui_ctx: cc.egui_ctx.clone(),
|
||||||
@ -278,7 +356,7 @@ impl MyApp {
|
|||||||
.name("fft".into())
|
.name("fft".into())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
fft_thread_impl(
|
fft_thread_impl(
|
||||||
fft_analyzer,
|
fft_thread_analyzer_ref,
|
||||||
fft_thread_ctx,
|
fft_thread_ctx,
|
||||||
sample_rx,
|
sample_rx,
|
||||||
fft_close_channel.1,
|
fft_close_channel.1,
|
||||||
@ -286,6 +364,7 @@ impl MyApp {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
fft_analyzer,
|
||||||
fft_length: FFT_LENGTHS[DEFAULT_FFT_LENGTH],
|
fft_length: FFT_LENGTHS[DEFAULT_FFT_LENGTH],
|
||||||
fft_progress: Arc::new(AtomicUsize::new(0)),
|
fft_progress: Arc::new(AtomicUsize::new(0)),
|
||||||
fft_close_channel: fft_close_channel.0,
|
fft_close_channel: fft_close_channel.0,
|
||||||
@ -296,6 +375,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,
|
||||||
@ -383,17 +463,30 @@ impl MyApp {
|
|||||||
data_pipe: Receiver<Vec<SampleType>>,
|
data_pipe: Receiver<Vec<SampleType>>,
|
||||||
close_pipe: Receiver<()>,
|
close_pipe: Receiver<()>,
|
||||||
) -> JoinHandle<()> {
|
) -> 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 result_ref = self.last_result.clone();
|
||||||
let ctx_ref = self.egui_ctx.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()
|
thread::Builder::new()
|
||||||
.name("fft".into())
|
.name("fft".into())
|
||||||
.spawn(move || {
|
.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()
|
.unwrap()
|
||||||
}
|
}
|
||||||
@ -424,11 +517,11 @@ fn plot_max(data: &Vec<Vec<[f64; 2]>>) -> f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn fft_thread_impl(
|
fn fft_thread_impl(
|
||||||
mut analyzer: FftAnalyzer,
|
analyzer: Arc<Mutex<FftAnalyzer<SampleType, FloatType, NUM_CHANNELS>>>,
|
||||||
ctx: egui::Context,
|
ctx: egui::Context,
|
||||||
data_pipe: Receiver<Vec<SampleType>>,
|
data_pipe: Receiver<Vec<SampleType>>,
|
||||||
close_pipe: Receiver<()>,
|
close_pipe: Receiver<()>,
|
||||||
last_result: FftResult<f32>,
|
last_result: FftResult<FloatType>,
|
||||||
) {
|
) {
|
||||||
println!("FFT thread initialized");
|
println!("FFT thread initialized");
|
||||||
loop {
|
loop {
|
||||||
@ -437,20 +530,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.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() {
|
||||||
.last_analysis
|
lock.last_analysis
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.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);
|
||||||
@ -463,12 +570,10 @@ fn fft_thread_impl(
|
|||||||
};
|
};
|
||||||
// while holding the plot_lock which will block the gui thread, update the rest
|
// while holding the plot_lock which will block the gui thread, update the rest
|
||||||
*last_result.fft_analyses.lock().unwrap() =
|
*last_result.fft_analyses.lock().unwrap() =
|
||||||
analyzer.last_analysis.lock().unwrap().clone();
|
analyzer.lock().unwrap().last_analysis.clone();
|
||||||
drop(plot_data);
|
drop(plot_data);
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
println!("Didn't receive any data, looping around");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -479,7 +584,6 @@ fn handle_audio_frame(
|
|||||||
fft_progress: Arc<AtomicUsize>,
|
fft_progress: Arc<AtomicUsize>,
|
||||||
) {
|
) {
|
||||||
// Copy and send to processor thread
|
// 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();
|
chan.send(data.to_vec()).unwrap_or_default();
|
||||||
fft_progress.store(
|
fft_progress.store(
|
||||||
fft_progress.load(Ordering::Relaxed) + data.len(),
|
fft_progress.load(Ordering::Relaxed) + data.len(),
|
||||||
@ -488,8 +592,9 @@ fn handle_audio_frame(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
env_logger::init();
|
||||||
let options = eframe::NativeOptions {
|
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()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user