Add an audio visualizer

This commit is contained in:
Muaz Ahmad 2024-12-11 16:18:17 +05:00
parent 3108c16566
commit 9f53765c3b
5 changed files with 67 additions and 37 deletions

View file

@ -32,20 +32,16 @@ impl FFTFrame {
pub fn push(&mut self, new_samples: [[Complex<f64>; 480]; 2]) { pub fn push(&mut self, new_samples: [[Complex<f64>; 480]; 2]) {
if self.buffer.len() == FFT_WINDOW { 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; self.n_iter = (self.n_iter + 1) % FFT_WINDOW;
} }
pub fn compute(&mut self) -> Option<FFTResult> { pub fn compute(&mut self) -> Option<FFTResult> {
if self.n_iter != 0 { if self.n_iter % 2 != 0 {
return None; return None;
} }
let mut FFTResult = FFTResult {
bins_l: [0.0; 20],
bins_r: [0.0; 20],
};
let (mut buff_l, mut buff_r) = ( let (mut buff_l, mut buff_r) = (
[Complex { [Complex {
re: 0.0f64, re: 0.0f64,
@ -57,22 +53,43 @@ impl FFTFrame {
}; FFT_FRAME_SIZE], }; FFT_FRAME_SIZE],
); );
for i in 0..FFT_WINDOW { for i in 0..FFT_WINDOW {
let [l, r] = self.buffer.pop_front().unwrap(); let [l, r] = &self.buffer[i];
buff_l[i..i + 480].copy_from_slice(&l); buff_l[i..i + 480].copy_from_slice(l);
buff_r[i..i + 480].copy_from_slice(&r); 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_l);
self.fft_instance.process(&mut buff_r); self.fft_instance.process(&mut buff_r);
let (mut bins_l, mut bins_r) = ([0.0; 20], [0.0; 20]); let mut result = [0.0; 20];
for i in 0..20 { let mut bin_start = 0;
for j in 0..120 { for bin_i in 0..20 {
bins_l[i] += buff_l[120 * i + j].re; let bin_end =
bins_r[i] += buff_r[120 * i + j].re; (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;
} }
bins_l[i] /= 20.0; Some(result)
bins_r[i] /= 20.0; }
fn hamming(&self, buff: &mut [Complex<f64>]) {
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 })
} }
} }

View file

@ -15,11 +15,16 @@ pub struct Metadata {
pub size: u32, pub size: u32,
pub bytes_received: u32, pub bytes_received: u32,
pub last_byte_range_start: u32, pub last_byte_range_start: u32,
pub spectrogram: FFTResult, pub spectrogram: Vec<(f64, f64)>,
} }
impl Metadata { impl Metadata {
pub fn new() -> 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 { Metadata {
id: String::new(), id: String::new(),
name: String::new(), name: String::new(),
@ -30,10 +35,7 @@ impl Metadata {
size: 0, size: 0,
bytes_received: 0, bytes_received: 0,
last_byte_range_start: 0, last_byte_range_start: 0,
spectrogram: FFTResult { spectrogram,
bins_l: [0.0; 20],
bins_r: [0.0; 20],
},
} }
} }
@ -59,6 +61,8 @@ impl Metadata {
} }
pub fn update_spectrogram(&mut self, bins: FFTResult) { 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];
}
} }
} }

View file

@ -34,7 +34,7 @@ impl Player {
self.handle_events()?; self.handle_events()?;
self.handle_inputs()?; self.handle_inputs()?;
self.update()?; 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); thread::sleep(time_left_cycle);
} }
} }

View file

@ -5,7 +5,7 @@ use layout::Flex;
use ratatui::{ use ratatui::{
layout::{Constraint, Layout}, layout::{Constraint, Layout},
prelude::*, prelude::*,
widgets::{Paragraph, Widget, Wrap}, widgets::{Axis, BarChart, Block, Chart, Dataset, Gauge, Paragraph, Widget, Wrap},
}; };
use ratatui_image::{picker::Picker, protocol::StatefulProtocol, StatefulImage}; use ratatui_image::{picker::Picker, protocol::StatefulProtocol, StatefulImage};
@ -25,14 +25,15 @@ impl Widget for &mut Root {
Self: Sized, Self: Sized,
{ {
let layout_main = Layout::vertical([ let layout_main = Layout::vertical([
Constraint::Ratio(1, 5), Constraint::Ratio(1, 6),
Constraint::Ratio(3, 5), Constraint::Ratio(2, 6),
Constraint::Ratio(1, 5), Constraint::Ratio(2, 6),
Constraint::Ratio(1, 6),
]) ])
.flex(Flex::Center); .flex(Flex::Center);
let [_, info, progress] = layout_main.areas(area); let [_, info, spec, _] = layout_main.areas(area);
self.render_info(info, buf); 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); 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) { fn render_cover(&mut self, area: Rect, buf: &mut Buffer) {
StatefulImage::new(None).render(area, buf, &mut self.image_state); StatefulImage::new(None).render(area, buf, &mut self.image_state);
} }
fn render_track_info(&self, area: Rect, buf: &mut Buffer) { fn render_track_info(&self, area: Rect, buf: &mut Buffer) {
let layout_track = Layout::vertical([ let layout_track = Layout::vertical([
Constraint::Max(2), Constraint::Max(2),

View file

@ -76,7 +76,4 @@ pub fn time_rem(start_time: SystemTime, target_dur: Duration) -> Result<Duration
Ok(target_dur.saturating_sub(time_since)) Ok(target_dur.saturating_sub(time_since))
} }
pub struct FFTResult { pub type FFTResult = [f64; 20];
pub bins_l: [f64; 20],
pub bins_r: [f64; 20],
}