use super::{quantize_to_scale, AudioParams, Scale, SonifMode, Sonification};
use crate::config::SonificationConfig;
const VOWELS: [(f32, f32, f32); 6] = [
(800.0, 1200.0, 2500.0), (400.0, 2000.0, 2600.0), (300.0, 2300.0, 3000.0), (500.0, 900.0, 2500.0), (300.0, 800.0, 2300.0), (700.0, 1700.0, 2600.0), ];
pub struct VocalMapping {
min: Vec<f64>,
max: Vec<f64>,
alpha: f64,
vowel_pos: f32,
breathiness: f32,
}
impl Default for VocalMapping {
fn default() -> Self {
Self::new()
}
}
impl VocalMapping {
pub fn new() -> Self {
Self {
min: Vec::new(),
max: Vec::new(),
alpha: 0.02,
vowel_pos: 0.0,
breathiness: 0.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) as f32
})
.collect()
}
fn interpolate_formants(t: f32) -> (f32, f32, f32) {
let n = VOWELS.len() as f32;
let scaled = (t.clamp(0.0, 1.0) * (n - 1.0)).max(0.0);
let lo = scaled.floor() as usize;
let hi = (lo + 1).min(VOWELS.len() - 1);
let frac = scaled - lo as f32;
let a = VOWELS[lo];
let b = VOWELS[hi];
(
a.0 + frac * (b.0 - a.0),
a.1 + frac * (b.1 - a.1),
a.2 + frac * (b.2 - a.2),
)
}
}
impl Sonification for VocalMapping {
fn map(&mut self, state: &[f64], speed: f64, config: &SonificationConfig) -> AudioParams {
let norm = self.normalize(state);
let x_norm = norm.get(0).copied().unwrap_or(0.5);
let y_norm = norm.get(1).copied().unwrap_or(0.5);
let blend_rate = 0.005 + y_norm * 0.05;
self.vowel_pos += blend_rate * (x_norm - self.vowel_pos);
let (f1, f2, f3) = Self::interpolate_formants(self.vowel_pos);
let chaos = (speed.abs() as f32 / 100.0).clamp(0.0, 1.0);
self.breathiness += 0.01 * ((chaos * 0.6).max(0.08) - self.breathiness);
let fundamental = if state.len() >= 3 {
let scale = Scale::from(config.scale.as_str());
let base = config.base_frequency as f32;
let oct = config.octave_range as f32;
let z_norm = (state[2] as f32 / 30.0).tanh() * 0.5 + 0.5;
quantize_to_scale(z_norm, base, oct, scale)
} else {
config.base_frequency as f32
};
let mut params = AudioParams {
mode: SonifMode::Vocal,
gain: 0.3,
filter_cutoff: f1,
filter_q: 5.0,
chaos_level: chaos,
..Default::default()
};
params.freqs[0] = fundamental;
params.freqs[1] = f1;
params.freqs[2] = f2;
params.freqs[3] = f3;
params.amps[0] = 0.8;
params.amps[1] = 0.5;
params.amps[2] = 0.3;
params.amps[3] = self.breathiness;
params
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::SonificationConfig;
fn default_config() -> SonificationConfig {
SonificationConfig::default()
}
#[test]
fn test_vocal_output_finite() {
let mut m = VocalMapping::new();
let p = m.map(&[1.0, 2.0, 3.0], 10.0, &default_config());
assert!(p.freqs.iter().all(|f| f.is_finite()));
assert!(p.gain.is_finite());
assert_eq!(p.mode, SonifMode::Vocal);
}
#[test]
fn test_vocal_empty_state() {
let mut m = VocalMapping::new();
let p = m.map(&[], 0.0, &default_config());
assert_eq!(p.mode, SonifMode::Vocal);
assert!(p.freqs[0].is_finite());
}
#[test]
fn test_vocal_formants_positive() {
let mut m = VocalMapping::new();
let p = m.map(&[0.5, 0.5, 1.0], 5.0, &default_config());
assert!(p.freqs[1] > 0.0, "F1 should be positive: {}", p.freqs[1]);
assert!(p.freqs[2] > 0.0, "F2 should be positive: {}", p.freqs[2]);
assert!(p.freqs[3] > 0.0, "F3 should be positive: {}", p.freqs[3]);
}
#[test]
fn test_vocal_vowel_pos_stays_in_range() {
let mut m = VocalMapping::new();
for i in 0..200 {
let v = (i as f64 * 0.3).sin() * 100.0;
let p = m.map(&[v, v * 0.5, v * 0.3], 30.0, &default_config());
assert!(p.freqs.iter().all(|f| f.is_finite()), "non-finite at step {}", i);
}
}
#[test]
fn test_vocal_breathiness_increases_with_speed() {
let mut m_slow = VocalMapping::new();
let mut m_fast = VocalMapping::new();
let state = vec![1.0, 1.0, 1.0];
let mut breathiness_slow = 0.0_f32;
let mut breathiness_fast = 0.0_f32;
for _ in 0..200 {
breathiness_slow = m_slow.map(&state, 0.1, &default_config()).amps[3];
breathiness_fast = m_fast.map(&state, 500.0, &default_config()).amps[3];
}
assert!(
breathiness_fast > breathiness_slow,
"high speed should produce more breathiness: slow={}, fast={}",
breathiness_slow, breathiness_fast
);
}
#[test]
fn test_vocal_fundamental_changes_with_z_dim() {
let mut m = VocalMapping::new();
for _ in 0..10 {
m.map(&[1.0, 1.0, -20.0], 5.0, &default_config());
m.map(&[1.0, 1.0, 20.0], 5.0, &default_config());
}
let mut m1 = VocalMapping::new();
let mut m2 = VocalMapping::new();
let p1 = m1.map(&[1.0, 1.0, -20.0], 5.0, &default_config());
let p2 = m2.map(&[1.0, 1.0, 20.0], 5.0, &default_config());
assert!(
(p1.freqs[0] - p2.freqs[0]).abs() > 1.0,
"different z should give different fundamental: z=-20 → {}, z=20 → {}",
p1.freqs[0], p2.freqs[0]
);
}
#[test]
fn test_vocal_interpolate_formants_boundary() {
let (f1_lo, f2_lo, f3_lo) = VocalMapping::interpolate_formants(0.0);
let (f1_hi, f2_hi, f3_hi) = VocalMapping::interpolate_formants(1.0);
assert!((f1_lo - 800.0).abs() < 1.0, "F1 at t=0 should be 800: {}", f1_lo);
assert!((f2_lo - 1200.0).abs() < 1.0, "F2 at t=0 should be 1200: {}", f2_lo);
assert!((f3_lo - 2500.0).abs() < 1.0, "F3 at t=0 should be 2500: {}", f3_lo);
assert!((f1_hi - 700.0).abs() < 1.0, "F1 at t=1 should be 700: {}", f1_hi);
assert!((f2_hi - 1700.0).abs() < 1.0, "F2 at t=1 should be 1700: {}", f2_hi);
assert!((f3_hi - 2600.0).abs() < 1.0, "F3 at t=1 should be 2600: {}", f3_hi);
}
}