From 9f53765c3b49989ce19999ba49c16fa0e85809c2 Mon Sep 17 00:00:00 2001 From: Muaz Ahmad Date: Wed, 11 Dec 2024 16:18:17 +0500 Subject: [PATCH] Add an audio visualizer --- src/audio/fft.rs | 55 ++++++++++++++++++++++++++++--------------- src/player/current.rs | 16 ++++++++----- src/player/player.rs | 2 +- src/player/tui.rs | 26 ++++++++++++++------ src/utils.rs | 5 +--- 5 files changed, 67 insertions(+), 37 deletions(-) diff --git a/src/audio/fft.rs b/src/audio/fft.rs index eb0f890..dbcb02f 100644 --- a/src/audio/fft.rs +++ b/src/audio/fft.rs @@ -32,20 +32,16 @@ impl FFTFrame { pub fn push(&mut self, new_samples: [[Complex; 480]; 2]) { if self.buffer.len() == FFT_WINDOW { - self.buffer.pop_front(); + self.buffer.pop_back(); } - self.buffer.push_back(new_samples); + self.buffer.push_front(new_samples); self.n_iter = (self.n_iter + 1) % FFT_WINDOW; } pub fn compute(&mut self) -> Option { - if self.n_iter != 0 { + if self.n_iter % 2 != 0 { return None; } - let mut FFTResult = FFTResult { - bins_l: [0.0; 20], - bins_r: [0.0; 20], - }; let (mut buff_l, mut buff_r) = ( [Complex { re: 0.0f64, @@ -57,22 +53,43 @@ impl FFTFrame { }; FFT_FRAME_SIZE], ); for i in 0..FFT_WINDOW { - let [l, r] = self.buffer.pop_front().unwrap(); - buff_l[i..i + 480].copy_from_slice(&l); - buff_r[i..i + 480].copy_from_slice(&r); + let [l, r] = &self.buffer[i]; + buff_l[i..i + 480].copy_from_slice(l); + buff_r[i..i + 480].copy_from_slice(r); } + self.hamming(&mut buff_l); + self.hamming(&mut buff_r); self.fft_instance.process(&mut buff_l); self.fft_instance.process(&mut buff_r); - let (mut bins_l, mut bins_r) = ([0.0; 20], [0.0; 20]); - for i in 0..20 { - for j in 0..120 { - bins_l[i] += buff_l[120 * i + j].re; - bins_r[i] += buff_r[120 * i + j].re; - } - bins_l[i] /= 20.0; - bins_r[i] /= 20.0; + let mut result = [0.0; 20]; + let mut bin_start = 0; + for bin_i in 0..20 { + let bin_end = + (f64::powi((bin_i + 1) as f64 / 20.0, 2) * FFT_FRAME_SIZE as f64) as usize; + let max_l = buff_l[bin_start..std::cmp::min(bin_end, FFT_FRAME_SIZE)] + .iter() + .map(|x| x.norm_sqr() / FFT_FRAME_SIZE as f64) + .max_by(|a, b| a.total_cmp(b)) + .unwrap(); + let max_r = buff_r[bin_start..std::cmp::min(bin_end, FFT_FRAME_SIZE)] + .iter() + .map(|x| x.norm_sqr() / FFT_FRAME_SIZE as f64) + .max_by(|a, b| a.total_cmp(b)) + .unwrap(); + result[bin_i] = + (10.0 * f64::log10(f64::max(max_l, max_r))).clamp(-70.0, 0.0) / 70.0 + 1.0; + bin_start = bin_end; + } + Some(result) + } + + fn hamming(&self, buff: &mut [Complex]) { + for i in 0..buff.len() { + let mult = 25.0 / 46.0 + - 21.0 / 46.0 + * f64::cos(2.0 * std::f64::consts::PI * i as f64 / FFT_FRAME_SIZE as f64); + buff[i].re *= mult; } - Some(FFTResult { bins_l, bins_r }) } } diff --git a/src/player/current.rs b/src/player/current.rs index 966daff..6db0566 100644 --- a/src/player/current.rs +++ b/src/player/current.rs @@ -15,11 +15,16 @@ pub struct Metadata { pub size: u32, pub bytes_received: u32, pub last_byte_range_start: u32, - pub spectrogram: FFTResult, + pub spectrogram: Vec<(f64, f64)>, } impl Metadata { pub fn new() -> Metadata { + let mut spectrogram = vec![(0.0, 0.0); 20]; + spectrogram + .iter_mut() + .enumerate() + .for_each(|(i, x)| x.0 = i as f64); Metadata { id: String::new(), name: String::new(), @@ -30,10 +35,7 @@ impl Metadata { size: 0, bytes_received: 0, last_byte_range_start: 0, - spectrogram: FFTResult { - bins_l: [0.0; 20], - bins_r: [0.0; 20], - }, + spectrogram, } } @@ -59,6 +61,8 @@ impl Metadata { } pub fn update_spectrogram(&mut self, bins: FFTResult) { - self.spectrogram = bins; + for i in 0..20 { + self.spectrogram[i].1 = self.spectrogram[i].1 * 0.8 + 0.2 * bins[i]; + } } } diff --git a/src/player/player.rs b/src/player/player.rs index 8eb58d0..e8644f6 100644 --- a/src/player/player.rs +++ b/src/player/player.rs @@ -34,7 +34,7 @@ impl Player { self.handle_events()?; self.handle_inputs()?; self.update()?; - let time_left_cycle = time_rem(st, Duration::from_millis(49))?; + let time_left_cycle = time_rem(st, Duration::from_millis(19))?; thread::sleep(time_left_cycle); } } diff --git a/src/player/tui.rs b/src/player/tui.rs index f7e42e6..2120417 100644 --- a/src/player/tui.rs +++ b/src/player/tui.rs @@ -5,7 +5,7 @@ use layout::Flex; use ratatui::{ layout::{Constraint, Layout}, prelude::*, - widgets::{Paragraph, Widget, Wrap}, + widgets::{Axis, BarChart, Block, Chart, Dataset, Gauge, Paragraph, Widget, Wrap}, }; use ratatui_image::{picker::Picker, protocol::StatefulProtocol, StatefulImage}; @@ -25,14 +25,15 @@ impl Widget for &mut Root { Self: Sized, { let layout_main = Layout::vertical([ - Constraint::Ratio(1, 5), - Constraint::Ratio(3, 5), - Constraint::Ratio(1, 5), + Constraint::Ratio(1, 6), + Constraint::Ratio(2, 6), + Constraint::Ratio(2, 6), + Constraint::Ratio(1, 6), ]) .flex(Flex::Center); - let [_, info, progress] = layout_main.areas(area); + let [_, info, spec, _] = layout_main.areas(area); self.render_info(info, buf); - self.render_progress(progress, buf); + self.render_spectrogram(spec, buf); } } @@ -55,10 +56,21 @@ impl Root { self.render_track_info(track_info, buf); } - fn render_progress(&self, area: Rect, buf: &mut Buffer) {} + fn render_spectrogram(&self, area: Rect, buf: &mut Buffer) { + eprintln!("{:?}", self.metadata.spectrogram); + Chart::new(vec![Dataset::default() + .graph_type(ratatui::widgets::GraphType::Bar) + .data(&self.metadata.spectrogram)]) + .style(Style::default().fg(Color::Gray)) + .x_axis(Axis::default().bounds([0.0, 19.0])) + .y_axis(Axis::default().bounds([0.0, 1.0])) + .render(area, buf); + } + fn render_cover(&mut self, area: Rect, buf: &mut Buffer) { StatefulImage::new(None).render(area, buf, &mut self.image_state); } + fn render_track_info(&self, area: Rect, buf: &mut Buffer) { let layout_track = Layout::vertical([ Constraint::Max(2), diff --git a/src/utils.rs b/src/utils.rs index 16abcb7..4ecbd2c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -76,7 +76,4 @@ pub fn time_rem(start_time: SystemTime, target_dur: Duration) -> Result