Initial commit, basic working version
This commit is contained in:
commit
6d105b11d9
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
3307
Cargo.lock
generated
Normal file
3307
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "aa"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
apodize = "1.0.0"
|
||||||
|
cpal = { version = "0.15.2" }
|
||||||
|
eframe = { version = "0.23.0" }
|
||||||
|
egui = { version = "0.23.0", features = ["puffin"] }
|
||||||
|
egui_plot = "0.23.0"
|
||||||
|
num = "0.4.1"
|
||||||
|
realfft = "3.3.0"
|
||||||
|
ringbuf = "0.3.3"
|
||||||
|
rustfft = "6.1.0"
|
236
src/analyzer.rs
Normal file
236
src/analyzer.rs
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
use num::complex::ComplexFloat;
|
||||||
|
use num::integer::Roots;
|
||||||
|
use num::traits::real::Real;
|
||||||
|
use num::{Float, FromPrimitive, Signed};
|
||||||
|
use realfft::{RealFftPlanner, RealToComplex};
|
||||||
|
use ringbuf::{HeapRb, Rb};
|
||||||
|
use rustfft::Fft;
|
||||||
|
use rustfft::{num_complex::Complex};
|
||||||
|
|
||||||
|
use std::iter::{repeat};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fmt::Debug,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::f64::consts::SQRT_2;
|
||||||
|
|
||||||
|
type FloatType = f32;
|
||||||
|
type SampleType = f32;
|
||||||
|
|
||||||
|
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 set_length(&mut self, length: usize);
|
||||||
|
fn set_channels(&mut self, channels: usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AudioBuf<T> {
|
||||||
|
samples: Vec<HeapRb<T>>,
|
||||||
|
read_since_last_update: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait WindowFunction<T: Real> {
|
||||||
|
fn amplitude_correction_factor(&self) -> T;
|
||||||
|
fn energy_correction_factor(&self) -> T;
|
||||||
|
fn apply(&self, buf: &mut [T]);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HanningWindow<T: Real>(Vec<T>);
|
||||||
|
|
||||||
|
impl<T: Real + FromPrimitive> HanningWindow<T> {
|
||||||
|
fn new(fft_length: usize) -> Self {
|
||||||
|
Self(
|
||||||
|
apodize::hanning_iter(fft_length)
|
||||||
|
.map(|x| T::from_f64(x).unwrap())
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Real + FromPrimitive> WindowFunction<T> for HanningWindow<T> {
|
||||||
|
fn amplitude_correction_factor(&self) -> T {
|
||||||
|
T::from_f64(2.0).unwrap()
|
||||||
|
}
|
||||||
|
fn energy_correction_factor(&self) -> T {
|
||||||
|
T::from_f64(2.0 / num::Float::sqrt(3.0 / 2.0)).unwrap()
|
||||||
|
}
|
||||||
|
fn apply(&self, buf: &mut [T]) {
|
||||||
|
for (a, b) in buf.iter_mut().zip(self.0.iter()) {
|
||||||
|
*a = *a * *b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lin_to_db(lin: f64) -> f64 {
|
||||||
|
20.0 * lin.log10()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lin_to_dbfs(lin: f64) -> f64 {
|
||||||
|
20.0 * (lin * SQRT_2).log10()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FftAnalyzer {
|
||||||
|
pub last_analysis: Arc<Mutex<Vec<FftAnalysis<SampleType>>>>,
|
||||||
|
|
||||||
|
fft_length: usize,
|
||||||
|
sample_rate: FloatType,
|
||||||
|
channels: usize,
|
||||||
|
norm_factor: FloatType,
|
||||||
|
bin_freqs: Vec<FloatType>,
|
||||||
|
|
||||||
|
planner: RealFftPlanner<FloatType>,
|
||||||
|
processor: Arc<dyn RealToComplex<FloatType>>,
|
||||||
|
window: HanningWindow<FloatType>,
|
||||||
|
|
||||||
|
audio_buf: Arc<Mutex<AudioBuf<FloatType>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FftAnalyzer {
|
||||||
|
pub fn new(fft_length: usize, sample_rate: FloatType, 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()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Self {
|
||||||
|
last_analysis: Arc::new(Mutex::new(Vec::with_capacity(channels))),
|
||||||
|
fft_length,
|
||||||
|
sample_rate,
|
||||||
|
channels,
|
||||||
|
norm_factor: FloatType::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::<SampleType> {
|
||||||
|
samples: repeat(0)
|
||||||
|
.take(channels)
|
||||||
|
.map(|_| HeapRb::new(fft_length))
|
||||||
|
.collect(),
|
||||||
|
read_since_last_update: 0,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_power = (rms_accumulator / buf.len() as f32).sqrt();
|
||||||
|
|
||||||
|
(local_min, local_max, total_power)
|
||||||
|
}
|
||||||
|
fn process_frame(&mut self) {
|
||||||
|
let _channels_float = FloatType::from(self.channels as f32);
|
||||||
|
let mut result = Vec::with_capacity(self.channels);
|
||||||
|
|
||||||
|
for chan in 0..self.channels {
|
||||||
|
let mut in_buf: Vec<FloatType> = self.audio_buf.lock().unwrap().samples[chan]
|
||||||
|
.iter()
|
||||||
|
.map(|x| *x)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let td_result = self.time_domain_process(&in_buf);
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
// do fft
|
||||||
|
self.processor
|
||||||
|
.process(&mut in_buf, out_buf.as_mut_slice())
|
||||||
|
.expect("fft failed?");
|
||||||
|
|
||||||
|
let analysis_buf = 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();
|
||||||
|
(
|
||||||
|
self.bin_freqs[bin],
|
||||||
|
raw_mag * self.window.amplitude_correction_factor(),
|
||||||
|
raw_mag * self.window.energy_correction_factor(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut lock = self.last_analysis.lock().unwrap();
|
||||||
|
*lock = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Analyzer for FftAnalyzer {
|
||||||
|
fn process_data(&mut self, data: &[SampleType]) -> bool {
|
||||||
|
let _channels_float = FloatType::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.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 {
|
||||||
|
buf.read_since_last_update = 0;
|
||||||
|
drop(buf);
|
||||||
|
self.process_frame();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn set_channels(&mut self, channels: usize) {
|
||||||
|
self.channels = channels;
|
||||||
|
}
|
||||||
|
fn set_length(&mut self, _length: usize) {
|
||||||
|
unimplemented!(); // Need to rebuild the window and plan
|
||||||
|
}
|
||||||
|
fn set_samplerate(&mut self, rate: FloatType) {
|
||||||
|
self.sample_rate = rate;
|
||||||
|
}
|
||||||
|
}
|
432
src/main.rs
Normal file
432
src/main.rs
Normal file
@ -0,0 +1,432 @@
|
|||||||
|
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||||
|
use cpal::SampleFormat;
|
||||||
|
|
||||||
|
use egui::epaint::Hsva;
|
||||||
|
use egui::Color32;
|
||||||
|
use egui_plot::{Legend, Line, Plot};
|
||||||
|
|
||||||
|
use std::mem::swap;
|
||||||
|
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};
|
||||||
|
|
||||||
|
const SAMPLE_RATES: &[u32] = &[
|
||||||
|
11025, 22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000,
|
||||||
|
];
|
||||||
|
const CHANNEL_NAMES: &[&str] = &["Left", "Right"];
|
||||||
|
const CHANNEL_COLOURS: &[Hsva] = &[
|
||||||
|
Hsva {
|
||||||
|
h: 0.618,
|
||||||
|
s: 0.85,
|
||||||
|
v: 0.5,
|
||||||
|
a: 0.75,
|
||||||
|
},
|
||||||
|
Hsva {
|
||||||
|
h: 0.0,
|
||||||
|
s: 0.85,
|
||||||
|
v: 0.5,
|
||||||
|
a: 0.75,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const DEFAULT_SAMPLE_RATE: u32 = 48000;
|
||||||
|
|
||||||
|
const AUDIO_FRAME_SIZE: cpal::BufferSize = cpal::BufferSize::Fixed(512);
|
||||||
|
|
||||||
|
type SampleType = f32;
|
||||||
|
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_min: f64,
|
||||||
|
plot_max: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
struct FftResult<T> {
|
||||||
|
fft_analyses: Arc<Mutex<Vec<FftAnalysis<T>>>>,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MyApp {
|
||||||
|
egui_ctx: egui::Context,
|
||||||
|
|
||||||
|
fft_thread: JoinHandle<()>,
|
||||||
|
fft_close_channel: Sender<()>,
|
||||||
|
fft_length: usize,
|
||||||
|
|
||||||
|
audio_device: cpal::Device,
|
||||||
|
audio_config: cpal::StreamConfig,
|
||||||
|
|
||||||
|
audio_devices: Vec<String>,
|
||||||
|
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>,
|
||||||
|
|
||||||
|
ui_selections: MyAppUiSelections,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for MyApp {
|
||||||
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
egui::SidePanel::left("left_panel").show(ctx, |ui| {
|
||||||
|
egui::ComboBox::from_label("Source")
|
||||||
|
.selected_text(self.audio_devices[self.ui_selections.audio_device].clone())
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
self.audio_devices
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.for_each(|(index, dev)| {
|
||||||
|
ui.selectable_value(&mut self.ui_selections.audio_device, index, dev);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
self.sample_rate_box(ui);
|
||||||
|
self.fft_length_box(ui);
|
||||||
|
});
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
ui_plot(ui, self.last_result.clone());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MyApp {
|
||||||
|
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());
|
||||||
|
|
||||||
|
sample_rate_box.show_ui(ui, |ui| {
|
||||||
|
self.sample_rates
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.for_each(|(index, rate)| {
|
||||||
|
ui.selectable_value(
|
||||||
|
&mut self.ui_selections.sample_rate,
|
||||||
|
index,
|
||||||
|
rate.to_string(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if self.sample_rates[self.ui_selections.sample_rate] != self.audio_config.sample_rate.0 {
|
||||||
|
self.set_sample_rate(self.sample_rates[self.ui_selections.sample_rate]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn fft_length_box(&mut self, ui: &mut egui::Ui) {
|
||||||
|
let fft_length_box = egui::ComboBox::from_label("FFT Length")
|
||||||
|
.selected_text(FFT_LENGTHS[self.ui_selections.fft_length].to_string());
|
||||||
|
|
||||||
|
fft_length_box.show_ui(ui, |ui| {
|
||||||
|
for (index, length) in FFT_LENGTHS.iter().enumerate() {
|
||||||
|
ui.selectable_value(
|
||||||
|
&mut self.ui_selections.fft_length,
|
||||||
|
index,
|
||||||
|
length.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if FFT_LENGTHS[self.ui_selections.fft_length] != self.fft_length {
|
||||||
|
self.set_fft_length(FFT_LENGTHS[self.ui_selections.fft_length]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn new(cc: &eframe::CreationContext) -> Self {
|
||||||
|
let host = cpal::default_host();
|
||||||
|
println!("Audio host: {}", host.id().name());
|
||||||
|
|
||||||
|
let (sample_tx, sample_rx) = channel::<Vec<SampleType>>();
|
||||||
|
|
||||||
|
let device_names: Vec<_> = host
|
||||||
|
.input_devices()
|
||||||
|
.expect("No available input devices")
|
||||||
|
.map(|x| x.name().unwrap())
|
||||||
|
.collect();
|
||||||
|
let default_device = host.default_input_device().unwrap();
|
||||||
|
let mut supported_configs = default_device.supported_input_configs().unwrap();
|
||||||
|
let supported_sample_rates: Vec<_> = SAMPLE_RATES
|
||||||
|
.iter()
|
||||||
|
.filter(|x| {
|
||||||
|
supported_configs
|
||||||
|
.any(|y| **x <= y.max_sample_rate().0 && **x >= y.min_sample_rate().0)
|
||||||
|
})
|
||||||
|
.map(|x| *x)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let sample_rate_idx = supported_sample_rates
|
||||||
|
.iter()
|
||||||
|
.position(|x| *x == DEFAULT_SAMPLE_RATE)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let audio_config = cpal::StreamConfig {
|
||||||
|
channels: 2,
|
||||||
|
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 fft_thread_result = last_result.clone();
|
||||||
|
|
||||||
|
let fft_close_channel = channel::<()>();
|
||||||
|
|
||||||
|
let fft_analyzer = FftAnalyzer::new(
|
||||||
|
FFT_LENGTHS[DEFAULT_FFT_LENGTH],
|
||||||
|
supported_sample_rates[sample_rate_idx] as f32,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
egui_ctx: cc.egui_ctx.clone(),
|
||||||
|
fft_thread: thread::Builder::new()
|
||||||
|
.name("fft".into())
|
||||||
|
.spawn(move || {
|
||||||
|
fft_thread_impl(
|
||||||
|
fft_analyzer,
|
||||||
|
fft_thread_ctx,
|
||||||
|
sample_rx,
|
||||||
|
fft_close_channel.1,
|
||||||
|
fft_thread_result,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
fft_length: FFT_LENGTHS[DEFAULT_FFT_LENGTH],
|
||||||
|
fft_close_channel: fft_close_channel.0,
|
||||||
|
ui_selections: MyAppUiSelections {
|
||||||
|
audio_device: device_names
|
||||||
|
.iter()
|
||||||
|
.position(|x| *x == default_device.name().unwrap())
|
||||||
|
.unwrap_or(0),
|
||||||
|
sample_rate: sample_rate_idx,
|
||||||
|
fft_length: DEFAULT_FFT_LENGTH,
|
||||||
|
},
|
||||||
|
sample_rates: supported_sample_rates,
|
||||||
|
audio_device: default_device,
|
||||||
|
audio_stream: None,
|
||||||
|
audio_devices: device_names,
|
||||||
|
last_result,
|
||||||
|
sample_channel: sample_tx,
|
||||||
|
audio_config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn run(&mut self) {
|
||||||
|
self.audio_thread();
|
||||||
|
}
|
||||||
|
fn restart_threads(&mut self) {
|
||||||
|
let sample_channel = channel::<Vec<SampleType>>();
|
||||||
|
let close_channel = channel::<()>();
|
||||||
|
|
||||||
|
// Stop the audio first so it doesn't try to send to a closed channel
|
||||||
|
self.audio_stream = None;
|
||||||
|
|
||||||
|
// Stop fft thread, rebuild it, swap with the old and join on the old thread
|
||||||
|
self.fft_close_channel.send(()).unwrap();
|
||||||
|
let mut new_thread = self.fft_thread(sample_channel.1, close_channel.1);
|
||||||
|
swap(&mut self.fft_thread, &mut new_thread);
|
||||||
|
new_thread.join().unwrap();
|
||||||
|
|
||||||
|
// Replace the channels in self
|
||||||
|
self.fft_close_channel = close_channel.0;
|
||||||
|
self.sample_channel = sample_channel.0;
|
||||||
|
|
||||||
|
// Restart the audio stream
|
||||||
|
self.audio_thread();
|
||||||
|
}
|
||||||
|
fn set_sample_rate(&mut self, rate: u32) {
|
||||||
|
match self.sample_rates.iter().position(|x| *x == rate) {
|
||||||
|
Some(rate_idx) => {
|
||||||
|
self.ui_selections.sample_rate = rate_idx;
|
||||||
|
self.audio_config.sample_rate = cpal::SampleRate(SAMPLE_RATES[rate_idx]);
|
||||||
|
self.restart_threads();
|
||||||
|
}
|
||||||
|
None => unimplemented!("Make some graceful error handler"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn set_fft_length(&mut self, length: usize) {
|
||||||
|
match FFT_LENGTHS.iter().position(|x| *x == length) {
|
||||||
|
Some(length_idx) => {
|
||||||
|
self.fft_length = length;
|
||||||
|
self.ui_selections.fft_length = length_idx;
|
||||||
|
self.restart_threads();
|
||||||
|
}
|
||||||
|
None => unimplemented!("Make some graceful error handler"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn audio_thread(&mut self) {
|
||||||
|
if self.audio_stream.is_some() {
|
||||||
|
self.audio_stream = None;
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"Starting audio thread on device `{}` with config `{:?}`",
|
||||||
|
self.audio_device.name().unwrap(),
|
||||||
|
self.audio_config
|
||||||
|
);
|
||||||
|
|
||||||
|
let new_channel = self.sample_channel.clone();
|
||||||
|
|
||||||
|
self.audio_stream = Some(
|
||||||
|
self.audio_device
|
||||||
|
.build_input_stream(
|
||||||
|
&self.audio_config,
|
||||||
|
move |data, _| handle_audio_frame(data, new_channel.clone()),
|
||||||
|
move |err| panic!("{:?}", err),
|
||||||
|
Some(std::time::Duration::from_secs(1)),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
println!("Stream built");
|
||||||
|
self.audio_stream.as_ref().unwrap().play().unwrap(); // this doesn't block
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fft_thread(
|
||||||
|
&mut self,
|
||||||
|
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();
|
||||||
|
thread::Builder::new()
|
||||||
|
.name("fft".into())
|
||||||
|
.spawn(move || {
|
||||||
|
fft_thread_impl(fft_analyzer, ctx_ref, data_pipe, close_pipe, result_ref)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plot_min(data: &Vec<Vec<[f64; 2]>>) -> f64 {
|
||||||
|
let mut min = f64::INFINITY;
|
||||||
|
for channel in data {
|
||||||
|
for point in channel {
|
||||||
|
if point[1] < min && point[1] > f64::NEG_INFINITY {
|
||||||
|
min = point[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
min.clamp(-160.0, 160.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plot_max(data: &Vec<Vec<[f64; 2]>>) -> f64 {
|
||||||
|
let mut max = f64::NEG_INFINITY;
|
||||||
|
for channel in data {
|
||||||
|
for point in channel {
|
||||||
|
if point[1] > max && point[1] < f64::INFINITY {
|
||||||
|
max = point[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
max.clamp(-160.0, 160.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fft_thread_impl(
|
||||||
|
mut analyzer: FftAnalyzer,
|
||||||
|
ctx: egui::Context,
|
||||||
|
data_pipe: Receiver<Vec<SampleType>>,
|
||||||
|
close_pipe: Receiver<()>,
|
||||||
|
last_result: FftResult<f32>,
|
||||||
|
) {
|
||||||
|
println!("FFT thread initialized");
|
||||||
|
loop {
|
||||||
|
if close_pipe.try_recv().is_ok() {
|
||||||
|
println!("FFT thread ended");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Ok(buf) = data_pipe.recv_timeout(std::time::Duration::from_secs_f64(0.1)) {
|
||||||
|
if analyzer.process_data(&buf) {
|
||||||
|
// Prepare the data for plotting
|
||||||
|
let charts: Vec<Vec<[f64; 2]>> =
|
||||||
|
analyzer
|
||||||
|
.last_analysis
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|analysis| {
|
||||||
|
Vec::from_iter(analysis.fft.iter().map(|(freq, amp, _energy)| {
|
||||||
|
[(*freq as f64), lin_to_db(*amp as f64)]
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let plot_min = plot_min(&charts);
|
||||||
|
let plot_max = plot_max(&charts);
|
||||||
|
|
||||||
|
let mut plot_data = last_result.plot_data.lock().unwrap();
|
||||||
|
*plot_data = FftPlotData {
|
||||||
|
plot_points: charts,
|
||||||
|
plot_min,
|
||||||
|
plot_max,
|
||||||
|
};
|
||||||
|
// 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();
|
||||||
|
drop(plot_data);
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Didn't receive any data, looping around");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_audio_frame(data: &[SampleType], chan: Sender<Vec<SampleType>>) {
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let options = eframe::NativeOptions {
|
||||||
|
initial_window_size: Some(egui::vec2(640.0, 480.0)),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("App initialized");
|
||||||
|
|
||||||
|
eframe::run_native(
|
||||||
|
"rust-aa",
|
||||||
|
options,
|
||||||
|
Box::new(|cc| {
|
||||||
|
let mut app = Box::new(MyApp::new(cc));
|
||||||
|
app.run();
|
||||||
|
app
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user