use super::{AudioParams, SonifMode, Sonification};
use crate::config::SonificationConfig;
pub struct WaveguideMapping {
min: Vec<f64>,
max: Vec<f64>,
alpha: f64, excite_cooldown: u32,
}
impl WaveguideMapping {
pub fn new() -> Self {
Self {
min: Vec::new(),
max: Vec::new(),
alpha: 0.002,
excite_cooldown: 0,
}
}
fn normalize(&mut self, state: &[f64]) -> Vec<f32> {
if self.min.len() != state.len() {
self.min = state.to_vec();
self.max = state.to_vec();
}
state
.iter()
.enumerate()
.map(|(i, &v)| {
if v < self.min[i] {
self.min[i] = v;
} else {
self.min[i] += self.alpha * (v - self.min[i]);
}
if v > self.max[i] {
self.max[i] = v;
} else {
self.max[i] += self.alpha * (v - self.max[i]);
}
let range = (self.max[i] - self.min[i]).abs().max(1e-9);
((v - self.min[i]) / range).clamp(0.0, 1.0) as f32
})
.collect()
}
}
impl Default for WaveguideMapping {
fn default() -> Self {
Self::new()
}
}
impl Sonification for WaveguideMapping {
fn map(&mut self, state: &[f64], speed: f64, config: &SonificationConfig) -> AudioParams {
let norm = self.normalize(state);
let tension = norm.first().copied().unwrap_or(0.5);
let damping_norm = if norm.len() >= 2 { norm[1] } else { 0.5 };
let damping = 0.90 + damping_norm * 0.099;
let energy = (speed.abs() as f32 / 200.0).clamp(0.0, 1.0);
let excite = if self.excite_cooldown == 0 && energy > 0.6 {
self.excite_cooldown = 30; true
} else {
if self.excite_cooldown > 0 {
self.excite_cooldown -= 1;
}
false
};
let base_hz = config.base_frequency as f32;
AudioParams {
mode: SonifMode::Waveguide,
gain: 0.5 + energy * 0.5,
waveguide_tension: tension,
waveguide_damping: damping,
waveguide_excite: excite,
ks_freq: base_hz.max(40.0),
chaos_level: energy,
filter_cutoff: 2000.0 + energy * 8000.0,
filter_q: 0.7,
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::SonificationConfig;
fn default_config() -> SonificationConfig {
SonificationConfig::default()
}
#[test]
fn test_waveguide_mapping_output_finite() {
let mut m = WaveguideMapping::new();
let p = m.map(&[1.0, 2.0, 3.0], 10.0, &default_config());
assert!(p.waveguide_tension.is_finite());
assert!(p.waveguide_damping.is_finite());
assert!(p.gain.is_finite());
assert_eq!(p.mode, SonifMode::Waveguide);
}
#[test]
fn test_waveguide_damping_in_range() {
let mut m = WaveguideMapping::new();
let p = m.map(&[0.0, 1.0], 5.0, &default_config());
assert!(p.waveguide_damping >= 0.90 && p.waveguide_damping <= 0.999,
"damping {} out of [0.90, 0.999]", p.waveguide_damping);
}
#[test]
fn test_waveguide_excite_triggers_on_speed_spike() {
let mut m = WaveguideMapping::new();
let p_low = m.map(&[1.0, 1.0], 0.0, &default_config());
assert!(!p_low.waveguide_excite, "should not excite at low speed");
let p_high = m.map(&[1.0, 1.0], 200.0, &default_config());
assert!(p_high.waveguide_excite, "should excite at high speed");
}
#[test]
fn test_waveguide_tension_in_range() {
let mut m = WaveguideMapping::new();
for i in 0..20 {
let p = m.map(&[i as f64 * 0.5, 0.0], 5.0, &default_config());
assert!(p.waveguide_tension >= 0.0 && p.waveguide_tension <= 1.0,
"tension {} out of [0,1]", p.waveguide_tension);
}
}
#[test]
fn test_waveguide_gain_increases_with_speed() {
let mut m_low = WaveguideMapping::new();
let mut m_high = WaveguideMapping::new();
let p_low = m_low.map(&[1.0, 1.0], 0.0, &default_config());
let p_high = m_high.map(&[1.0, 1.0], 200.0, &default_config());
assert!(p_high.gain > p_low.gain,
"Higher speed should produce higher gain: low={}, high={}", p_low.gain, p_high.gain);
}
#[test]
fn test_waveguide_excite_cooldown_prevents_double_trigger() {
let mut m = WaveguideMapping::new();
let p1 = m.map(&[1.0, 1.0], 200.0, &default_config());
assert!(p1.waveguide_excite, "first high-speed call should excite");
let p2 = m.map(&[1.0, 1.0], 200.0, &default_config());
assert!(!p2.waveguide_excite, "cooldown should prevent immediate re-trigger");
}
#[test]
fn test_waveguide_filter_cutoff_increases_with_speed() {
let mut m_low = WaveguideMapping::new();
let mut m_high = WaveguideMapping::new();
let p_low = m_low.map(&[1.0, 1.0], 0.0, &default_config());
let p_high = m_high.map(&[1.0, 1.0], 200.0, &default_config());
assert!(p_high.filter_cutoff > p_low.filter_cutoff,
"filter_cutoff should increase with speed: low={}, high={}", p_low.filter_cutoff, p_high.filter_cutoff);
}
}