use super::*;
const SPECTRUM_ANALYSIS_BANDS: usize = 40;
const SPECTRUM_NOISE_FLOOR: f32 = 0.0008;
const SPECTRUM_OUTPUT_GAIN: f32 = 1.70;
const SPECTRUM_CONNECTING_PEAK_MIN: f32 = 0.03;
const SPECTRUM_CONNECTING_PEAK_MAX: f32 = 0.30;
const SPECTRUM_TREBLE_START: f32 = 0.82;
const SPECTRUM_TREBLE_SOFT_KNEE_SLOPE: f32 = 0.15;
const SPECTRUM_TREBLE_VARIANCE_EXPANSION: f32 = 0.22;
const SPECTRUM_TREBLE_VARIANCE_SENSITIVITY: f32 = 8.0;
impl App {
pub fn update_visualizer(&mut self) {
if self.playback == PlaybackState::Connecting && self.visualizer_mode == 0 {
update_connecting_spectrum_peaks(&mut self.visualizer_peaks, self.tick_count);
return;
}
if !playback_drives_sample_visualizer(&self.playback) {
for peak in &mut self.visualizer_peaks {
*peak = (*peak * 0.82).max(0.0);
}
return;
}
let mut samples = Vec::new();
if let Ok(buf) = self.sample_buffer.lock() {
let n = buf.len();
let window_size = 512;
if n >= window_size {
let start_idx = n - window_size;
samples.extend(buf.iter().skip(start_idx).take(window_size).copied());
} else {
samples.extend(buf.iter().copied());
while samples.len() < window_size {
samples.push(0.0);
}
}
}
if samples.is_empty() {
return;
}
let n = samples.len();
let mut windowed = vec![0.0; n];
for i in 0..n {
let w = 0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / (n - 1) as f32).cos());
windowed[i] = samples[i] * w;
}
let fft_input: Vec<Complex> = windowed.into_iter().map(Complex::from_real).collect();
let mut fft_output = vec![Complex::zero(); n];
fft_rec(&fft_input, &mut fft_output);
let bins_count = n / 2;
if self.visualizer_peaks.len() != SPECTRUM_ANALYSIS_BANDS {
self.visualizer_peaks = vec![0.0; SPECTRUM_ANALYSIS_BANDS];
}
let mut targets = Vec::with_capacity(SPECTRUM_ANALYSIS_BANDS);
for band in 0..SPECTRUM_ANALYSIS_BANDS {
let avg =
average_log_band_energy(&fft_output, band, SPECTRUM_ANALYSIS_BANDS, bins_count, n);
let target = spectrum_target(avg, band, SPECTRUM_ANALYSIS_BANDS);
targets.push(target);
}
let targets = smooth_spectrum_targets(&targets);
let treble_variance =
treble_delta_variance(&targets, &self.visualizer_peaks, SPECTRUM_ANALYSIS_BANDS);
for (band, target) in targets.iter().copied().enumerate() {
let target =
preserve_treble_variance(target, band, SPECTRUM_ANALYSIS_BANDS, treble_variance);
let current = self.visualizer_peaks[band];
if target > current {
self.visualizer_peaks[band] = target; } else {
let release = spectrum_release_curve(band, SPECTRUM_ANALYSIS_BANDS);
self.visualizer_peaks[band] = (current - release).max(target).max(0.0);
}
}
}
}
fn playback_drives_sample_visualizer(playback: &PlaybackState) -> bool {
matches!(
playback,
PlaybackState::Playing | PlaybackState::FadingOut { .. }
)
}
fn update_connecting_spectrum_peaks(peaks: &mut Vec<f32>, tick_count: u64) {
if peaks.len() != SPECTRUM_ANALYSIS_BANDS {
peaks.resize(SPECTRUM_ANALYSIS_BANDS, 0.0);
}
for (band, peak) in peaks.iter_mut().enumerate() {
*peak = connecting_spectrum_peak(band, tick_count, SPECTRUM_ANALYSIS_BANDS);
}
}
fn connecting_spectrum_peak(band: usize, tick_count: u64, total_bands: usize) -> f32 {
let denominator = total_bands.saturating_sub(1).max(1) as f32;
let band_t = band as f32 / denominator;
let t = tick_count as f32;
let sweep = ((band_t * 10.0 - t * 0.055).sin() + 1.0) * 0.5;
let shimmer = ((band_t * 27.0 + t * 0.037).sin() + 1.0) * 0.5;
let breathing = ((t * 0.045).sin() + 1.0) * 0.5;
let low_bias = (1.0 - band_t).powf(0.45) * 0.08;
let peak = low_bias + sweep.powf(2.0) * 0.15 + shimmer * 0.035 + breathing * 0.025;
peak.clamp(SPECTRUM_CONNECTING_PEAK_MIN, SPECTRUM_CONNECTING_PEAK_MAX)
}
fn average_log_band_energy(
fft_output: &[Complex],
band: usize,
total_bands: usize,
bins_count: usize,
sample_count: usize,
) -> f32 {
let t = band as f32 / total_bands as f32;
let min_bin = 1.0_f32;
let max_bin = bins_count as f32;
let bin_start_f = min_bin * (max_bin / min_bin).powf(t);
let bin_end_f = min_bin * (max_bin / min_bin).powf((band + 1) as f32 / total_bands as f32);
let start = (bin_start_f.floor() as usize).clamp(0, bins_count - 1);
let end = (bin_end_f.ceil() as usize).clamp(start + 1, bins_count);
let mut sum = 0.0;
let mut count = 0;
for bin in fft_output.iter().take(end).skip(start) {
sum += bin.norm() / sample_count as f32;
count += 1;
}
if count > 0 {
sum / count as f32
} else {
0.0
}
}
fn spectrum_target(avg: f32, band: usize, total_bands: usize) -> f32 {
let energy = (avg - SPECTRUM_NOISE_FLOOR).max(0.0);
let compressed = compress_spectrum_energy(energy);
let scaled = compressed * spectrum_gain_curve(band, total_bands) * SPECTRUM_OUTPUT_GAIN;
scaled.clamp(0.0, 0.92)
}
fn compress_spectrum_energy(avg: f32) -> f32 {
avg.powf(0.50)
}
fn spectrum_gain_curve(band: usize, total_bands: usize) -> f32 {
let t = band_position(band, total_bands);
spectrum_broad_lift(t) * high_treble_soft_knee(t)
}
fn spectrum_broad_lift(t: f32) -> f32 {
1.0 + 1.20 * t.powf(0.72)
}
fn high_treble_soft_knee(t: f32) -> f32 {
if t > SPECTRUM_TREBLE_START {
1.0 - (t - SPECTRUM_TREBLE_START) * SPECTRUM_TREBLE_SOFT_KNEE_SLOPE
} else {
1.0
}
}
fn preserve_treble_variance(target: f32, band: usize, total_bands: usize, variance: f32) -> f32 {
(target * treble_variance_expansion_factor(band, total_bands, variance)).clamp(0.0, 0.92)
}
fn treble_variance_expansion_factor(band: usize, total_bands: usize, variance: f32) -> f32 {
let t = band_position(band, total_bands);
if t <= SPECTRUM_TREBLE_START {
return 1.0;
}
let treble_t = ((t - SPECTRUM_TREBLE_START) / (1.0 - SPECTRUM_TREBLE_START)).clamp(0.0, 1.0);
let structured_motion =
(variance.sqrt() * SPECTRUM_TREBLE_VARIANCE_SENSITIVITY).clamp(0.0, 1.0);
1.0 + structured_motion * treble_t * SPECTRUM_TREBLE_VARIANCE_EXPANSION
}
fn treble_delta_variance(targets: &[f32], previous_peaks: &[f32], total_bands: usize) -> f32 {
let start = treble_start_band(total_bands);
let end = targets.len().min(previous_peaks.len());
if start >= end {
return 0.0;
}
let deltas = (start..end)
.map(|band| (targets[band] - previous_peaks[band]).abs())
.collect::<Vec<_>>();
variance(&deltas)
}
fn treble_start_band(total_bands: usize) -> usize {
if total_bands == 0 {
return 0;
}
let denominator = total_bands.saturating_sub(1).max(1) as f32;
((denominator * SPECTRUM_TREBLE_START).floor() as usize + 1).min(total_bands)
}
fn band_position(band: usize, total_bands: usize) -> f32 {
let denominator = total_bands.saturating_sub(1).max(1) as f32;
band as f32 / denominator
}
fn variance(values: &[f32]) -> f32 {
if values.len() < 2 {
return 0.0;
}
let mean = values.iter().sum::<f32>() / values.len() as f32;
values
.iter()
.map(|value| {
let delta = value - mean;
delta * delta
})
.sum::<f32>()
/ values.len() as f32
}
fn smooth_spectrum_targets(targets: &[f32]) -> Vec<f32> {
let mut smoothed = Vec::with_capacity(targets.len());
for i in 0..targets.len() {
let previous = if i > 0 { targets[i - 1] } else { targets[i] };
let current = targets[i];
let next = targets.get(i + 1).copied().unwrap_or(current);
smoothed.push(previous * 0.20 + current * 0.60 + next * 0.20);
}
smoothed
}
fn spectrum_release_curve(band: usize, total_bands: usize) -> f32 {
let denominator = total_bands.saturating_sub(1).max(1) as f32;
let t = band as f32 / denominator;
if t > 0.82 {
0.12
} else {
0.075
}
}
#[derive(Debug, Clone, Copy)]
struct Complex {
re: f32,
im: f32,
}
impl Complex {
fn new(re: f32, im: f32) -> Self {
Self { re, im }
}
fn zero() -> Self {
Self { re: 0.0, im: 0.0 }
}
fn from_real(re: f32) -> Self {
Self { re, im: 0.0 }
}
fn add(self, other: Self) -> Self {
Self {
re: self.re + other.re,
im: self.im + other.im,
}
}
fn sub(self, other: Self) -> Self {
Self {
re: self.re - other.re,
im: self.im - other.im,
}
}
fn mul(self, other: Self) -> Self {
Self {
re: self.re * other.re - self.im * other.im,
im: self.im * other.re + self.re * other.im,
}
}
fn norm(self) -> f32 {
(self.re * self.re + self.im * self.im).sqrt()
}
}
fn fft_rec(input: &[Complex], output: &mut [Complex]) {
let n = input.len();
if n <= 1 {
if n == 1 {
output[0] = input[0];
}
return;
}
let mut even = vec![Complex::zero(); n / 2];
let mut odd = vec![Complex::zero(); n / 2];
for i in 0..n / 2 {
even[i] = input[2 * i];
odd[i] = input[2 * i + 1];
}
let mut even_fft = vec![Complex::zero(); n / 2];
let mut odd_fft = vec![Complex::zero(); n / 2];
fft_rec(&even, &mut even_fft);
fft_rec(&odd, &mut odd_fft);
for k in 0..n / 2 {
let angle = -2.0 * std::f32::consts::PI * (k as f32) / (n as f32);
let twiddle = Complex::new(angle.cos(), angle.sin());
let t = twiddle.mul(odd_fft[k]);
output[k] = even_fft[k].add(t);
output[k + n / 2] = even_fft[k].sub(t);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fading_out_drives_sample_visualizer() {
assert!(playback_drives_sample_visualizer(&PlaybackState::Playing));
assert!(playback_drives_sample_visualizer(
&PlaybackState::FadingOut {
current_volume: 0.4,
}
));
assert!(!playback_drives_sample_visualizer(&PlaybackState::Stopped));
assert!(!playback_drives_sample_visualizer(&PlaybackState::Paused));
}
#[test]
fn spectrum_gain_curve_softens_high_end_without_flattening_treble() {
let mid_gain = spectrum_gain_curve(20, SPECTRUM_ANALYSIS_BANDS);
let final_gain = spectrum_gain_curve(SPECTRUM_ANALYSIS_BANDS - 1, SPECTRUM_ANALYSIS_BANDS);
let full_final_lift = spectrum_broad_lift(1.0);
assert!(final_gain > mid_gain);
assert!(final_gain < full_final_lift);
assert!(final_gain > full_final_lift * 0.95);
}
#[test]
fn treble_variance_expansion_boosts_structured_highs_only() {
let quiet_treble = preserve_treble_variance(
0.4,
SPECTRUM_ANALYSIS_BANDS - 1,
SPECTRUM_ANALYSIS_BANDS,
0.0,
);
let structured_treble = preserve_treble_variance(
0.4,
SPECTRUM_ANALYSIS_BANDS - 1,
SPECTRUM_ANALYSIS_BANDS,
0.04,
);
let mid_band = preserve_treble_variance(0.4, 20, SPECTRUM_ANALYSIS_BANDS, 0.04);
assert!(structured_treble > quiet_treble);
assert_eq!(mid_band, 0.4);
assert!(structured_treble <= 0.92);
}
#[test]
fn treble_delta_variance_detects_structured_motion() {
let previous = vec![0.10; SPECTRUM_ANALYSIS_BANDS];
let flat_targets = vec![0.20; SPECTRUM_ANALYSIS_BANDS];
let mut structured_targets = flat_targets.clone();
for (band, target) in structured_targets
.iter_mut()
.enumerate()
.skip(treble_start_band(SPECTRUM_ANALYSIS_BANDS))
{
*target = if band.is_multiple_of(2) { 0.45 } else { 0.05 };
}
let flat_variance =
treble_delta_variance(&flat_targets, &previous, SPECTRUM_ANALYSIS_BANDS);
let structured_variance =
treble_delta_variance(&structured_targets, &previous, SPECTRUM_ANALYSIS_BANDS);
assert!(structured_variance > flat_variance);
}
#[test]
fn spectrum_target_gates_tiny_noise_floor() {
assert_eq!(
spectrum_target(SPECTRUM_NOISE_FLOOR * 0.5, 30, SPECTRUM_ANALYSIS_BANDS),
0.0
);
}
#[test]
fn smoothing_preserves_constant_flat_targets() {
let targets = vec![0.42; SPECTRUM_ANALYSIS_BANDS];
let smoothed = smooth_spectrum_targets(&targets);
assert_eq!(smoothed, targets);
}
#[test]
fn connecting_spectrum_pattern_resizes_and_stays_subtle() {
let mut peaks = vec![0.99; 3];
update_connecting_spectrum_peaks(&mut peaks, 42);
assert_eq!(peaks.len(), SPECTRUM_ANALYSIS_BANDS);
assert!(peaks.iter().all(|peak| {
(SPECTRUM_CONNECTING_PEAK_MIN..=SPECTRUM_CONNECTING_PEAK_MAX).contains(peak)
}));
assert!(peaks
.iter()
.any(|peak| *peak > SPECTRUM_CONNECTING_PEAK_MIN));
}
#[test]
fn connecting_spectrum_pattern_moves_over_time() {
let changed = (0..SPECTRUM_ANALYSIS_BANDS).any(|band| {
let early = connecting_spectrum_peak(band, 0, SPECTRUM_ANALYSIS_BANDS);
let later = connecting_spectrum_peak(band, 24, SPECTRUM_ANALYSIS_BANDS);
(early - later).abs() > 0.01
});
assert!(changed);
}
#[test]
fn high_treble_releases_faster_than_midrange() {
let mid_release = spectrum_release_curve(20, SPECTRUM_ANALYSIS_BANDS);
let high_release =
spectrum_release_curve(SPECTRUM_ANALYSIS_BANDS - 1, SPECTRUM_ANALYSIS_BANDS);
assert!(high_release > mid_release);
}
}