1use kira::{
31 manager::{AudioManager, AudioManagerSettings, backend::DefaultBackend},
32 sound::static_sound::{StaticSoundData, StaticSoundHandle},
33 tween::Tween,
34 Volume,
35};
36use std::path::{Path, PathBuf};
37use std::sync::{Arc, Mutex};
38use std::time::Duration;
39use rand::Rng;
40
41pub fn bundled_assets_path() -> PathBuf {
46 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
47 .join("assets")
48 .join("audio")
49 .join("voice")
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum VoiceType {
55 F1, F2, F3, F4,
56 M1, M2, M3, M4,
57}
58
59impl VoiceType {
60 fn filename(&self) -> &'static str {
61 match self {
62 VoiceType::F1 => "f1.ogg",
63 VoiceType::F2 => "f2.ogg",
64 VoiceType::F3 => "f3.ogg",
65 VoiceType::F4 => "f4.ogg",
66 VoiceType::M1 => "m1.ogg",
67 VoiceType::M2 => "m2.ogg",
68 VoiceType::M3 => "m3.ogg",
69 VoiceType::M4 => "m4.ogg",
70 }
71 }
72}
73
74#[derive(Debug, Clone)]
76pub struct VoiceProfile {
77 pub voice_type: VoiceType,
78 pub pitch_shift: f32, pub pitch_variation: f32, pub volume: f32, pub intonation: f32, }
83
84impl Default for VoiceProfile {
85 fn default() -> Self {
86 Self {
87 voice_type: VoiceType::F1,
88 pitch_shift: 0.0,
89 pitch_variation: 0.8,
90 volume: 0.65,
91 intonation: 0.0,
92 }
93 }
94}
95
96fn letter_to_sprite_time(c: char) -> Option<Duration> {
99 let c = c.to_ascii_lowercase();
100 if !c.is_ascii_lowercase() {
101 return None;
102 }
103
104 let index = (c as u32 - 'a' as u32) as u64;
105 Some(Duration::from_millis(index * 200))
106}
107
108fn special_to_sprite_time(name: &str) -> Option<Duration> {
110 match name {
111 "ok" => Some(Duration::from_millis(5200)),
112 "gwah" => Some(Duration::from_millis(5800)),
113 "deska" => Some(Duration::from_millis(6400)),
114 _ => None,
115 }
116}
117
118fn sfx_to_sprite_time(name: &str) -> Option<Duration> {
120 let index = match name {
121 "backspace" => 0,
122 "enter" => 1,
123 "tab" => 2,
124 "question" => 3,
125 "exclamation" => 4,
126 "at" => 5,
127 "pound" => 6,
128 "dollar" => 7,
129 "caret" => 8,
130 "ampersand" => 9,
131 "asterisk" => 10,
132 "parenthesis_open" => 11,
133 "parenthesis_closed" => 12,
134 "bracket_open" => 13,
135 "bracket_closed" => 14,
136 "brace_open" => 15,
137 "brace_closed" => 16,
138 "tilde" => 17,
139 "default" => 18,
140 "arrow_left" => 19,
141 "arrow_up" => 20,
142 "arrow_right" => 21,
143 "arrow_down" => 22,
144 "slash_forward" => 23,
145 "slash_back" => 24,
146 "percent" => 25,
147 _ => return None,
148 };
149 Some(Duration::from_millis(index * 600))
150}
151
152fn semitones_to_rate(semitones: f32) -> f32 {
155 2.0_f32.powf(semitones / 12.0)
156}
157
158pub struct Animalese {
160 manager: Arc<Mutex<AudioManager>>,
161 voice_path: String,
162 sfx_path: String,
163 profile: Arc<Mutex<VoiceProfile>>,
164 active_sounds: Arc<Mutex<Vec<StaticSoundHandle>>>,
165}
166
167impl Animalese {
168 pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
178 Self::with_custom_assets(bundled_assets_path().to_string_lossy().to_string())
179 }
180
181 pub fn with_custom_assets(assets_path: impl Into<String>) -> Result<Self, Box<dyn std::error::Error>> {
196 let voice_path = assets_path.into();
197
198 let sfx_path = Path::new(&voice_path)
200 .parent()
201 .ok_or("Invalid assets path")?
202 .join("sfx.ogg")
203 .to_string_lossy()
204 .to_string();
205
206 let manager = AudioManager::<DefaultBackend>::new(AudioManagerSettings::default())?;
208
209 let profile = Arc::new(Mutex::new(VoiceProfile::default()));
210 let active_sounds = Arc::new(Mutex::new(Vec::new()));
211
212 Ok(Self {
213 manager: Arc::new(Mutex::new(manager)),
214 voice_path,
215 sfx_path,
216 profile,
217 active_sounds,
218 })
219 }
220
221 pub fn set_profile(&mut self, new_profile: VoiceProfile) {
223 if let Ok(mut profile) = self.profile.lock() {
224 *profile = new_profile;
225 }
226 }
227
228 pub fn profile(&self) -> VoiceProfile {
230 self.profile.lock().unwrap().clone()
231 }
232
233 pub fn play_letter(&self, c: char) -> Result<(), Box<dyn std::error::Error>> {
235 self.play_letter_with_duration(c, None)
236 }
237
238 pub fn play_letter_with_duration(&self, c: char, max_duration: Option<Duration>) -> Result<(), Box<dyn std::error::Error>> {
240 self.play_letter_with_options(c, max_duration, 0.0)
241 }
242
243 fn play_letter_with_options(&self, c: char, max_duration: Option<Duration>, intonation_shift: f32) -> Result<(), Box<dyn std::error::Error>> {
245 let sprite_time = letter_to_sprite_time(c)
246 .ok_or("Not a valid letter")?;
247
248 self.play_sprite(&self.voice_path, sprite_time, Duration::from_millis(200), true, max_duration, intonation_shift)
249 }
250
251 pub fn play_special(&self, name: &str) -> Result<(), Box<dyn std::error::Error>> {
253 let sprite_time = special_to_sprite_time(name)
254 .ok_or("Unknown special sound")?;
255
256 self.play_sprite(&self.voice_path, sprite_time, Duration::from_millis(600), true, None, 0.0)
257 }
258
259 pub fn play_sfx(&self, name: &str) -> Result<(), Box<dyn std::error::Error>> {
261 let sprite_time = sfx_to_sprite_time(name)
262 .ok_or("Unknown SFX sound")?;
263
264 self.play_sprite(&self.sfx_path, sprite_time, Duration::from_millis(600), false, None, 0.0)
265 }
266
267 pub fn speak(&self, text: &str) -> Result<(), Box<dyn std::error::Error>> {
269 let profile = self.profile.lock().unwrap();
270 let base_intonation = profile.intonation;
271 drop(profile);
272
273 let has_question = text.trim_end().ends_with('?');
275 let intonation = if has_question && base_intonation == 0.0 {
276 0.5 } else {
278 base_intonation
279 };
280
281 let letters: Vec<char> = text.chars().filter(|c| c.is_ascii_alphabetic()).collect();
283 let total_letters = letters.len() as f32;
284
285 if total_letters == 0.0 {
286 return Ok(());
287 }
288
289 let mut letter_index = 0.0;
290 for c in text.chars() {
291 if c.is_ascii_alphabetic() {
292 let position = letter_index / total_letters;
294
295 let intonation_shift = intonation * position * 3.0; self.play_letter_with_options(c, None, intonation_shift)?;
301 letter_index += 1.0;
302
303 std::thread::sleep(Duration::from_millis(50));
305 }
306 }
307 Ok(())
308 }
309
310 pub fn speak_question(&self, text: &str) -> Result<(), Box<dyn std::error::Error>> {
323 let original_intonation = {
325 let mut profile = self.profile.lock().unwrap();
326 let original = profile.intonation;
327 profile.intonation = 0.6; original
329 };
330
331 let result = self.speak(text);
332
333 if let Ok(mut profile) = self.profile.lock() {
335 profile.intonation = original_intonation;
336 }
337
338 result
339 }
340
341 pub fn speak_excited(&self, text: &str) -> Result<(), Box<dyn std::error::Error>> {
354 let (original_pitch, original_intonation) = {
356 let mut profile = self.profile.lock().unwrap();
357 let orig_pitch = profile.pitch_shift;
358 let orig_intonation = profile.intonation;
359 profile.pitch_shift += 2.0; profile.intonation = 0.4; (orig_pitch, orig_intonation)
362 };
363
364 let result = self.speak(text);
365
366 if let Ok(mut profile) = self.profile.lock() {
368 profile.pitch_shift = original_pitch;
369 profile.intonation = original_intonation;
370 }
371
372 result
373 }
374
375 pub fn speak_statement(&self, text: &str) -> Result<(), Box<dyn std::error::Error>> {
388 let original_intonation = {
390 let mut profile = self.profile.lock().unwrap();
391 let original = profile.intonation;
392 profile.intonation = -0.3; original
394 };
395
396 let result = self.speak(text);
397
398 if let Ok(mut profile) = self.profile.lock() {
400 profile.intonation = original_intonation;
401 }
402
403 result
404 }
405
406 fn play_sprite(&self, audio_path: &str, start: Duration, duration: Duration, apply_pitch: bool, max_duration: Option<Duration>, intonation_shift: f32) -> Result<(), Box<dyn std::error::Error>> {
408 let file_path = if audio_path.ends_with(".ogg") {
410 audio_path.to_string()
412 } else {
413 let profile = self.profile.lock().unwrap();
415 let filename = profile.voice_type.filename();
416 Path::new(audio_path).join(filename)
417 .to_string_lossy()
418 .to_string()
419 };
420
421 let actual_duration = max_duration.unwrap_or(duration);
423
424 let start_time = start.as_secs_f64();
426 let end_time = start_time + actual_duration.as_secs_f64();
427 let mut sound_data = StaticSoundData::from_file(&file_path)?
428 .slice(start_time..end_time);
429
430 if apply_pitch {
431 let profile = self.profile.lock().unwrap();
432 let mut rng = rand::thread_rng();
433 let random_variation = rng.gen_range(-1.0..=1.0) * profile.pitch_variation;
434 let final_pitch = profile.pitch_shift + random_variation + intonation_shift;
435 let playback_rate = semitones_to_rate(final_pitch);
436 let volume = profile.volume;
437
438 sound_data = sound_data
440 .playback_rate(playback_rate as f64)
441 .volume(Volume::Amplitude(volume as f64))
442 .fade_in_tween(Tween {
443 duration: std::time::Duration::from_millis(5),
444 ..Default::default()
445 });
446 } else {
447 let profile = self.profile.lock().unwrap();
448 let volume = profile.volume;
449
450 sound_data = sound_data
451 .volume(Volume::Amplitude(volume as f64))
452 .fade_in_tween(Tween {
453 duration: std::time::Duration::from_millis(5),
454 ..Default::default()
455 });
456 }
457
458 let mut manager = self.manager.lock().unwrap();
460 let handle = manager.play(sound_data)?;
461
462 let mut active = self.active_sounds.lock().unwrap();
464 active.push(handle);
465
466 active.retain(|h| h.state() != kira::sound::PlaybackState::Stopped);
468
469 Ok(())
470 }
471
472 pub fn stop(&self) {
474 let mut active = self.active_sounds.lock().unwrap();
475 for handle in active.iter_mut() {
476 let _ = handle.stop(Tween::default());
477 }
478 active.clear();
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485 use std::time::Duration;
486
487 #[test]
488 fn test_letter_to_sprite_time() {
489 assert_eq!(letter_to_sprite_time('a'), Some(Duration::from_millis(0)));
490 assert_eq!(letter_to_sprite_time('b'), Some(Duration::from_millis(200)));
491 assert_eq!(letter_to_sprite_time('z'), Some(Duration::from_millis(5000)));
492 assert_eq!(letter_to_sprite_time('A'), Some(Duration::from_millis(0)));
493 assert_eq!(letter_to_sprite_time('1'), None);
494 }
495
496 #[test]
497 fn test_semitones_to_rate() {
498 assert!((semitones_to_rate(0.0) - 1.0).abs() < 0.001);
499 assert!((semitones_to_rate(12.0) - 2.0).abs() < 0.001);
500 assert!((semitones_to_rate(-12.0) - 0.5).abs() < 0.001);
501 }
502
503 #[test]
504 fn test_special_sounds() {
505 assert_eq!(special_to_sprite_time("ok"), Some(Duration::from_millis(5200)));
506 assert_eq!(special_to_sprite_time("gwah"), Some(Duration::from_millis(5800)));
507 assert_eq!(special_to_sprite_time("deska"), Some(Duration::from_millis(6400)));
508 assert_eq!(special_to_sprite_time("unknown"), None);
509 }
510
511 #[test]
512 fn test_voice_profile_default() {
513 let profile = VoiceProfile::default();
514 assert_eq!(profile.voice_type, VoiceType::F1);
515 assert_eq!(profile.pitch_shift, 0.0);
516 assert_eq!(profile.pitch_variation, 0.8);
517 assert_eq!(profile.volume, 0.65);
518 assert_eq!(profile.intonation, 0.0);
519 }
520
521 #[test]
522 fn test_intonation_values() {
523 let mut profile = VoiceProfile::default();
524
525 profile.intonation = 0.5;
527 assert_eq!(profile.intonation, 0.5);
528
529 profile.intonation = -0.5;
530 assert_eq!(profile.intonation, -0.5);
531
532 profile.intonation = 1.0;
533 assert_eq!(profile.intonation, 1.0);
534
535 profile.intonation = -1.0;
536 assert_eq!(profile.intonation, -1.0);
537 }
538}