1use std::cell::RefCell;
12use std::time::{Duration, Instant};
13
14use crate::audio::AudioSink;
15use crate::chunk::{chunk, refit, Chunk};
16use crate::cleanup::clean;
17use crate::config::Config;
18use crate::queue::{Policy, Queue, Source, Utterance};
19use crate::synth::Synthesizer;
20
21pub const SECONDS_PER_WORD: f64 = 0.365;
23
24#[derive(Copy, Clone, Debug, PartialEq, Eq)]
25pub enum State {
26 Idle,
27 Speaking,
28 Paused,
29 Error,
30}
31
32#[derive(Clone, Debug, Default)]
33pub struct SayOpts {
34 pub policy: Option<Policy>,
35 pub voice: Option<String>,
36 pub speed: Option<f32>,
37 pub source: Source,
39}
40
41#[derive(Clone, Debug)]
42pub enum Command {
43 Say { text: String, opts: SayOpts },
44 Pause,
45 Resume,
46 PlayPause,
47 Stop,
48 Next,
49 SkipSentence,
50 ClearQueue,
51 Cancel(u64),
52 SetMuted(bool),
53 SetVoice(String),
54 SetSpeed(f32),
55 Shutdown,
56}
57
58#[derive(Clone, Debug, PartialEq)]
59pub struct Snapshot {
60 pub state: State,
61 pub muted: bool,
62 pub voice: String,
63 pub speed: f32,
64 pub queue_len: usize,
65 pub remaining_secs: f64,
66 pub current_text: String,
67 pub current_id: u64,
68 pub error: Option<String>,
69}
70
71struct Current {
73 id: u64,
74 text: String,
75 voice: String,
76 speed: f32,
77 chunks: Vec<Chunk>,
78 next_chunk: usize,
79 carry: Vec<f32>,
81}
82
83pub struct Engine {
84 cfg: Config,
85 synth: Box<dyn Synthesizer>,
86 sink: Box<dyn AudioSink>,
87 queue: Queue,
88 current: Option<Current>,
89 state: State,
90 error: Option<String>,
107 idle_since: Option<Instant>,
108 shutdown: bool,
109}
110
111impl Engine {
112 pub fn new(cfg: Config, synth: Box<dyn Synthesizer>, sink: Box<dyn AudioSink>) -> Self {
113 Engine {
114 cfg,
115 synth,
116 sink,
117 queue: Queue::new(),
118 current: None,
119 state: State::Idle,
120 error: None,
121 idle_since: Some(Instant::now()),
122 shutdown: false,
123 }
124 }
125
126 pub fn is_shutdown(&self) -> bool {
127 self.shutdown
128 }
129
130 pub fn handle(&mut self, cmd: Command) {
131 match cmd {
132 Command::Say { text, opts } => {
133 let _ = self.submit(text, opts);
134 }
135 Command::Pause => {
136 if self.state == State::Speaking {
137 self.state = State::Paused;
138 self.sink.set_paused(true);
139 }
140 }
141 Command::Resume => {
142 if self.state == State::Paused {
143 self.state = State::Speaking;
144 self.sink.set_paused(false);
145 }
146 }
147 Command::PlayPause => match self.state {
148 State::Speaking => self.handle(Command::Pause),
149 State::Paused => self.handle(Command::Resume),
150 _ => {}
151 },
152 Command::Stop => {
153 self.queue.clear();
158 self.discard_current();
159 self.dismiss_error_and_go_idle();
160 }
161 Command::Next => {
162 self.discard_current();
163 if self.queue.is_empty() {
164 self.dismiss_error_and_go_idle();
165 }
166 }
167 Command::SkipSentence => {
168 self.sink.clear();
169 if let Some(c) = self.current.as_mut() {
170 c.carry.clear();
171 if c.next_chunk >= c.chunks.len() {
172 self.current = None;
173 if self.queue.is_empty() {
174 self.dismiss_error_and_go_idle();
175 }
176 }
177 } else if self.queue.is_empty() {
178 self.dismiss_error_and_go_idle();
179 }
180 }
181 Command::ClearQueue => {
182 self.queue.clear();
183 }
184 Command::Cancel(id) => {
185 self.queue.cancel(id);
186 }
187 Command::SetMuted(m) => {
188 self.cfg.muted = m;
189 if m {
190 self.queue.clear();
191 self.discard_current();
192 self.dismiss_error_and_go_idle();
193 }
194 }
195 Command::SetVoice(v) => self.cfg.voice = v,
196 Command::SetSpeed(s) => self.cfg.speed = s.clamp(0.5, 2.0),
197 Command::Shutdown => {
198 self.shutdown = true;
199 self.handle(Command::Stop);
200 }
201 }
202 }
203
204 pub fn submit(&mut self, text: String, opts: SayOpts) -> Result<Option<u64>, String> {
222 if text.chars().count() > self.cfg.max_chars {
223 let msg = format!(
224 "text is {} characters, limit is {}",
225 text.chars().count(),
226 self.cfg.max_chars
227 );
228 if self.state != State::Speaking && self.state != State::Paused {
234 self.state = State::Error;
235 self.error = Some(msg.clone());
236 }
237 return Err(msg);
238 }
239 if self.state == State::Error {
240 self.state = State::Idle;
244 self.error = None;
245 }
246 if self.cfg.muted {
247 return Ok(None); }
249
250 let cleaned = clean(&text, &self.cfg.cleanup);
251 if cleaned.trim().is_empty() {
252 return Ok(None);
253 }
254
255 let policy = opts.policy.unwrap_or_else(|| opts.source.default_policy());
256 let id = self.queue.next_id();
257 let u = Utterance {
258 id,
259 text: cleaned,
260 voice: opts.voice.unwrap_or_else(|| self.cfg.voice.clone()),
261 speed: opts.speed.unwrap_or(self.cfg.speed),
262 source: opts.source,
263 };
264 self.queue.submit(u, policy);
265
266 match policy {
267 Policy::Replace | Policy::Interrupt => self.discard_current(),
268 _ => {}
269 }
270
271 if self.state != State::Paused {
272 self.state = State::Speaking;
273 self.idle_since = None;
274 }
275
276 Ok(Some(id))
277 }
278
279 pub fn tick(&mut self) {
281 if let Some(e) = self.sink.take_error() {
296 self.state = State::Error;
297 self.error = Some(e);
298 self.current = None;
299 self.queue.clear();
300 self.sink.set_paused(false);
307 return;
308 }
309
310 if self.state == State::Paused {
311 return;
312 }
313
314 if let Some(c) = self.current.as_mut() {
316 if !c.carry.is_empty() {
317 let n = self.sink.push(&c.carry);
318 c.carry.drain(..n);
319 if !c.carry.is_empty() {
320 return; }
322 }
323 }
324
325 if self.current.is_none() {
327 match self.queue.pop_front() {
328 Some(u) => {
329 let voice = u.voice.clone();
344 let cs = chunk(&u.text, self.cfg.chunking.target_chars);
345 let synth = RefCell::new(&mut self.synth);
346 let cs = refit(cs, |t| {
347 let mut synth = synth.borrow_mut();
348 let ph = synth.phonemize(t, &voice);
349 synth.fits(&ph)
350 });
351 self.current = Some(Current {
352 id: u.id,
353 text: u.text,
354 voice: u.voice,
355 speed: u.speed,
356 chunks: cs,
357 next_chunk: 0,
358 carry: Vec::new(),
359 });
360 self.state = State::Speaking;
361 self.error = None;
362 self.idle_since = None;
363 }
364 None => {
365 if self.state != State::Error {
366 self.go_idle();
367 }
368 self.maybe_unload();
369 return;
370 }
371 }
372 }
373
374 let headroom = self.sink.capacity().saturating_sub(self.sink.pending());
381 let divisor = self.cfg.chunking.lookahead_chunks.saturating_add(1).max(2);
382 if headroom < self.sink.capacity() / divisor {
383 return;
384 }
385
386 let Some(c) = self.current.as_mut() else { return };
387 if c.next_chunk >= c.chunks.len() {
388 self.current = None;
389 if self.queue.is_empty() {
390 self.go_idle();
391 }
392 return;
393 }
394
395 let text = c.chunks[c.next_chunk].text.clone();
396 let voice = c.voice.clone();
397 let speed = c.speed;
398 c.next_chunk += 1;
399
400 let phonemes = self.synth.phonemize(&text, &voice);
401 match self.synth.synth(&phonemes, &voice, speed) {
402 Ok(samples) => {
403 let n = self.sink.push(&samples);
404 if n < samples.len() {
405 if let Some(c) = self.current.as_mut() {
406 c.carry = samples[n..].to_vec();
407 }
408 }
409 }
410 Err(e) => {
411 self.state = State::Error;
412 self.error = Some(e);
413 self.current = None;
414 self.queue.clear();
415 }
416 }
417 }
418
419 pub fn replace_sink(&mut self, sink: Box<dyn AudioSink>) {
426 self.sink = sink;
427 self.sink.set_paused(false);
433 self.current = None;
434 self.error = None;
435 self.state = State::Idle;
436 self.idle_since = Some(Instant::now());
437 }
438
439 fn go_idle(&mut self) {
452 if self.state != State::Error {
453 if self.sink.pending() > 0 {
454 self.state = State::Speaking;
461 return;
462 }
463 self.state = State::Idle;
464 }
465 if self.idle_since.is_none() {
466 self.idle_since = Some(Instant::now());
467 }
468 }
469
470 fn dismiss_error_and_go_idle(&mut self) {
487 self.state = State::Idle;
488 self.error = None;
489 self.sink.set_paused(false);
490 if self.idle_since.is_none() {
491 self.idle_since = Some(Instant::now());
492 }
493 }
494
495 fn discard_current(&mut self) {
500 self.current = None;
501 self.sink.clear();
502 }
503
504 fn maybe_unload(&mut self) {
505 if !self.synth.is_loaded() {
506 return;
507 }
508 let Some(since) = self.idle_since else { return };
509 if since.elapsed() >= Duration::from_secs(self.cfg.idle_unload_secs) {
510 self.synth.unload();
511 }
512 }
513
514 pub fn snapshot(&self) -> Snapshot {
515 Snapshot {
516 state: self.state,
517 muted: self.cfg.muted,
518 voice: self.cfg.voice.clone(),
519 speed: self.cfg.speed,
520 queue_len: self.queue.len(),
521 remaining_secs: self.remaining_secs(),
522 current_text: self.current.as_ref().map(|c| c.text.clone()).unwrap_or_default(),
523 current_id: self.current.as_ref().map(|c| c.id).unwrap_or(0),
524 error: self.error.clone(),
525 }
526 }
527
528 fn remaining_secs(&self) -> f64 {
533 let sr = self.synth.sample_rate() as f64;
534 let carried = self.current.as_ref().map(|c| c.carry.len()).unwrap_or(0) as f64;
535 let buffered = (self.sink.pending() as f64 + carried) / sr;
536
537 let mut words = 0usize;
538 if let Some(c) = self.current.as_ref() {
539 for ch in &c.chunks[c.next_chunk.min(c.chunks.len())..] {
540 words += ch.text.split_whitespace().count();
541 }
542 }
543 for u in self.queue.iter() {
544 words += u.text.split_whitespace().count();
545 }
546 let speed = self.cfg.speed.max(0.1) as f64;
547 buffered + (words as f64 * SECONDS_PER_WORD) / speed
548 }
549
550 #[cfg(test)]
553 fn audio_written(&self) -> usize {
554 self.sink.total_written()
555 }
556
557 #[cfg(test)]
558 fn is_model_loaded(&self) -> bool {
559 self.synth.is_loaded()
560 }
561
562 #[cfg(test)]
563 fn snapshot_queue_ids(&self) -> Vec<u64> {
564 self.queue.iter().map(|u| u.id).collect()
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use std::sync::{Arc, Mutex};
571
572 use super::*;
573 use crate::audio::{AudioSink, VecSink};
574 use crate::config::Config;
575 use crate::synth::StubSynthesizer;
576
577 fn engine() -> Engine {
578 Engine::new(
579 Config::default(),
580 Box::new(StubSynthesizer::new()),
581 Box::new(VecSink::new(24_000 * 10)),
582 )
583 }
584
585 #[derive(Clone)]
602 struct SharedVecSink(Arc<Mutex<VecSink>>);
603
604 impl AudioSink for SharedVecSink {
605 fn push(&mut self, samples: &[f32]) -> usize {
606 self.0.lock().unwrap().push(samples)
607 }
608 fn pending(&self) -> usize {
609 self.0.lock().unwrap().pending()
610 }
611 fn clear(&mut self) {
612 self.0.lock().unwrap().clear()
613 }
614 fn set_paused(&mut self, paused: bool) {
615 self.0.lock().unwrap().set_paused(paused)
616 }
617 fn is_paused(&self) -> bool {
618 self.0.lock().unwrap().is_paused()
619 }
620 fn capacity(&self) -> usize {
621 self.0.lock().unwrap().capacity()
622 }
623 fn total_written(&self) -> usize {
624 self.0.lock().unwrap().total_written()
625 }
626 }
627
628 fn engine_with_drainable_sink(capacity: usize) -> (Engine, Arc<Mutex<VecSink>>) {
631 let sink = Arc::new(Mutex::new(VecSink::new(capacity)));
632 let e = Engine::new(
633 Config::default(),
634 Box::new(StubSynthesizer::new()),
635 Box::new(SharedVecSink(sink.clone())),
636 );
637 (e, sink)
638 }
639
640 #[derive(Clone)]
650 struct FaultInjectableSink {
651 inner: Arc<Mutex<VecSink>>,
652 fault: Arc<Mutex<Option<String>>>,
653 }
654
655 impl FaultInjectableSink {
656 fn new(capacity: usize) -> Self {
657 FaultInjectableSink {
658 inner: Arc::new(Mutex::new(VecSink::new(capacity))),
659 fault: Arc::new(Mutex::new(None)),
660 }
661 }
662
663 fn inject_failure(&self, msg: &str) {
666 *self.fault.lock().unwrap() = Some(msg.into());
667 }
668 }
669
670 impl AudioSink for FaultInjectableSink {
671 fn push(&mut self, samples: &[f32]) -> usize {
672 self.inner.lock().unwrap().push(samples)
673 }
674 fn pending(&self) -> usize {
675 self.inner.lock().unwrap().pending()
676 }
677 fn clear(&mut self) {
678 self.inner.lock().unwrap().clear()
679 }
680 fn set_paused(&mut self, paused: bool) {
681 self.inner.lock().unwrap().set_paused(paused)
682 }
683 fn is_paused(&self) -> bool {
684 self.inner.lock().unwrap().is_paused()
685 }
686 fn capacity(&self) -> usize {
687 self.inner.lock().unwrap().capacity()
688 }
689 fn total_written(&self) -> usize {
690 self.inner.lock().unwrap().total_written()
691 }
692 fn take_error(&mut self) -> Option<String> {
693 self.fault.lock().unwrap().take()
694 }
695 }
696
697 fn say(text: &str) -> Command {
698 Command::Say { text: text.into(), opts: SayOpts::default() }
699 }
700
701 fn run(e: &mut Engine, max: usize) {
703 for _ in 0..max {
704 if e.snapshot().state == State::Idle {
705 return;
706 }
707 e.tick();
708 }
709 }
710
711 #[test]
712 fn starts_idle() {
713 let e = engine();
714 let s = e.snapshot();
715 assert_eq!(s.state, State::Idle);
716 assert_eq!(s.queue_len, 0);
717 assert_eq!(s.error, None);
718 }
719
720 #[test]
721 fn say_moves_to_speaking_and_produces_audio() {
722 let mut e = engine();
723 e.handle(say("Hello there. This is a test."));
724 e.tick();
725 assert_eq!(e.snapshot().state, State::Speaking);
726 run(&mut e, 500);
727 assert!(e.audio_written() > 0, "expected samples to reach the sink");
728 }
729
730 #[test]
731 fn returns_to_idle_when_the_queue_empties() {
732 let (mut e, sink) = engine_with_drainable_sink(24_000 * 10);
741 e.handle(say("Short."));
742 run(&mut e, 500);
743 sink.lock().unwrap().drain(usize::MAX);
744 e.tick();
745 assert_eq!(e.snapshot().state, State::Idle);
746 }
747
748 #[test]
749 fn state_stays_speaking_while_audio_is_still_pending_in_the_sink() {
750 let (mut e, sink) = engine_with_drainable_sink(24_000 * 10);
754 e.handle(say("Hello there. This is sayd speaking from the engine."));
755 run(&mut e, 500);
756
757 let pending = sink.lock().unwrap().pending();
758 assert!(pending > 0, "test is only meaningful with audio still buffered");
759 let s = e.snapshot();
760 assert_eq!(
761 s.state,
762 State::Speaking,
763 "must not report Idle with {pending} samples still unplayed"
764 );
765 assert!(
766 s.remaining_secs > 0.0,
767 "remaining_secs must agree with state: both say audio is still outstanding"
768 );
769
770 sink.lock().unwrap().drain(usize::MAX);
771 e.tick();
772 assert_eq!(e.snapshot().state, State::Idle, "must go Idle once the sink actually drains");
773 }
774
775 #[test]
776 fn paused_engine_with_pending_audio_does_not_go_idle_or_spin() {
777 let (mut e, sink) = engine_with_drainable_sink(24_000 * 10);
783 e.handle(say("Hello there. This is a reasonably long test sentence."));
784 run(&mut e, 500);
785 assert_eq!(e.snapshot().state, State::Speaking);
786 let pending_before = sink.lock().unwrap().pending();
787 assert!(pending_before > 0, "test is only meaningful with audio still buffered");
788
789 e.handle(Command::Pause);
790 assert_eq!(e.snapshot().state, State::Paused);
791
792 for _ in 0..200 {
793 e.tick();
794 }
795
796 let s = e.snapshot();
797 assert_eq!(s.state, State::Paused, "must not spuriously become Idle while paused");
798 assert_eq!(
799 sink.lock().unwrap().pending(),
800 pending_before,
801 "a paused sink must not drain, and tick must not touch it while paused"
802 );
803 }
804
805 #[test]
806 fn pause_and_resume_toggle_state() {
807 let mut e = engine();
808 e.handle(say("Hello there. This is a test."));
809 e.tick();
810 e.handle(Command::Pause);
811 assert_eq!(e.snapshot().state, State::Paused);
812 e.handle(Command::Resume);
813 assert_eq!(e.snapshot().state, State::Speaking);
814 }
815
816 #[test]
817 fn play_pause_toggles_both_ways() {
818 let mut e = engine();
819 e.handle(say("Hello there."));
820 e.tick();
821 e.handle(Command::PlayPause);
822 assert_eq!(e.snapshot().state, State::Paused);
823 e.handle(Command::PlayPause);
824 assert_eq!(e.snapshot().state, State::Speaking);
825 }
826
827 #[test]
828 fn pause_when_idle_is_a_no_op() {
829 let mut e = engine();
830 e.handle(Command::Pause);
831 assert_eq!(e.snapshot().state, State::Idle);
832 }
833
834 #[test]
835 fn stop_clears_the_queue_and_goes_idle() {
836 let mut e = engine();
837 e.handle(say("First one here."));
838 e.handle(say("Second one here."));
839 e.tick();
840 e.handle(Command::Stop);
841 let s = e.snapshot();
842 assert_eq!(s.state, State::Idle);
843 assert_eq!(s.queue_len, 0, "Stop is the shut-up verb: it clears everything");
844 }
845
846 #[test]
847 fn clear_queue_keeps_the_current_utterance() {
848 let mut e = engine();
849 e.handle(say("First one here."));
850 e.handle(say("Second one here."));
851 e.tick();
852 e.handle(Command::ClearQueue);
853 let s = e.snapshot();
854 assert_eq!(s.state, State::Speaking, "the current utterance survives");
855 assert_eq!(s.queue_len, 0);
856 }
857
858 #[test]
859 fn next_advances_to_the_following_utterance() {
860 let mut e = engine();
861 e.handle(say("First."));
862 e.handle(say("Second."));
863 e.tick();
864 let first = e.snapshot().current_id;
865 e.handle(Command::Next);
866 e.tick();
867 assert_ne!(e.snapshot().current_id, first);
868 }
869
870 #[test]
871 fn hotkey_source_replaces_by_default() {
872 let mut e = engine();
873 e.handle(say("First one here."));
874 e.handle(say("Second one here."));
875 e.tick();
876 e.handle(Command::Say {
877 text: "Selected text.".into(),
878 opts: SayOpts { source: Source::Hotkey, ..Default::default() },
879 });
880 assert_eq!(e.snapshot().queue_len, 1, "replace drops everything pending");
881 }
882
883 #[test]
884 fn explicit_policy_overrides_the_source_default() {
885 let mut e = engine();
886 e.handle(say("First one here."));
887 e.handle(Command::Say {
888 text: "Selected.".into(),
889 opts: SayOpts {
890 source: Source::Hotkey,
891 policy: Some(Policy::Enqueue),
892 ..Default::default()
893 },
894 });
895 assert_eq!(e.snapshot().queue_len, 2, "explicit enqueue beats the hotkey default");
896 }
897
898 #[test]
899 fn muted_accepts_and_discards() {
900 let mut e = engine();
901 e.handle(Command::SetMuted(true));
902 e.handle(say("Nobody hears this."));
903 run(&mut e, 100);
904 assert_eq!(e.audio_written(), 0, "muted must produce no audio");
905 assert_eq!(e.snapshot().state, State::Idle);
906 }
907
908 #[test]
909 fn text_over_max_chars_is_rejected() {
910 let cfg = Config { max_chars: 10, ..Config::default() };
911 let mut e = Engine::new(
912 cfg,
913 Box::new(StubSynthesizer::new()),
914 Box::new(VecSink::new(24_000)),
915 );
916 e.handle(say("this is definitely longer than ten characters"));
917 let s = e.snapshot();
918 assert_eq!(s.state, State::Error);
919 assert!(s.error.as_deref().unwrap_or("").contains("10"));
920 }
921
922 #[test]
923 fn a_later_successful_say_clears_the_error() {
924 let cfg = Config { max_chars: 10, ..Config::default() };
925 let mut e = Engine::new(
926 cfg,
927 Box::new(StubSynthesizer::new()),
928 Box::new(VecSink::new(24_000 * 10)),
929 );
930 e.handle(say("far too long to be accepted"));
931 assert_eq!(e.snapshot().state, State::Error);
932 e.handle(say("ok."));
933 assert_eq!(e.snapshot().error, None);
934 }
935
936 #[test]
937 fn stop_dismisses_a_stuck_error() {
938 let cfg = Config { max_chars: 5, ..Config::default() };
941 let mut e = Engine::new(
942 cfg,
943 Box::new(StubSynthesizer::new()),
944 Box::new(VecSink::new(24_000 * 10)),
945 );
946 e.handle(say("way too long for the limit"));
947 assert_eq!(e.snapshot().state, State::Error);
948 e.handle(Command::Stop);
949 let s = e.snapshot();
950 assert_eq!(s.state, State::Idle);
951 assert_eq!(s.error, None);
952 }
953
954 #[test]
955 fn next_dismisses_a_stuck_error_when_the_queue_is_empty() {
956 let cfg = Config { max_chars: 5, ..Config::default() };
957 let mut e = Engine::new(
958 cfg,
959 Box::new(StubSynthesizer::new()),
960 Box::new(VecSink::new(24_000 * 10)),
961 );
962 e.handle(say("way too long for the limit"));
963 assert_eq!(e.snapshot().state, State::Error);
964 e.handle(Command::Next);
965 let s = e.snapshot();
966 assert_eq!(s.state, State::Idle);
967 assert_eq!(s.error, None);
968 }
969
970 #[test]
971 fn a_stuck_error_survives_plain_ticking_with_no_command() {
972 let cfg = Config { max_chars: 5, ..Config::default() };
978 let mut e = Engine::new(
979 cfg,
980 Box::new(StubSynthesizer::new()),
981 Box::new(VecSink::new(24_000 * 10)),
982 );
983 e.handle(say("way too long for the limit"));
984 assert_eq!(e.snapshot().state, State::Error);
985 for _ in 0..20 {
986 e.tick();
987 }
988 let s = e.snapshot();
989 assert_eq!(s.state, State::Error, "no command was issued; the error must persist");
990 assert!(s.error.is_some());
991 }
992
993 #[test]
994 fn rejection_while_speaking_leaves_playback_untouched() {
995 let cfg = Config { max_chars: 5, ..Config::default() };
1000 let mut e = Engine::new(
1001 cfg,
1002 Box::new(StubSynthesizer::new()),
1003 Box::new(VecSink::new(24_000 * 10)),
1004 );
1005 e.handle(say("Hi.")); e.tick();
1007 let before = e.snapshot();
1008 assert_eq!(before.state, State::Speaking);
1009 let id = before.current_id;
1010
1011 let result = e.submit(
1012 "this one is definitely too long for the limit".into(),
1013 SayOpts::default(),
1014 );
1015
1016 assert!(
1017 result.as_ref().unwrap_err().contains('5'),
1018 "the rejection must still be observable: {result:?}"
1019 );
1020 let after = e.snapshot();
1021 assert_eq!(after.state, State::Speaking, "A must keep playing");
1022 assert_eq!(after.current_id, id, "A must not be disturbed");
1023 assert_eq!(after.error, None, "nothing about A is actually wrong");
1024 assert_eq!(after.queue_len, 0, "the rejected text must not be queued");
1025 }
1026
1027 #[test]
1028 fn rejection_while_paused_leaves_playback_untouched() {
1029 let cfg = Config { max_chars: 5, ..Config::default() };
1030 let mut e = Engine::new(
1031 cfg,
1032 Box::new(StubSynthesizer::new()),
1033 Box::new(VecSink::new(24_000 * 10)),
1034 );
1035 e.handle(say("Hi."));
1036 e.tick();
1037 e.handle(Command::Pause);
1038 assert_eq!(e.snapshot().state, State::Paused);
1039
1040 let result = e.submit(
1041 "this one is definitely too long for the limit".into(),
1042 SayOpts::default(),
1043 );
1044
1045 assert!(result.is_err());
1046 let after = e.snapshot();
1047 assert_eq!(after.state, State::Paused);
1048 assert_eq!(after.error, None);
1049 }
1050
1051 #[test]
1052 fn error_state_invariant_holds_after_every_command_from_every_state() {
1053 fn assert_invariants(e: &Engine, ctx: &str) {
1063 let s = e.snapshot();
1064 assert_eq!(
1065 s.error.is_some(),
1066 s.state == State::Error,
1067 "{ctx}: error={:?} state={:?}",
1068 s.error,
1069 s.state
1070 );
1071 assert!(
1072 s.state == State::Paused || !e.sink.is_paused(),
1073 "{ctx}: state={:?} but the sink is still paused",
1074 s.state
1075 );
1076 }
1077
1078 fn all_commands() -> Vec<Command> {
1079 vec![
1080 say("Something reasonably short."),
1081 Command::Pause,
1082 Command::Resume,
1083 Command::PlayPause,
1084 Command::Stop,
1085 Command::Next,
1086 Command::SkipSentence,
1087 Command::ClearQueue,
1088 Command::Cancel(1),
1089 Command::SetMuted(true),
1090 Command::SetMuted(false),
1091 Command::SetVoice("am_fenrir".into()),
1092 Command::SetSpeed(1.5),
1093 Command::Shutdown,
1094 ]
1095 }
1096
1097 fn build_idle() -> Engine {
1098 engine()
1099 }
1100 fn build_speaking() -> Engine {
1101 let mut e = engine();
1102 e.handle(say("Hello there. This keeps it busy for quite a while indeed."));
1103 e.tick();
1104 e
1105 }
1106 fn build_paused() -> Engine {
1107 let mut e = build_speaking();
1108 e.handle(Command::Pause);
1109 e
1110 }
1111 fn build_error() -> Engine {
1112 let cfg = Config { max_chars: 5, ..Config::default() };
1113 let mut e = Engine::new(
1114 cfg,
1115 Box::new(StubSynthesizer::new()),
1116 Box::new(VecSink::new(24_000 * 10)),
1117 );
1118 e.handle(say("way too long for the limit"));
1119 e
1120 }
1121 fn build_error_from_device_failure_while_paused() -> Engine {
1130 let sink = FaultInjectableSink::new(24_000 * 10);
1131 let mut e = Engine::new(
1132 Config::default(),
1133 Box::new(StubSynthesizer::new()),
1134 Box::new(sink.clone()),
1135 );
1136 e.handle(say("Hello there. This keeps it busy for quite a while indeed."));
1137 e.tick();
1138 e.handle(Command::Pause);
1139 sink.inject_failure("audio device disappeared");
1140 e.tick();
1141 e
1142 }
1143
1144 fn check_from(name: &str, build: fn() -> Engine) {
1145 for cmd in all_commands() {
1146 let mut e = build();
1147 assert_invariants(&e, &format!("before {name} -> {cmd:?}"));
1148 e.handle(cmd.clone());
1149 assert_invariants(&e, &format!("after {name} -> {cmd:?}"));
1150 }
1151 }
1152
1153 check_from("idle", build_idle);
1154 check_from("speaking", build_speaking);
1155 check_from("paused", build_paused);
1156 check_from("error", build_error);
1157 check_from("device_failed_while_paused", build_error_from_device_failure_while_paused);
1158 }
1159
1160 #[test]
1161 fn huge_lookahead_chunks_does_not_overflow() {
1162 let mut cfg = Config::default();
1167 cfg.chunking.lookahead_chunks = usize::MAX;
1168 let mut e = Engine::new(
1169 cfg,
1170 Box::new(StubSynthesizer::new()),
1171 Box::new(VecSink::new(24_000 * 10)),
1172 );
1173 e.handle(say("Hello there. This is a test."));
1174 for _ in 0..10 {
1175 e.tick();
1176 }
1177 assert!(e.audio_written() > 0, "expected samples to reach the sink");
1178 }
1179
1180 #[test]
1181 fn remaining_seconds_includes_audio_parked_in_carry() {
1182 let mut e = Engine::new(
1187 Config::default(),
1188 Box::new(StubSynthesizer::new()),
1189 Box::new(VecSink::new(100)),
1190 );
1191 e.handle(say("A reasonably long sentence to force a big carry remainder."));
1192 e.tick(); let s = e.snapshot();
1194 assert!(
1197 s.remaining_secs > 1.0,
1198 "carry must count toward remaining_secs, got {}",
1199 s.remaining_secs
1200 );
1201 }
1202
1203 #[test]
1204 fn remaining_seconds_scales_with_word_count() {
1205 let mut e = engine();
1206 e.handle(say("one two three four five six seven eight nine ten."));
1207 let s = e.snapshot();
1208 assert!(s.remaining_secs > 2.0, "got {}", s.remaining_secs);
1210 assert!(s.remaining_secs < 6.0, "got {}", s.remaining_secs);
1211 }
1212
1213 #[test]
1214 fn remaining_seconds_halves_at_double_speed() {
1215 let mut e = engine();
1216 e.handle(Command::SetSpeed(2.0));
1217 e.handle(say("one two three four five six seven eight nine ten."));
1218 let fast = e.snapshot().remaining_secs;
1219 let mut e2 = engine();
1220 e2.handle(say("one two three four five six seven eight nine ten."));
1221 let normal = e2.snapshot().remaining_secs;
1222 assert!(fast < normal * 0.75, "fast {fast} vs normal {normal}");
1223 }
1224
1225 #[test]
1226 fn set_voice_applies_to_the_next_utterance() {
1227 let mut e = engine();
1228 e.handle(Command::SetVoice("am_fenrir".into()));
1229 assert_eq!(e.snapshot().voice, "am_fenrir");
1230 }
1231
1232 #[test]
1233 fn cancel_removes_a_queued_utterance() {
1234 let mut e = engine();
1235 e.handle(say("First."));
1236 e.handle(say("Second."));
1237 let id = e.snapshot_queue_ids()[1];
1238 e.handle(Command::Cancel(id));
1239 assert_eq!(e.snapshot().queue_len, 1);
1240 }
1241
1242 #[test]
1243 fn idle_unload_drops_the_model_after_the_configured_delay() {
1244 let cfg = Config { idle_unload_secs: 0, ..Config::default() };
1248 let sink = Arc::new(Mutex::new(VecSink::new(24_000 * 10)));
1249 let mut e = Engine::new(
1250 cfg,
1251 Box::new(StubSynthesizer::new()),
1252 Box::new(SharedVecSink(sink.clone())),
1253 );
1254 e.handle(say("Hello."));
1255 run(&mut e, 500);
1256 assert_eq!(
1257 e.snapshot().state,
1258 State::Speaking,
1259 "sanity: audio must still be pending before the drain below"
1260 );
1261 sink.lock().unwrap().drain(usize::MAX);
1262 e.tick(); assert_eq!(e.snapshot().state, State::Idle);
1264 assert!(!e.is_model_loaded(), "expected the model to unload when idle");
1265 }
1266
1267 #[test]
1268 fn model_does_not_unload_while_speaking() {
1269 let cfg = Config { idle_unload_secs: 0, ..Config::default() };
1270 let mut e = Engine::new(
1271 cfg,
1272 Box::new(StubSynthesizer::new()),
1273 Box::new(VecSink::new(24_000 * 10)),
1274 );
1275 e.handle(say("A reasonably long sentence to keep it busy for a while."));
1276 e.tick();
1277 e.tick();
1278 assert!(e.is_model_loaded());
1279 }
1280
1281 #[test]
1282 fn lookahead_is_bounded() {
1283 let mut e = engine();
1284 e.handle(say(&"word ".repeat(500)));
1287 for _ in 0..50 {
1288 e.tick();
1289 }
1290 assert!(
1291 e.audio_written() <= 24_000 * 10 + 24_000,
1292 "engine ran further ahead than the sink can hold"
1293 );
1294 }
1295
1296 #[test]
1297 fn skip_sentence_stops_current_audio_promptly() {
1298 let mut cfg = Config::default();
1305 cfg.chunking.target_chars = 25;
1306 let mut e = Engine::new(
1307 cfg,
1308 Box::new(StubSynthesizer::new()),
1309 Box::new(VecSink::new(24_000 * 10)),
1310 );
1311 e.handle(say("First sentence here. Second sentence here. Third one here."));
1312 e.tick();
1313 let before = e.audio_written();
1314 e.handle(Command::SkipSentence);
1315 e.tick();
1316 assert!(e.audio_written() >= before, "skip must not lose the sink");
1317 assert_eq!(e.snapshot().state, State::Speaking);
1318 }
1319
1320 #[test]
1321 fn skip_sentence_dismisses_a_stuck_error_from_a_rejected_submission() {
1322 let cfg = Config { max_chars: 5, ..Config::default() };
1329 let mut e = Engine::new(
1330 cfg,
1331 Box::new(StubSynthesizer::new()),
1332 Box::new(VecSink::new(24_000 * 10)),
1333 );
1334 e.handle(say("way too long for the limit"));
1335 assert_eq!(e.snapshot().state, State::Error);
1336 e.handle(Command::SkipSentence);
1337 let s = e.snapshot();
1338 assert_eq!(s.state, State::Idle);
1339 assert_eq!(s.error, None);
1340 }
1341
1342 #[test]
1343 fn skip_sentence_dismisses_a_stuck_error_from_a_synthesis_failure() {
1344 struct Failing;
1346 impl crate::synth::Synthesizer for Failing {
1347 fn phonemize(&mut self, t: &str, _voice: &str) -> String {
1348 t.into()
1349 }
1350 fn fits(&mut self, _: &str) -> bool {
1351 true
1352 }
1353 fn synth(&mut self, _: &str, _: &str, _: f32) -> Result<Vec<f32>, String> {
1354 Err("model exploded".into())
1355 }
1356 fn unload(&mut self) {}
1357 fn is_loaded(&self) -> bool {
1358 true
1359 }
1360 }
1361 let mut e = Engine::new(
1362 Config::default(),
1363 Box::new(Failing),
1364 Box::new(VecSink::new(24_000)),
1365 );
1366 e.handle(say("Anything."));
1367 for _ in 0..20 {
1368 e.tick();
1369 }
1370 assert_eq!(e.snapshot().state, State::Error);
1371 e.handle(Command::SkipSentence);
1372 let s = e.snapshot();
1373 assert_eq!(s.state, State::Idle);
1374 assert_eq!(s.error, None);
1375 }
1376
1377 #[test]
1378 fn synth_failure_surfaces_as_error_and_does_not_wedge() {
1379 struct Failing;
1380 impl crate::synth::Synthesizer for Failing {
1381 fn phonemize(&mut self, t: &str, _voice: &str) -> String {
1382 t.into()
1383 }
1384 fn fits(&mut self, _: &str) -> bool {
1385 true
1386 }
1387 fn synth(&mut self, _: &str, _: &str, _: f32) -> Result<Vec<f32>, String> {
1388 Err("model exploded".into())
1389 }
1390 fn unload(&mut self) {}
1391 fn is_loaded(&self) -> bool {
1392 true
1393 }
1394 }
1395 let mut e = Engine::new(
1396 Config::default(),
1397 Box::new(Failing),
1398 Box::new(VecSink::new(24_000)),
1399 );
1400 e.handle(say("Anything."));
1401 for _ in 0..20 {
1402 e.tick();
1403 }
1404 let s = e.snapshot();
1405 assert_eq!(s.state, State::Error);
1406 assert!(s.error.as_deref().unwrap_or("").contains("model exploded"));
1407 }
1408
1409 #[test]
1410 fn empty_text_is_accepted_and_produces_nothing() {
1411 let mut e = engine();
1412 e.handle(say(" "));
1413 run(&mut e, 50);
1414 assert_eq!(e.snapshot().state, State::Idle);
1415 assert_eq!(e.audio_written(), 0);
1416 }
1417
1418 #[test]
1419 fn submit_rejection_while_idle_returns_err_and_sets_error_state() {
1420 let cfg = Config { max_chars: 5, ..Config::default() };
1423 let mut e = Engine::new(
1424 cfg,
1425 Box::new(StubSynthesizer::new()),
1426 Box::new(VecSink::new(24_000 * 10)),
1427 );
1428 let result = e.submit("way too long for the limit".into(), SayOpts::default());
1429 let msg = result.expect_err("over-long text must be rejected");
1430 assert!(msg.contains('5'), "got {msg:?}");
1431 let s = e.snapshot();
1432 assert_eq!(s.state, State::Error);
1433 assert_eq!(s.error.as_deref(), Some(msg.as_str()));
1434 }
1435
1436 #[test]
1437 fn submit_rejection_while_speaking_returns_err_but_leaves_state_untouched() {
1438 let cfg = Config { max_chars: 5, ..Config::default() };
1442 let mut e = Engine::new(
1443 cfg,
1444 Box::new(StubSynthesizer::new()),
1445 Box::new(VecSink::new(24_000 * 10)),
1446 );
1447 e.handle(say("Hi."));
1448 e.tick();
1449 assert_eq!(e.snapshot().state, State::Speaking);
1450
1451 let result = e.submit(
1452 "this one is definitely too long for the limit".into(),
1453 SayOpts::default(),
1454 );
1455
1456 assert!(result.is_err());
1457 let s = e.snapshot();
1458 assert_eq!(s.state, State::Speaking, "the unrelated playback must continue");
1459 assert_eq!(s.error, None);
1460 }
1461
1462 #[test]
1463 fn submit_accepted_returns_the_id_that_later_appears_as_current_id() {
1464 let mut e = engine();
1465 let id = e
1466 .submit("Hello there. This is a test.".into(), SayOpts::default())
1467 .expect("well-formed text must be accepted")
1468 .expect("well-formed text must be queued");
1469 e.tick();
1470 assert_eq!(e.snapshot().current_id, id);
1471 }
1472
1473 #[test]
1474 fn submit_returns_none_when_muted() {
1475 let mut e = engine();
1476 e.handle(Command::SetMuted(true));
1477 assert_eq!(e.submit("nobody hears this".into(), SayOpts::default()), Ok(None));
1478 }
1479
1480 #[test]
1481 fn submit_returns_none_for_text_that_is_empty_after_cleanup() {
1482 let mut e = engine();
1483 assert_eq!(e.submit(" ".into(), SayOpts::default()), Ok(None));
1484 }
1485
1486 #[test]
1487 fn submit_returns_some_nonzero_id_when_queued() {
1488 let mut e = engine();
1489 let id = e.submit("hello there.".into(), SayOpts::default()).expect("accepted");
1490 assert!(id.is_some());
1491 assert_ne!(id, Some(0), "id 0 is the nothing-is-playing sentinel");
1492 }
1493
1494 #[test]
1495 fn submit_still_returns_err_when_rejected() {
1496 let mut e = Engine::new(
1497 Config { max_chars: 5, ..Config::default() },
1498 Box::new(StubSynthesizer::new()),
1499 Box::new(VecSink::new(24_000)),
1500 );
1501 assert!(e.submit("far too long".into(), SayOpts::default()).is_err());
1502 }
1503
1504 struct FailingSink {
1506 accepted_once: bool,
1507 err: Option<String>,
1508 paused: bool,
1509 }
1510
1511 impl FailingSink {
1512 fn new() -> Self {
1513 FailingSink { accepted_once: false, err: None, paused: false }
1514 }
1515 }
1516
1517 impl crate::audio::AudioSink for FailingSink {
1518 fn push(&mut self, samples: &[f32]) -> usize {
1519 if self.accepted_once {
1520 self.err = Some("audio device disappeared".into());
1521 return 0;
1522 }
1523 self.accepted_once = true;
1524 samples.len()
1525 }
1526 fn pending(&self) -> usize {
1527 0
1528 }
1529 fn clear(&mut self) {}
1530 fn set_paused(&mut self, p: bool) {
1531 self.paused = p
1532 }
1533 fn is_paused(&self) -> bool {
1534 self.paused
1535 }
1536 fn capacity(&self) -> usize {
1537 24_000
1538 }
1539 fn total_written(&self) -> usize {
1540 0
1541 }
1542 fn take_error(&mut self) -> Option<String> {
1543 self.err.take()
1544 }
1545 }
1546
1547 fn text_spanning_multiple_chunks() -> String {
1555 "This is one sentence in a long batch of text. ".repeat(15)
1556 }
1557
1558 #[test]
1559 fn a_device_failure_surfaces_as_error_rather_than_wedging() {
1560 let mut e = Engine::new(
1561 Config::default(),
1562 Box::new(StubSynthesizer::new()),
1563 Box::new(FailingSink::new()),
1564 );
1565 e.submit(text_spanning_multiple_chunks(), SayOpts::default()).expect("accepted");
1566 for _ in 0..200 {
1567 e.tick();
1568 }
1569 let s = e.snapshot();
1570 assert_eq!(s.state, State::Error, "a dead device must not leave the engine Speaking");
1571 assert!(s.error.as_deref().unwrap_or("").contains("device"));
1572 }
1573
1574 #[test]
1575 fn replace_sink_clears_the_error_and_accepts_new_work() {
1576 let mut e = Engine::new(
1577 Config::default(),
1578 Box::new(StubSynthesizer::new()),
1579 Box::new(FailingSink::new()),
1580 );
1581 e.submit(text_spanning_multiple_chunks(), SayOpts::default()).expect("accepted");
1582 for _ in 0..200 {
1583 e.tick();
1584 }
1585 assert_eq!(e.snapshot().state, State::Error);
1586
1587 e.replace_sink(Box::new(VecSink::new(24_000 * 10)));
1588 let s = e.snapshot();
1589 assert_eq!(s.state, State::Idle, "a fresh sink clears the failure");
1590 assert_eq!(s.error, None);
1591
1592 e.submit("after recovery.".into(), SayOpts::default()).expect("accepted");
1593 for _ in 0..500 {
1594 e.tick();
1595 }
1596 assert!(e.audio_written() > 0, "the engine must work again after the sink is replaced");
1597 }
1598
1599 #[test]
1600 fn device_failure_while_paused_unpauses_the_sink_and_reaches_error() {
1601 let sink = FaultInjectableSink::new(24_000 * 10);
1613 let mut e = Engine::new(
1614 Config::default(),
1615 Box::new(StubSynthesizer::new()),
1616 Box::new(sink.clone()),
1617 );
1618 e.handle(say("Hello there. This keeps it busy for quite a while indeed."));
1619 e.tick();
1620 e.handle(Command::Pause);
1621 assert_eq!(e.snapshot().state, State::Paused);
1622 assert!(sink.is_paused());
1623
1624 sink.inject_failure("audio device disappeared");
1625 e.tick();
1626
1627 let s = e.snapshot();
1628 assert_eq!(s.state, State::Error, "a device failure must surface even while paused");
1629 assert!(
1630 !sink.is_paused(),
1631 "leaving Paused for Error must unpause the sink in the same step"
1632 );
1633 assert_eq!(
1634 s.error.is_some(),
1635 s.state == State::Error,
1636 "error={:?} state={:?}",
1637 s.error,
1638 s.state
1639 );
1640 }
1641
1642 #[test]
1643 fn replace_sink_recovers_from_a_device_failure_that_arrived_while_paused() {
1644 let sink = FaultInjectableSink::new(24_000 * 10);
1645 let mut e = Engine::new(
1646 Config::default(),
1647 Box::new(StubSynthesizer::new()),
1648 Box::new(sink.clone()),
1649 );
1650 e.handle(say("Hello there. This keeps it busy for quite a while indeed."));
1651 e.tick();
1652 e.handle(Command::Pause);
1653 sink.inject_failure("audio device disappeared");
1654 e.tick();
1655 assert_eq!(e.snapshot().state, State::Error);
1656
1657 e.replace_sink(Box::new(VecSink::new(24_000 * 10)));
1658 let s = e.snapshot();
1659 assert_eq!(s.state, State::Idle, "a fresh sink clears the failure");
1660 assert_eq!(s.error, None);
1661
1662 e.submit("after recovery.".into(), SayOpts::default()).expect("accepted");
1663 for _ in 0..500 {
1664 e.tick();
1665 }
1666 assert!(
1667 e.audio_written() > 0,
1668 "the engine must work again after the sink is replaced, even though the failure \
1669 arrived while paused"
1670 );
1671 }
1672}