use crate::glottal::GlottalModel;
use crate::prosody::{IntonationPattern, Stress};
#[must_use]
#[inline]
pub fn rd_from_arousal(arousal: f32) -> f32 {
let arousal = arousal.clamp(0.0, 1.0);
2.2 - 1.8 * arousal
}
#[must_use]
#[inline]
pub fn vibrato_depth_from_valence(valence: f32) -> f32 {
let valence = valence.clamp(-1.0, 1.0);
(0.03 + 0.03 * valence).clamp(0.0, 0.06)
}
#[must_use]
#[inline]
pub fn breathiness_from_arousal(arousal: f32) -> f32 {
let arousal = arousal.clamp(0.0, 1.0);
0.3 - 0.28 * arousal
}
#[must_use]
#[inline]
pub fn jitter_from_arousal(arousal: f32) -> f32 {
let arousal = arousal.clamp(0.0, 1.0);
let distance = (arousal - 0.5).abs();
0.005 + 0.025 * distance * 2.0
}
#[must_use]
pub fn intonation_from_emotion(category: u8) -> Option<IntonationPattern> {
match category {
0 => Some(IntonationPattern::Declarative),
1 | 3 => Some(IntonationPattern::Exclamatory),
2 => Some(IntonationPattern::Declarative),
4 => Some(IntonationPattern::Interrogative),
5 => Some(IntonationPattern::Continuation),
_ => None,
}
}
#[must_use]
#[inline]
pub fn f0_range_scale_from_arousal(arousal: f32) -> f32 {
let arousal = arousal.clamp(0.0, 1.0);
0.6 + 1.2 * arousal
}
#[must_use]
#[inline]
pub fn duration_scale_from_speech_rate(rate: f32) -> f32 {
if rate <= 0.0 {
return 1.0;
}
(1.0 / rate).clamp(0.3, 3.0)
}
#[must_use]
pub fn stress_from_tobi_accent(level: u8) -> Stress {
match level {
0 => Stress::Unstressed,
1 => Stress::Secondary,
_ => Stress::Primary,
}
}
#[must_use]
#[inline]
pub fn f0_peak_from_prominence(prominence: f32) -> f32 {
let prominence = prominence.clamp(0.0, 1.0);
1.0 + 0.3 * prominence
}
#[must_use]
#[inline]
pub fn formant_scale_from_body_size(size: f32) -> f32 {
let size = size.clamp(0.0, 1.0);
1.4 - 0.55 * size
}
#[must_use]
#[inline]
pub fn f0_from_body_size(size: f32) -> f32 {
let size = size.clamp(0.0, 1.0);
400.0 - 330.0 * size
}
#[must_use]
#[inline]
pub fn jitter_from_age(age: f32) -> f32 {
let age = age.clamp(0.0, 1.0);
let distance = (age - 0.4).abs();
0.005 + 0.035 * distance / 0.6
}
#[must_use]
pub fn glottal_model_from_effort(effort: f32) -> (GlottalModel, f32) {
let effort = effort.clamp(0.0, 1.0);
if effort < 0.3 {
(GlottalModel::LF, 2.0 + (0.3 - effort) * 2.3)
} else if effort > 0.7 {
(GlottalModel::LF, 0.8 - (effort - 0.7) * 1.5)
} else {
(GlottalModel::Rosenberg, 1.0)
}
}
#[must_use]
#[inline]
pub fn gain_from_distance(ref_distance: f32, distance: f32) -> f32 {
if distance <= ref_distance || ref_distance <= 0.0 {
return 1.0;
}
(ref_distance / distance).clamp(0.0, 1.0)
}
#[must_use]
#[inline]
pub fn bandwidth_scale_from_reverb(rt60: f32) -> f32 {
let rt60 = rt60.max(0.0);
(1.0 + 0.4 * rt60).min(2.0)
}
#[must_use]
#[inline]
pub fn spectral_tilt_from_distance(distance_m: f32) -> f32 {
(distance_m * 0.005).min(6.0)
}
#[must_use]
#[inline]
pub fn lombard_effort_from_noise(ambient_db_spl: f32) -> f32 {
if ambient_db_spl < 45.0 {
1.0
} else if ambient_db_spl > 85.0 {
1.8
} else {
1.0 + 0.8 * (ambient_db_spl - 45.0) / 40.0
}
}
#[must_use]
#[inline]
pub fn lombard_f0_shift(ambient_db_spl: f32) -> f32 {
let excess = (ambient_db_spl - 50.0).max(0.0);
(excess * 0.75).min(30.0)
}
#[must_use]
#[inline]
pub fn breathiness_reduction_from_wind(wind_speed_ms: f32) -> f32 {
let wind = wind_speed_ms.max(0.0);
if wind < 5.0 {
1.0
} else {
(1.0 - (wind - 5.0) / 10.0).clamp(0.2, 1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rd_from_arousal_range() {
let calm = rd_from_arousal(0.0);
let excited = rd_from_arousal(1.0);
assert!(calm > excited, "calm should have higher Rd (breathier)");
assert!((0.3..=2.7).contains(&calm));
assert!((0.3..=2.7).contains(&excited));
}
#[test]
fn test_rd_from_arousal_clamps() {
assert!((rd_from_arousal(-1.0) - rd_from_arousal(0.0)).abs() < f32::EPSILON);
assert!((rd_from_arousal(2.0) - rd_from_arousal(1.0)).abs() < f32::EPSILON);
}
#[test]
fn test_breathiness_from_arousal() {
let calm = breathiness_from_arousal(0.0);
let excited = breathiness_from_arousal(1.0);
assert!(calm > excited);
assert!(calm <= 1.0 && excited >= 0.0);
}
#[test]
fn test_jitter_from_arousal_u_shape() {
let low = jitter_from_arousal(0.0);
let mid = jitter_from_arousal(0.5);
let high = jitter_from_arousal(1.0);
assert!(mid < low, "mid-arousal should be most stable");
assert!(mid < high, "mid-arousal should be most stable");
}
#[test]
fn test_duration_scale_from_speech_rate() {
assert!((duration_scale_from_speech_rate(1.0) - 1.0).abs() < f32::EPSILON);
assert!(duration_scale_from_speech_rate(2.0) < 1.0);
assert!(duration_scale_from_speech_rate(0.5) > 1.0);
assert!((duration_scale_from_speech_rate(0.0) - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_stress_from_tobi_accent() {
assert_eq!(stress_from_tobi_accent(0), Stress::Unstressed);
assert_eq!(stress_from_tobi_accent(1), Stress::Secondary);
assert_eq!(stress_from_tobi_accent(2), Stress::Primary);
assert_eq!(stress_from_tobi_accent(4), Stress::Primary);
}
#[test]
fn test_formant_scale_from_body_size() {
let small = formant_scale_from_body_size(0.0);
let large = formant_scale_from_body_size(1.0);
assert!(small > large, "small body should have higher formant scale");
}
#[test]
fn test_f0_from_body_size() {
let small = f0_from_body_size(0.0);
let large = f0_from_body_size(1.0);
assert!(small > large, "small body should have higher f0");
assert!((20.0..=2000.0).contains(&small));
assert!((20.0..=2000.0).contains(&large));
}
#[test]
fn test_gain_from_distance() {
assert!((gain_from_distance(1.0, 1.0) - 1.0).abs() < f32::EPSILON);
assert!((gain_from_distance(1.0, 2.0) - 0.5).abs() < f32::EPSILON);
assert!((gain_from_distance(1.0, 0.5) - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_lombard_effort_from_noise() {
assert!((lombard_effort_from_noise(30.0) - 1.0).abs() < f32::EPSILON);
assert!(lombard_effort_from_noise(65.0) > 1.0);
assert!((lombard_effort_from_noise(90.0) - 1.8).abs() < f32::EPSILON);
}
#[test]
fn test_glottal_model_from_effort() {
let (model_low, rd_low) = glottal_model_from_effort(0.1);
assert_eq!(model_low, GlottalModel::LF);
assert!(rd_low > 1.5, "low effort should be breathy (high Rd)");
let (model_mid, _) = glottal_model_from_effort(0.5);
assert_eq!(model_mid, GlottalModel::Rosenberg);
let (model_high, rd_high) = glottal_model_from_effort(0.9);
assert_eq!(model_high, GlottalModel::LF);
assert!(rd_high < 1.0, "high effort should be pressed (low Rd)");
}
#[test]
fn test_bandwidth_scale_from_reverb() {
let dry = bandwidth_scale_from_reverb(0.0);
let wet = bandwidth_scale_from_reverb(2.0);
assert!((dry - 1.0).abs() < f32::EPSILON);
assert!(wet > dry);
assert!(wet <= 2.0);
}
#[test]
fn test_breathiness_reduction_from_wind() {
assert!((breathiness_reduction_from_wind(0.0) - 1.0).abs() < f32::EPSILON);
assert!(breathiness_reduction_from_wind(10.0) < 1.0);
assert!(breathiness_reduction_from_wind(20.0) >= 0.2);
}
}