use std::collections::VecDeque; use crate::analyzer::FrameData; use crate::palette; use crate::visualizer::pipeline::BinGpu; use crate::visualizer::VizParams; const HUE_HISTORY_LEN: usize = 40; const HISTORY_LEN: usize = 30; /// per-bin smoothed magnitude, modulation offsets, palette color, and recent visual history. #[derive(Debug, Clone, Default)] pub struct BinState { pub visual_db: f32, pub primary_visual_db: f32, pub last_raw_db: f32, pub bright_mod: f32, pub alpha_mod: f32, pub cached_color: [f32; 3], pub history: VecDeque, } /// row of bins for one audio channel. #[derive(Debug, Default, Clone)] pub struct ChannelState { pub bins: Vec, } /// cpu-side smoothing state for the visualizer. #[derive(Debug, Default)] pub struct VisState { pub channels: Vec, pub hue_history: VecDeque<(f32, f32)>, pub hue_sum_cos: f32, pub hue_sum_sin: f32, pub unified_color: [f32; 3], pub smoothed_cepstrum: Vec, pub last_frames_id: usize, } impl VisState { /// folds a fresh analyzer frame into the smoothed bin, hue, and cepstrum tracks. pub fn ingest( &mut self, frames: &[FrameData], frames_id: usize, params: &VizParams, palette: Option<&[[f32; 3]]>, ) { self.last_frames_id = frames_id; if self.channels.len() != frames.len() { self.channels.resize(frames.len(), ChannelState::default()); } if params.glass { if let Some(f0) = frames.first() { self.unified_color = self.update_glass_color(f0, params); } } else { self.unified_color = [0.0, 0.0, 1.0]; } for (ch_idx, frame) in frames.iter().enumerate() { ingest_channel(&mut self.channels[ch_idx], frame, params, palette); } if params.mirrored { if let Some(f0) = frames.first() { let raw = &f0.cepstrum; if self.smoothed_cepstrum.len() != raw.len() { self.smoothed_cepstrum = vec![0.0; raw.len()]; } for (i, r) in raw.iter().enumerate() { self.smoothed_cepstrum[i] = 0.15 * r + 0.85 * self.smoothed_cepstrum[i]; } } } } /// flattens every channel's bins into the gpu-bound vector and applies a small x-shift to the right channel. pub fn pack_bins(&self, frames: &[FrameData], stereo: bool, out: &mut Vec) { out.clear(); let n_bins = self.channels.first().map(|c| c.bins.len()).unwrap_or(0); if n_bins == 0 { return; } for (ch_idx, channel) in self.channels.iter().enumerate() { let freqs = frames .get(ch_idx) .map(|f| f.freqs.as_slice()) .unwrap_or(&[]); let x_offset = if ch_idx == 1 && stereo { 1.005 } else { 1.0 }; for (i, b) in channel.bins.iter().enumerate() { let freq = freqs.get(i).copied().unwrap_or(0.0); let visual_norm = ((b.visual_db + 80.0) / 80.0).clamp(0.0, 1.0); let primary_norm = ((b.primary_visual_db + 80.0) / 80.0).clamp(0.0, 1.0); out.push(BinGpu { log_x: log_x(freq * x_offset), visual_norm, primary_norm, bright_mod: b.bright_mod, alpha_mod: b.alpha_mod, hue: b.cached_color[0], sat: b.cached_color[1], val: b.cached_color[2], }); } } } /// derives a hue from spectral midpoint and mean amplitude, smoothed by a circular running mean. fn update_glass_color(&mut self, f0: &FrameData, params: &VizParams) -> [f32; 3] { let mid_freq = f0 .freqs .get(f0.freqs.len() / 2) .copied() .unwrap_or(1000.0); let mean_db = if f0.db.is_empty() { -80.0 } else { f0.db.iter().sum::() / f0.db.len() as f32 }; let log_min = 20.0_f32.log10(); let log_max = 20_000.0_f32.log10(); let freq_norm = (mid_freq.max(1e-9).log10() - log_min) / (log_max - log_min); let amp_norm = ((mean_db + 80.0) / 80.0).clamp(0.0, 1.0); let amp_weight = (1.0 / (freq_norm + 1e-4).powf(5.0) * 2.0).clamp(0.5, 6.0); let mut hue = (freq_norm + amp_norm * amp_weight * params.hue).rem_euclid(1.0); if params.mirrored { hue = 1.0 - hue; } if hue < 0.0 { hue += 1.0; } let angle = hue * std::f32::consts::TAU; let cos_v = angle.cos(); let sin_v = angle.sin(); self.hue_history.push_back((cos_v, sin_v)); self.hue_sum_cos += cos_v; self.hue_sum_sin += sin_v; if self.hue_history.len() > HUE_HISTORY_LEN { if let Some((c, s)) = self.hue_history.pop_front() { self.hue_sum_cos -= c; self.hue_sum_sin -= s; } } let smoothed_angle = self.hue_sum_sin.atan2(self.hue_sum_cos); let mut smoothed_hue = smoothed_angle / std::f32::consts::TAU; if smoothed_hue < 0.0 { smoothed_hue += 1.0; } [smoothed_hue, 1.0, 1.0] } } /// maps a frequency in hertz to a 0..1 horizontal position on the 20 hz to 20 khz log axis. fn log_x(freq: f32) -> f32 { let log_min = 20.0_f32.log10(); let log_max = 20_000.0_f32.log10(); if freq <= 0.0 { return 0.0; } (freq.max(1e-9).log10() - log_min) / (log_max - log_min) } /// updates one channel's bin smoothing, peak modulations, treble compensation, and palette colors. fn ingest_channel( channel: &mut ChannelState, frame: &FrameData, params: &VizParams, palette: Option<&[[f32; 3]]>, ) { let n = frame.db.len(); if channel.bins.len() != n { channel.bins.resize(n, BinState::default()); } if n == 0 { return; } let use_entropy = params.entropy_on; let mut bin_entropy = vec![0.0_f32; n]; if use_entropy { for (i, b) in channel.bins.iter().enumerate() { bin_entropy[i] = calculate_entropy(&b.history); } } let median_entropy = if use_entropy { median_of(&bin_entropy) } else { 0.0 }; for (i, b) in channel.bins.iter_mut().enumerate() { let raw = frame.db[i]; let primary = frame.primary_db.get(i).copied().unwrap_or(raw); let change = raw - b.visual_db; if use_entropy { let relative = median_entropy - bin_entropy[i]; let base = 1.5_f32; let reward_gain = base + params.entropy_strength; let penalty_gain = base - params.entropy_strength; let gain = if relative >= 0.0 { reward_gain } else { penalty_gain }; let multiplier = (1.0 + relative * gain * 2.0).clamp(0.05, 4.0); b.visual_db += change * multiplier; b.history.push_back(b.visual_db); while b.history.len() > HISTORY_LEN { b.history.pop_front(); } } else { let resp = 0.2_f32; b.visual_db = b.visual_db * (1.0 - resp) + raw * resp; b.history.push_back(b.visual_db); while b.history.len() > HISTORY_LEN { b.history.pop_front(); } } let pattern_resp = 0.1_f32; b.primary_visual_db = b.primary_visual_db * (1.0 - pattern_resp) + primary * pattern_resp; b.last_raw_db = raw; } let mut vertex_energy: Vec = channel .bins .iter() .map(|b| ((b.primary_visual_db + 80.0) / 80.0).clamp(0.0, 1.0)) .collect(); let split = n / 2; let mut max_low = 0.01_f32; let mut max_high = 0.01_f32; for v in vertex_energy.iter().take(split) { max_low = max_low.max(*v); } for v in vertex_energy.iter().skip(split) { max_high = max_high.max(*v); } let treble_boost = (max_low / max_high).clamp(1.0, 40.0); let mut global_max = 0.001_f32; for (j, v) in vertex_energy.iter_mut().enumerate() { if j >= split { let t = (j - split) as f32 / (n - split) as f32; *v *= 1.0 + (treble_boost - 1.0) * t; } let compressed = v.tanh(); *v = compressed; if compressed > global_max { global_max = compressed; } } for v in vertex_energy.iter_mut() { *v = (*v / global_max).clamp(0.0, 1.0); } for b in channel.bins.iter_mut() { b.bright_mod = 0.0; b.alpha_mod = 0.0; } let entropy_factor = if use_entropy { params.entropy_strength.abs().max(0.1) } else { 1.0 }; if n >= 3 { for i in 1..n - 1 { let curr = vertex_energy[i]; let prev = vertex_energy[i - 1]; let next = vertex_energy[i + 1]; if curr > prev && curr > next { let left_dominant = prev > next; let sharpness = (curr - prev).min(curr - next); let peak_intensity = (sharpness * 10.0 * entropy_factor).powf(0.3).clamp(0.0, 1.0); let decay_base = 0.65 - (sharpness * 3.0).clamp(0.0, 0.35); for d in 1..=12_i32 { apply_pattern(&mut channel.bins, i, d, left_dominant, -1, peak_intensity, decay_base); apply_pattern(&mut channel.bins, i, d, !left_dominant, 1, peak_intensity, decay_base); } } } } let use_palette = params.album_colors && palette .map(|p| !p.is_empty()) .unwrap_or(false); let denom = (n as f32 - 1.0).max(1.0); for (i, b) in channel.bins.iter_mut().enumerate() { if use_palette { let pal = palette.unwrap(); let plen = pal.len(); let raw_idx = if plen >= n { i * (plen - 1) / (n - 1).max(1) } else { i * plen / n.max(1) }; let pal_idx = if params.mirrored { plen.saturating_sub(1).saturating_sub(raw_idx) } else { raw_idx } .min(plen - 1); let rgb = pal[pal_idx]; let (h, s, v) = palette::rgb_to_hsv(rgb[0], rgb[1], rgb[2]); b.cached_color = [h, s, v]; } else { let mut hue = i as f32 / denom; if params.mirrored { hue = 1.0 - hue; } b.cached_color = [hue, 1.0, 1.0]; } } } /// stamps a three-step bright/alpha modulation pattern outward from a peak bin, decaying with distance. fn apply_pattern( bins: &mut [BinState], centre: usize, dist: i32, is_bright_side: bool, direction: i32, peak_intensity: f32, decay_base: f32, ) { let target = if direction == -1 { centre as isize - dist as isize } else { centre as isize + dist as isize - 1 }; if target < 0 || target as usize >= bins.len() { return; } let cycle = (dist - 1) / 3; let step = (dist - 1) % 3; let decay = decay_base.powi(cycle); let intensity = peak_intensity * decay; if intensity < 0.01 { return; } let mut ty = step; if is_bright_side { ty = (ty + 2) % 3; } let bin = &mut bins[target as usize]; match ty { 0 => { bin.bright_mod += 0.8 * intensity; bin.alpha_mod -= 0.8 * intensity; } 1 => { bin.bright_mod -= 0.8 * intensity; bin.alpha_mod += 0.2 * intensity; } _ => { bin.bright_mod += 0.8 * intensity; bin.alpha_mod += 0.2 * intensity; } } } /// scores deviation of a bin's recent history from a low-frequency reconstruction as the entropy proxy. fn calculate_entropy(history: &VecDeque) -> f32 { let buf: Vec = history.iter().copied().collect(); let n = buf.len(); if n < 4 { return 0.0; } let nf = n as f64; let mut x_re = vec![0.0_f64; n]; let mut x_im = vec![0.0_f64; n]; for k in 0..n { let mut re = 0.0; let mut im = 0.0; for (idx, &h) in buf.iter().enumerate() { let angle = -2.0 * std::f64::consts::PI * k as f64 * idx as f64 / nf; re += h as f64 * angle.cos(); im += h as f64 * angle.sin(); } x_re[k] = re; x_im[k] = im; } for k in (n / 2 + 1)..n { x_re[k] = 0.0; x_im[k] = 0.0; } for k in 1..n.div_ceil(2) { x_re[k] *= 2.0; x_im[k] *= 2.0; } let mut sq_sum = 0.0_f64; for idx in 0..n { let mut im = 0.0; for k in 0..n { let angle = 2.0 * std::f64::consts::PI * k as f64 * idx as f64 / nf; im += x_re[k] * angle.sin() + x_im[k] * angle.cos(); } im /= nf; sq_sum += im * im; } ((sq_sum / nf).sqrt() as f32 / 10.0).clamp(0.0, 1.0) } /// returns the median element via a partial selection sort over a copy. fn median_of(values: &[f32]) -> f32 { if values.is_empty() { return 0.0; } let mut v = values.to_vec(); let mid = v.len() / 2; v.select_nth_unstable_by(mid, |a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); v[mid] }