446 lines
14 KiB
Rust
446 lines
14 KiB
Rust
|
|
|
|
use std::collections::VecDeque;
|
|
|
|
use crate::palette;
|
|
use crate::visualizer::pipeline::BinGpu;
|
|
use crate::visualizer::{FrameData, 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<f32>,
|
|
}
|
|
|
|
/// row of bins for one audio channel.
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct ChannelState {
|
|
pub bins: Vec<BinState>,
|
|
}
|
|
|
|
/// cpu-side smoothing state for the visualizer.
|
|
#[derive(Debug, Default)]
|
|
pub struct VisState {
|
|
pub channels: Vec<ChannelState>,
|
|
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<f32>,
|
|
|
|
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<BinGpu>) {
|
|
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],
|
|
});
|
|
}
|
|
}
|
|
|
|
// flushes the rightmost bin to the viewport edge.
|
|
let max_x = out.iter().map(|b| b.log_x).fold(f32::NEG_INFINITY, f32::max);
|
|
if max_x.is_finite() && max_x > 0.0 {
|
|
let inv = 1.0 / max_x;
|
|
for b in out.iter_mut() {
|
|
b.log_x *= inv;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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::<f32>() / 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<f32> = 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>) -> f32 {
|
|
let buf: Vec<f32> = 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]
|
|
}
|