use serde::{Deserialize, Serialize};
use crate::vocalization::{CallIntent, Vocalization};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[must_use]
pub struct EmotionState {
valence: f32,
arousal: f32,
smoothing: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[must_use]
pub struct EmotionOutput {
pub vocalization: Vocalization,
pub intent: CallIntent,
pub vocal_effort: f32,
pub pitch_scale: f32,
pub breathiness_delta: f32,
}
impl EmotionState {
pub fn new() -> Self {
Self {
valence: 0.0,
arousal: 0.2,
smoothing: 0.1,
}
}
pub fn with_values(valence: f32, arousal: f32) -> Self {
Self {
valence: valence.clamp(-1.0, 1.0),
arousal: arousal.clamp(0.0, 1.0),
smoothing: 0.1,
}
}
pub fn with_smoothing(mut self, smoothing: f32) -> Self {
self.smoothing = smoothing.clamp(0.0, 0.95);
self
}
#[must_use]
pub fn valence(&self) -> f32 {
self.valence
}
#[must_use]
pub fn arousal(&self) -> f32 {
self.arousal
}
pub fn update(&mut self, target_valence: f32, target_arousal: f32) {
let tv = target_valence.clamp(-1.0, 1.0);
let ta = target_arousal.clamp(0.0, 1.0);
let s = self.smoothing;
self.valence = self.valence * s + tv * (1.0 - s);
self.arousal = self.arousal * s + ta * (1.0 - s);
}
pub fn set(&mut self, valence: f32, arousal: f32) {
self.valence = valence.clamp(-1.0, 1.0);
self.arousal = arousal.clamp(0.0, 1.0);
}
#[must_use = "returns synthesis parameters derived from the emotional state"]
pub fn evaluate(&self) -> EmotionOutput {
let vocalization = self.select_vocalization();
let intent = self.select_intent();
let vocal_effort = 0.2 + self.arousal * 0.7;
let pitch_scale = 1.0 + self.valence * 0.15;
let breathiness_delta = if self.arousal > 0.8 {
(self.arousal - 0.8) * 0.75 } else if self.arousal < 0.2 {
(0.2 - self.arousal) * 0.5 } else {
0.0
};
EmotionOutput {
vocalization,
intent,
vocal_effort,
pitch_scale,
breathiness_delta,
}
}
fn select_vocalization(&self) -> Vocalization {
match (self.valence_zone(), self.arousal_zone()) {
(ValenceZone::Negative, ArousalZone::High) => {
if self.valence < -0.5 {
Vocalization::Screech
} else {
Vocalization::Bark
}
}
(ValenceZone::Positive, ArousalZone::High) => {
if self.valence > 0.5 {
Vocalization::Trill
} else {
Vocalization::Chirp
}
}
(ValenceZone::Neutral, ArousalZone::High) => Vocalization::Bark,
(ValenceZone::Negative, ArousalZone::Low) => Vocalization::Growl,
(ValenceZone::Positive, ArousalZone::Low) => Vocalization::Purr,
(ValenceZone::Neutral, ArousalZone::Low) => Vocalization::Rumble,
(ValenceZone::Negative, ArousalZone::Mid) => Vocalization::Growl,
(ValenceZone::Positive, ArousalZone::Mid) => Vocalization::Howl,
(ValenceZone::Neutral, ArousalZone::Mid) => Vocalization::Whine,
}
}
fn select_intent(&self) -> CallIntent {
match (self.valence_zone(), self.arousal_zone()) {
(ValenceZone::Negative, ArousalZone::High) => {
if self.valence < -0.7 {
CallIntent::Distress
} else {
CallIntent::Alarm
}
}
(ValenceZone::Positive, ArousalZone::High) => CallIntent::Mating,
(ValenceZone::Neutral, ArousalZone::High) => CallIntent::Alarm,
(ValenceZone::Negative, ArousalZone::Low) => CallIntent::Threat,
(ValenceZone::Positive, ArousalZone::Low) => CallIntent::Idle,
(ValenceZone::Neutral, ArousalZone::Low) => CallIntent::Idle,
(ValenceZone::Negative, ArousalZone::Mid) => CallIntent::Threat,
(ValenceZone::Positive, ArousalZone::Mid) => CallIntent::Social,
(ValenceZone::Neutral, ArousalZone::Mid) => CallIntent::Social,
}
}
fn valence_zone(&self) -> ValenceZone {
if self.valence < -0.2 {
ValenceZone::Negative
} else if self.valence > 0.2 {
ValenceZone::Positive
} else {
ValenceZone::Neutral
}
}
fn arousal_zone(&self) -> ArousalZone {
if self.arousal < 0.33 {
ArousalZone::Low
} else if self.arousal > 0.66 {
ArousalZone::High
} else {
ArousalZone::Mid
}
}
}
impl Default for EmotionState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
enum ValenceZone {
Negative,
Neutral,
Positive,
}
#[derive(Debug, Clone, Copy)]
enum ArousalZone {
Low,
Mid,
High,
}