diff --git a/Cargo.lock b/Cargo.lock index cb2d155..4792d37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1366,6 +1366,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.4.2" @@ -1578,6 +1587,15 @@ dependencies = [ "yansi", ] +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + [[package]] name = "proc-macro2" version = "1.0.92" @@ -1882,6 +1900,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustfft" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43806561bc506d0c5d160643ad742e3161049ac01027b5e6d7524091fd401d86" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", + "version_check", +] + [[package]] name = "rustix" version = "0.38.41" @@ -2131,6 +2164,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + [[package]] name = "strsim" version = "0.11.1" @@ -2166,11 +2205,13 @@ dependencies = [ "crossterm", "image", "md5", + "num-complex", "pipewire", "rand", "ratatui", "ratatui-image", "reqwest", + "rustfft", "serde", "serde_json", "toml", @@ -2415,6 +2456,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -2508,6 +2559,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 49bca74..3de5434 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,11 +7,13 @@ edition = "2021" crossterm = "0.28.1" image = "0.25.5" md5 = "0.7.0" +num-complex = "0.4.6" pipewire = "0.8.0" rand = "0.8.5" ratatui = "0.29.0" ratatui-image = "3.0.0" reqwest = { version = "0.12.9", features = ["blocking", "json"] } +rustfft = "6.2.0" serde = { version = "1.0.215", features = ["derive"] } serde_json = "1.0.133" toml = "0.8.19" diff --git a/src/audio/fft.rs b/src/audio/fft.rs index caca333..eb0f890 100644 --- a/src/audio/fft.rs +++ b/src/audio/fft.rs @@ -1,29 +1,78 @@ +use num_complex::{Complex, Complex64}; +use rustfft::{Fft, FftPlanner}; + use crate::utils::FFTResult; -use std::collections::VecDeque; +use std::{collections::VecDeque, sync::Arc}; + +const FFT_WINDOW: usize = 5; +const FFT_FRAME_SIZE: usize = FFT_WINDOW * 480; pub struct FFTFrame { - buffer: VecDeque<[[i16; 480]; 2]>, + buffer: VecDeque<[[Complex; 480]; 2]>, + planner: FftPlanner, + fft_instance: Arc>, + n_iter: usize, } impl FFTFrame { pub fn new() -> FFTFrame { + let mut planner = FftPlanner::new(); + let plan = planner.plan_fft_forward(FFT_FRAME_SIZE); let mut frame = FFTFrame { buffer: VecDeque::new(), + planner, + fft_instance: plan, + n_iter: 0, }; - for _ in 0..5 { - frame.push([[0; 480]; 2]); + for _ in 0..FFT_WINDOW { + frame.push([[Complex { re: 0.0, im: 0.0 }; 480]; 2]); } frame } - pub fn push(&mut self, new_samples: [[i16; 480]; 2]) { - if self.buffer.len() == 5 { + pub fn push(&mut self, new_samples: [[Complex; 480]; 2]) { + if self.buffer.len() == FFT_WINDOW { self.buffer.pop_front(); } self.buffer.push_back(new_samples); + self.n_iter = (self.n_iter + 1) % FFT_WINDOW; } - pub fn compute(&self) -> FFTResult { - unimplemented!() + pub fn compute(&mut self) -> Option { + if self.n_iter != 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, + im: 0.0f64, + }; FFT_FRAME_SIZE], + [Complex { + re: 0.0f64, + im: 0.0f64, + }; 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); + } + 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; + } + Some(FFTResult { bins_l, bins_r }) } } diff --git a/src/audio/sound_mgr.rs b/src/audio/sound_mgr.rs index 24b2fdd..155a0c5 100644 --- a/src/audio/sound_mgr.rs +++ b/src/audio/sound_mgr.rs @@ -8,6 +8,8 @@ use std::{ time::{Duration, SystemTime}, }; +use num_complex::Complex; + use crate::{ config::Settings, player::PlayerEvent, @@ -83,13 +85,16 @@ impl SoundManager { } } - fn fft_compute(&mut self, frame: &[u8]) -> FFTResult { - let mut samples = [[0; 480]; 2]; + fn fft_compute(&mut self, frame: &[u8]) -> Option { + let mut samples = [[Complex { + re: 0.0f64, + im: 0.0f64, + }; 480]; 2]; for i in 0..frame.len() / 4 { for j in 0..2 { let mut val_buff = [0; 2]; val_buff.copy_from_slice(&frame[4 * i + 2 * j..4 * i + 2 * j + 2]); - samples[i][j] = i16::from_le_bytes(val_buff); + samples[j][i].re = i16::from_le_bytes(val_buff) as f64 / 32767.0; } } self.fft_frame.push(samples); @@ -98,8 +103,9 @@ impl SoundManager { fn push_samples(&mut self) -> Result<(), Error> { let (mut frame, fetch_more) = self.decoder_context.next_sample(); - let bins = self.fft_compute(frame.as_slice()); - self.player_chan.send(PlayerEvent::FFTBins(bins))?; + if let Some(bins) = self.fft_compute(frame.as_slice()) { + self.player_chan.send(PlayerEvent::FFTBins(bins))?; + } self.adjust_volume(frame.as_mut_slice()); self.sample_in.send(frame)?; match fetch_more { diff --git a/src/player/current.rs b/src/player/current.rs index f1be98e..966daff 100644 --- a/src/player/current.rs +++ b/src/player/current.rs @@ -1,6 +1,9 @@ use image::DynamicImage; -use crate::{ssonic::response::Song, utils::default_cover}; +use crate::{ + ssonic::response::Song, + utils::{default_cover, FFTResult}, +}; pub struct Metadata { pub id: String, @@ -12,6 +15,7 @@ pub struct Metadata { pub size: u32, pub bytes_received: u32, pub last_byte_range_start: u32, + pub spectrogram: FFTResult, } impl Metadata { @@ -26,6 +30,10 @@ impl Metadata { size: 0, bytes_received: 0, last_byte_range_start: 0, + spectrogram: FFTResult { + bins_l: [0.0; 20], + bins_r: [0.0; 20], + }, } } @@ -49,4 +57,8 @@ impl Metadata { self.bytes_received = 0; self.size = song.size; } + + pub fn update_spectrogram(&mut self, bins: FFTResult) { + self.spectrogram = bins; + } } diff --git a/src/player/player.rs b/src/player/player.rs index 5c9e136..8eb58d0 100644 --- a/src/player/player.rs +++ b/src/player/player.rs @@ -10,7 +10,7 @@ use image::DynamicImage; use crate::{ audio::AudioEvent, ssonic::{response::Song, APIEvent, MAX_CHUNK_SIZE}, - utils::{default_cover, time_rem, Error}, + utils::{default_cover, time_rem, Error, FFTResult}, }; use super::{errors::PlayerError, Player, PlayerEvent}; @@ -53,6 +53,7 @@ impl Player { PlayerEvent::UpdateCover(cover) => self.tui_root.update_cover(cover), PlayerEvent::AddAudioChunk(chunk_len) => self.recv_chunk(chunk_len)?, PlayerEvent::FetchChunk => self.fetch_audio_chunk(false)?, + PlayerEvent::FFTBins(bins) => self.tui_root.metadata.update_spectrogram(bins), _ => unimplemented!(), } } @@ -90,7 +91,6 @@ impl Player { } fn play_next(&mut self) -> Result<(), Error> { - dbg!("playing next"); let song = match self.playlist.get_next() { None => { // no song exists, requeue the event diff --git a/src/utils.rs b/src/utils.rs index b3c6f89..16abcb7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -76,4 +76,7 @@ pub fn time_rem(start_time: SystemTime, target_dur: Duration) -> Result