use super::{AudioParams, SonifMode, Sonification};
use crate::config::SonificationConfig;
use std::collections::VecDeque;
const NUM_PARTIALS: usize = 32;
const HISTORY_LEN: usize = 256;
pub struct SpectralMapping {
smoothed: [f32; NUM_PARTIALS],
alpha: f32,
history: VecDeque<Vec<f64>>,
cached_raw: [f32; NUM_PARTIALS],
prev_state_hash: u64,
}
impl Default for SpectralMapping {
fn default() -> Self {
Self::new()
}
}
impl SpectralMapping {
pub fn new() -> Self {
Self {
smoothed: [0.0; NUM_PARTIALS],
alpha: 0.05,
history: VecDeque::with_capacity(HISTORY_LEN + 1),
cached_raw: [0.0; NUM_PARTIALS],
prev_state_hash: u64::MAX,
}
}
fn hash_state(state: &[f64]) -> u64 {
let mut h: u64 = 0xcbf29ce484222325; for &v in state {
let bits = v.to_bits();
h ^= bits;
h = h.wrapping_mul(0x100000001b3); }
h
}
#[inline]
fn dft_mag(signal: &[f64], k: usize) -> f32 {
let n = signal.len();
let nf = n as f64;
let angle_step = std::f64::consts::TAU * k as f64 / nf;
let (ca, sa) = (angle_step.cos(), angle_step.sin());
let mut cos_t = 1.0f64;
let mut sin_t = 0.0f64;
let mut re = 0.0f64;
let mut im = 0.0f64;
for (i, &v) in signal.iter().enumerate() {
let w = 0.5 * (1.0 - (std::f64::consts::TAU * i as f64 / (nf - 1.0)).cos());
let wv = v * w;
re += wv * cos_t;
im -= wv * sin_t;
let new_cos = cos_t * ca - sin_t * sa;
let new_sin = sin_t * ca + cos_t * sa;
cos_t = new_cos;
sin_t = new_sin;
}
((re * re + im * im) / (nf * nf / 4.0)).sqrt() as f32
}
}
impl Sonification for SpectralMapping {
fn map(&mut self, state: &[f64], _speed: f64, config: &SonificationConfig) -> AudioParams {
if state.is_empty() {
return AudioParams {
mode: SonifMode::Spectral,
..Default::default()
};
}
self.history.push_back(state.to_vec());
if self.history.len() > HISTORY_LEN {
self.history.pop_front();
}
let state_hash = Self::hash_state(state);
if state_hash != self.prev_state_hash {
self.prev_state_hash = state_hash;
let mut raw = [0.0f32; NUM_PARTIALS];
if self.history.len() >= 32 {
let hn = self.history.len();
let n_dims = state.len().min(4);
let n_bins = NUM_PARTIALS.min(hn / 2 + 1);
let signals: Vec<Vec<f64>> = (0..n_dims)
.map(|d| {
let s: Vec<f64> = self
.history
.iter()
.map(|h| h.get(d).copied().unwrap_or(0.0))
.collect();
let mean = s.iter().sum::<f64>() / s.len() as f64;
s.iter().map(|&v| v - mean).collect()
})
.collect();
for k in 0..n_bins {
raw[k] = signals.iter().map(|sig| Self::dft_mag(sig, k)).sum();
}
let max_val = raw.iter().cloned().fold(0.0f32, f32::max).max(1e-9);
for r in &mut raw {
*r /= max_val;
}
} else {
let max_abs = state
.iter()
.map(|v| v.abs())
.fold(0.0f64, f64::max)
.max(1e-9);
for (k, slot) in raw.iter_mut().enumerate() {
let i = k % state.len();
*slot = (state[i].abs() / max_abs) as f32;
}
}
for (k, slot) in raw.iter_mut().enumerate() {
*slot *= 1.0 / (1.0 + (k as f32).powf(1.5) * 0.08);
}
self.cached_raw = raw;
}
for (k, s) in self.smoothed.iter_mut().enumerate() {
*s += self.alpha * (self.cached_raw[k] - *s);
}
let chaos_level = {
let mag: f64 = state.iter().take(3).map(|v| v * v).sum::<f64>().sqrt();
((mag / 50.0) as f32).clamp(0.0, 1.0)
};
let base = config.base_frequency as f32;
let buzz_freq = base * (1.0 + chaos_level * 0.08);
let active_partials = self.smoothed.iter().filter(|&&v| v > 0.01).count().max(1);
let gain = (1.0 / (active_partials as f32).sqrt()).clamp(0.08, 0.5);
let mut p = AudioParams {
mode: SonifMode::Spectral,
partials: self.smoothed,
partials_base_freq: buzz_freq,
gain,
filter_cutoff: 8000.0,
filter_q: 0.7,
..Default::default()
};
p.chaos_level = chaos_level;
p
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::SonificationConfig;
fn default_config() -> SonificationConfig {
SonificationConfig::default()
}
#[test]
fn test_spectral_output_finite() {
let mut m = SpectralMapping::new();
let p = m.map(&[1.0, 2.0, 3.0], 10.0, &default_config());
assert!(p.partials.iter().all(|v| v.is_finite()));
assert!(p.gain.is_finite());
assert_eq!(p.mode, SonifMode::Spectral);
}
#[test]
fn test_spectral_empty_state() {
let mut m = SpectralMapping::new();
let p = m.map(&[], 0.0, &default_config());
assert_eq!(p.mode, SonifMode::Spectral);
assert!(p.gain.is_finite());
}
#[test]
fn test_spectral_history_fills() {
let mut m = SpectralMapping::new();
for i in 0..300 {
let state = vec![(i as f64 * 0.1).sin() * 10.0, (i as f64 * 0.07).cos() * 5.0];
let p = m.map(&state, 5.0, &default_config());
assert!(p.partials.iter().all(|v| v.is_finite()),
"partials non-finite at step {}", i);
}
}
#[test]
fn test_spectral_partials_in_range() {
let mut m = SpectralMapping::new();
for i in 0..100 {
let state = vec![(i as f64).sin() * 20.0];
let p = m.map(&state, 10.0, &default_config());
for (k, &v) in p.partials.iter().enumerate() {
assert!(v >= 0.0, "partial {} has negative value {}", k, v);
}
}
}
#[test]
fn test_spectral_dft_path_nonzero() {
let mut m = SpectralMapping::new();
let mut last_p = None;
for i in 0..50 {
let state = vec![(i as f64 * 0.3).sin() * 5.0, (i as f64 * 0.17).cos() * 3.0];
last_p = Some(m.map(&state, 10.0, &default_config()));
}
let p = last_p.unwrap();
let nonzero = p.partials.iter().filter(|&&v| v > 0.0).count();
assert!(nonzero > 0, "DFT path should produce some non-zero partials after warmup");
}
#[test]
fn test_spectral_caching_same_state() {
let mut m = SpectralMapping::new();
let state = vec![3.0, -1.5, 2.0];
let p1 = m.map(&state, 5.0, &default_config());
let p2 = m.map(&state, 5.0, &default_config());
assert!(p1.partials.iter().all(|v| v.is_finite()), "first call should be finite");
assert!(p2.partials.iter().all(|v| v.is_finite()), "second call should be finite");
}
#[test]
fn test_spectral_gain_bounded() {
let mut m = SpectralMapping::new();
for i in 0..200 {
let state = vec![(i as f64 * 0.2).sin() * 15.0, (i as f64 * 0.13).cos() * 8.0];
let p = m.map(&state, 20.0, &default_config());
assert!(
p.gain >= 0.08 && p.gain <= 0.5,
"gain {} out of [0.08, 0.5] at step {}", p.gain, i
);
}
}
}