use serde::{Deserialize, Serialize};
use crate::world::{Entity, World};
pub use svara::formant;
pub use svara::glottal;
pub use svara::lod as svara_lod;
pub use svara::phoneme;
pub use svara::pool as svara_pool;
pub use svara::prosody;
pub use svara::render as svara_render;
pub use svara::sequence;
pub use svara::spectral as svara_spectral;
pub use svara::tract;
pub use svara::trajectory;
pub use svara::voice as svara_voice;
pub use shabda::engine as g2p_engine;
pub use shabda::heteronym;
pub use shabda::normalize;
pub use shabda::prosody as g2p_prosody;
pub use shabda::rules as g2p_rules;
pub use shabda::ssml;
pub use shabda::syllable;
pub use prani::emotion as creature_emotion;
pub use prani::fatigue;
pub use prani::preset;
pub use prani::sequence as creature_sequence;
pub use prani::species;
pub use prani::tract as creature_tract;
pub use prani::vocalization;
pub use prani::voice as creature_voice;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoiceSource {
pub profile_name: String,
pub rate: f32,
pub pitch_shift: f32,
pub volume: f32,
#[serde(skip)]
pub speaking: bool,
}
impl VoiceSource {
pub fn new(profile_name: impl Into<String>) -> Self {
Self {
profile_name: profile_name.into(),
rate: 1.0,
pitch_shift: 0.0,
volume: 1.0,
speaking: false,
}
}
pub fn with_rate(mut self, rate: f32) -> Self {
self.rate = rate;
self
}
pub fn with_pitch_shift(mut self, semitones: f32) -> Self {
self.pitch_shift = semitones;
self
}
pub fn with_volume(mut self, volume: f32) -> Self {
self.volume = volume;
self
}
}
impl Default for VoiceSource {
fn default() -> Self {
Self::new("default")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreatureVoiceSource {
pub species_name: String,
pub arousal: f32,
pub fatigue: f32,
pub volume: f32,
#[serde(skip)]
pub vocalizing: bool,
}
impl CreatureVoiceSource {
pub fn new(species_name: impl Into<String>) -> Self {
Self {
species_name: species_name.into(),
arousal: 0.0,
fatigue: 0.0,
volume: 1.0,
vocalizing: false,
}
}
pub fn with_arousal(mut self, arousal: f32) -> Self {
self.arousal = arousal.clamp(0.0, 1.0);
self
}
pub fn with_fatigue(mut self, fatigue: f32) -> Self {
self.fatigue = fatigue.clamp(0.0, 1.0);
self
}
pub fn with_volume(mut self, volume: f32) -> Self {
self.volume = volume;
self
}
}
#[derive(Debug, Clone)]
pub struct SpeechRequest {
pub entity: Entity,
pub text: String,
}
impl SpeechRequest {
pub fn new(entity: Entity, text: impl Into<String>) -> Self {
Self {
entity,
text: text.into(),
}
}
}
#[derive(Debug, Clone)]
pub struct VocalizeRequest {
pub entity: Entity,
pub intent: String,
}
impl VocalizeRequest {
pub fn new(entity: Entity, intent: impl Into<String>) -> Self {
Self {
entity,
intent: intent.into(),
}
}
}
pub fn process_speech_requests(world: &mut World) {
let requests = {
let Some(bus) = world.get_resource_mut::<crate::world::EventBus>() else {
return;
};
bus.drain::<SpeechRequest>()
};
let count = requests.len();
for req in requests {
if let Some(voice) = world.get_component_mut::<VoiceSource>(req.entity) {
voice.speaking = true;
}
}
if count > 0 {
tracing::info!(count, "processed speech requests");
}
}
pub fn process_vocalize_requests(world: &mut World) {
let requests = {
let Some(bus) = world.get_resource_mut::<crate::world::EventBus>() else {
return;
};
bus.drain::<VocalizeRequest>()
};
let count = requests.len();
for req in requests {
if let Some(voice) = world.get_component_mut::<CreatureVoiceSource>(req.entity) {
voice.vocalizing = true;
}
}
if count > 0 {
tracing::info!(count, "processed vocalize requests");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::world::EventBus;
#[test]
fn voice_source_builder() {
let v = VoiceSource::new("narrator")
.with_rate(1.2)
.with_pitch_shift(-2.0)
.with_volume(0.8);
assert_eq!(v.profile_name, "narrator");
assert_eq!(v.rate, 1.2);
assert_eq!(v.pitch_shift, -2.0);
assert_eq!(v.volume, 0.8);
assert!(!v.speaking);
}
#[test]
fn voice_source_default() {
let v = VoiceSource::default();
assert_eq!(v.profile_name, "default");
assert_eq!(v.rate, 1.0);
}
#[test]
fn creature_voice_builder() {
let v = CreatureVoiceSource::new("wolf")
.with_arousal(0.7)
.with_fatigue(0.3)
.with_volume(0.9);
assert_eq!(v.species_name, "wolf");
assert_eq!(v.arousal, 0.7);
assert_eq!(v.fatigue, 0.3);
assert!(!v.vocalizing);
}
#[test]
fn creature_voice_clamps() {
let v = CreatureVoiceSource::new("bird")
.with_arousal(5.0)
.with_fatigue(-1.0);
assert_eq!(v.arousal, 1.0);
assert_eq!(v.fatigue, 0.0);
}
#[test]
fn speech_request_system() {
let mut world = World::new();
world.insert_resource(EventBus::new());
let npc = world.spawn();
world
.insert_component(npc, VoiceSource::new("guard"))
.unwrap();
{
let bus = world.get_resource_mut::<EventBus>().unwrap();
bus.publish(SpeechRequest::new(npc, "Halt!"));
}
process_speech_requests(&mut world);
let voice = world.get_component::<VoiceSource>(npc).unwrap();
assert!(voice.speaking);
}
#[test]
fn vocalize_request_system() {
let mut world = World::new();
world.insert_resource(EventBus::new());
let creature = world.spawn();
world
.insert_component(creature, CreatureVoiceSource::new("wolf"))
.unwrap();
{
let bus = world.get_resource_mut::<EventBus>().unwrap();
bus.publish(VocalizeRequest::new(creature, "howl"));
}
process_vocalize_requests(&mut world);
let voice = world
.get_component::<CreatureVoiceSource>(creature)
.unwrap();
assert!(voice.vocalizing);
}
#[test]
fn voice_as_component() {
let mut world = World::new();
let e = world.spawn();
world.insert_component(e, VoiceSource::new("bard")).unwrap();
assert!(world.has_component::<VoiceSource>(e));
}
#[test]
fn creature_voice_as_component() {
let mut world = World::new();
let e = world.spawn();
world
.insert_component(e, CreatureVoiceSource::new("cat"))
.unwrap();
assert!(world.has_component::<CreatureVoiceSource>(e));
}
}