FFT computation and value updating
This commit is contained in:
parent
1c3dc2abd7
commit
3108c16566
7 changed files with 146 additions and 17 deletions
57
Cargo.lock
generated
57
Cargo.lock
generated
|
@ -1366,6 +1366,15 @@ dependencies = [
|
||||||
"num-traits",
|
"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]]
|
[[package]]
|
||||||
name = "num-derive"
|
name = "num-derive"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
|
@ -1578,6 +1587,15 @@ dependencies = [
|
||||||
"yansi",
|
"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]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.92"
|
version = "1.0.92"
|
||||||
|
@ -1882,6 +1900,21 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
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]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.41"
|
version = "0.38.41"
|
||||||
|
@ -2131,6 +2164,12 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strength_reduce"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
|
@ -2166,11 +2205,13 @@ dependencies = [
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"image",
|
"image",
|
||||||
"md5",
|
"md5",
|
||||||
|
"num-complex",
|
||||||
"pipewire",
|
"pipewire",
|
||||||
"rand",
|
"rand",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"ratatui-image",
|
"ratatui-image",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rustfft",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"toml",
|
"toml",
|
||||||
|
@ -2415,6 +2456,16 @@ dependencies = [
|
||||||
"once_cell",
|
"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]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
|
@ -2508,6 +2559,12 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
|
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|
|
@ -7,11 +7,13 @@ edition = "2021"
|
||||||
crossterm = "0.28.1"
|
crossterm = "0.28.1"
|
||||||
image = "0.25.5"
|
image = "0.25.5"
|
||||||
md5 = "0.7.0"
|
md5 = "0.7.0"
|
||||||
|
num-complex = "0.4.6"
|
||||||
pipewire = "0.8.0"
|
pipewire = "0.8.0"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
ratatui = "0.29.0"
|
ratatui = "0.29.0"
|
||||||
ratatui-image = "3.0.0"
|
ratatui-image = "3.0.0"
|
||||||
reqwest = { version = "0.12.9", features = ["blocking", "json"] }
|
reqwest = { version = "0.12.9", features = ["blocking", "json"] }
|
||||||
|
rustfft = "6.2.0"
|
||||||
serde = { version = "1.0.215", features = ["derive"] }
|
serde = { version = "1.0.215", features = ["derive"] }
|
||||||
serde_json = "1.0.133"
|
serde_json = "1.0.133"
|
||||||
toml = "0.8.19"
|
toml = "0.8.19"
|
||||||
|
|
|
@ -1,29 +1,78 @@
|
||||||
|
use num_complex::{Complex, Complex64};
|
||||||
|
use rustfft::{Fft, FftPlanner};
|
||||||
|
|
||||||
use crate::utils::FFTResult;
|
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 {
|
pub struct FFTFrame {
|
||||||
buffer: VecDeque<[[i16; 480]; 2]>,
|
buffer: VecDeque<[[Complex<f64>; 480]; 2]>,
|
||||||
|
planner: FftPlanner<f64>,
|
||||||
|
fft_instance: Arc<dyn Fft<f64>>,
|
||||||
|
n_iter: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FFTFrame {
|
impl FFTFrame {
|
||||||
pub fn new() -> FFTFrame {
|
pub fn new() -> FFTFrame {
|
||||||
|
let mut planner = FftPlanner::new();
|
||||||
|
let plan = planner.plan_fft_forward(FFT_FRAME_SIZE);
|
||||||
let mut frame = FFTFrame {
|
let mut frame = FFTFrame {
|
||||||
buffer: VecDeque::new(),
|
buffer: VecDeque::new(),
|
||||||
|
planner,
|
||||||
|
fft_instance: plan,
|
||||||
|
n_iter: 0,
|
||||||
};
|
};
|
||||||
for _ in 0..5 {
|
for _ in 0..FFT_WINDOW {
|
||||||
frame.push([[0; 480]; 2]);
|
frame.push([[Complex { re: 0.0, im: 0.0 }; 480]; 2]);
|
||||||
}
|
}
|
||||||
frame
|
frame
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push(&mut self, new_samples: [[i16; 480]; 2]) {
|
pub fn push(&mut self, new_samples: [[Complex<f64>; 480]; 2]) {
|
||||||
if self.buffer.len() == 5 {
|
if self.buffer.len() == FFT_WINDOW {
|
||||||
self.buffer.pop_front();
|
self.buffer.pop_front();
|
||||||
}
|
}
|
||||||
self.buffer.push_back(new_samples);
|
self.buffer.push_back(new_samples);
|
||||||
|
self.n_iter = (self.n_iter + 1) % FFT_WINDOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compute(&self) -> FFTResult {
|
pub fn compute(&mut self) -> Option<FFTResult> {
|
||||||
unimplemented!()
|
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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ use std::{
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use num_complex::Complex;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Settings,
|
config::Settings,
|
||||||
player::PlayerEvent,
|
player::PlayerEvent,
|
||||||
|
@ -83,13 +85,16 @@ impl SoundManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fft_compute(&mut self, frame: &[u8]) -> FFTResult {
|
fn fft_compute(&mut self, frame: &[u8]) -> Option<FFTResult> {
|
||||||
let mut samples = [[0; 480]; 2];
|
let mut samples = [[Complex {
|
||||||
|
re: 0.0f64,
|
||||||
|
im: 0.0f64,
|
||||||
|
}; 480]; 2];
|
||||||
for i in 0..frame.len() / 4 {
|
for i in 0..frame.len() / 4 {
|
||||||
for j in 0..2 {
|
for j in 0..2 {
|
||||||
let mut val_buff = [0; 2];
|
let mut val_buff = [0; 2];
|
||||||
val_buff.copy_from_slice(&frame[4 * i + 2 * j..4 * i + 2 * j + 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);
|
self.fft_frame.push(samples);
|
||||||
|
@ -98,8 +103,9 @@ impl SoundManager {
|
||||||
|
|
||||||
fn push_samples(&mut self) -> Result<(), Error> {
|
fn push_samples(&mut self) -> Result<(), Error> {
|
||||||
let (mut frame, fetch_more) = self.decoder_context.next_sample();
|
let (mut frame, fetch_more) = self.decoder_context.next_sample();
|
||||||
let bins = self.fft_compute(frame.as_slice());
|
if let Some(bins) = self.fft_compute(frame.as_slice()) {
|
||||||
self.player_chan.send(PlayerEvent::FFTBins(bins))?;
|
self.player_chan.send(PlayerEvent::FFTBins(bins))?;
|
||||||
|
}
|
||||||
self.adjust_volume(frame.as_mut_slice());
|
self.adjust_volume(frame.as_mut_slice());
|
||||||
self.sample_in.send(frame)?;
|
self.sample_in.send(frame)?;
|
||||||
match fetch_more {
|
match fetch_more {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
|
|
||||||
use crate::{ssonic::response::Song, utils::default_cover};
|
use crate::{
|
||||||
|
ssonic::response::Song,
|
||||||
|
utils::{default_cover, FFTResult},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct Metadata {
|
pub struct Metadata {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
@ -12,6 +15,7 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Metadata {
|
impl Metadata {
|
||||||
|
@ -26,6 +30,10 @@ impl Metadata {
|
||||||
size: 0,
|
size: 0,
|
||||||
bytes_received: 0,
|
bytes_received: 0,
|
||||||
last_byte_range_start: 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.bytes_received = 0;
|
||||||
self.size = song.size;
|
self.size = song.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_spectrogram(&mut self, bins: FFTResult) {
|
||||||
|
self.spectrogram = bins;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ use image::DynamicImage;
|
||||||
use crate::{
|
use crate::{
|
||||||
audio::AudioEvent,
|
audio::AudioEvent,
|
||||||
ssonic::{response::Song, APIEvent, MAX_CHUNK_SIZE},
|
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};
|
use super::{errors::PlayerError, Player, PlayerEvent};
|
||||||
|
@ -53,6 +53,7 @@ impl Player {
|
||||||
PlayerEvent::UpdateCover(cover) => self.tui_root.update_cover(cover),
|
PlayerEvent::UpdateCover(cover) => self.tui_root.update_cover(cover),
|
||||||
PlayerEvent::AddAudioChunk(chunk_len) => self.recv_chunk(chunk_len)?,
|
PlayerEvent::AddAudioChunk(chunk_len) => self.recv_chunk(chunk_len)?,
|
||||||
PlayerEvent::FetchChunk => self.fetch_audio_chunk(false)?,
|
PlayerEvent::FetchChunk => self.fetch_audio_chunk(false)?,
|
||||||
|
PlayerEvent::FFTBins(bins) => self.tui_root.metadata.update_spectrogram(bins),
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,7 +91,6 @@ impl Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn play_next(&mut self) -> Result<(), Error> {
|
fn play_next(&mut self) -> Result<(), Error> {
|
||||||
dbg!("playing next");
|
|
||||||
let song = match self.playlist.get_next() {
|
let song = match self.playlist.get_next() {
|
||||||
None => {
|
None => {
|
||||||
// no song exists, requeue the event
|
// no song exists, requeue the event
|
||||||
|
|
|
@ -76,4 +76,7 @@ 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 struct FFTResult {
|
||||||
|
pub bins_l: [f64; 20],
|
||||||
|
pub bins_r: [f64; 20],
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue