1use std::sync::Arc;
4use std::thread::{self, JoinHandle};
5
6#[cfg(feature = "playback")]
7use super::{PlaybackError, SpeakerOutput};
8use crate::audio::Clip;
9use crate::backend::BackendError;
10use crate::cancel::CancelToken;
11
12use super::{Engine, SpeakingRate, SpeechStream, SpeechText, SynthesisError, Voice, stream};
13
14const CLIP_READ_SAMPLES: usize = 4_096;
15
16pub struct Synthesis<T> {
18 pub(super) engine: Arc<T>,
19}
20
21impl<T> Synthesis<T> {
22 pub fn new(engine: T) -> Self {
23 Self {
24 engine: Arc::new(engine),
25 }
26 }
27}
28
29pub struct Session {
34 stream: Option<SpeechStream>,
35 worker: Option<JoinHandle<Result<(), SynthesisError>>>,
36 stop: CancelToken,
37 #[cfg_attr(
38 not(feature = "playback"),
39 expect(dead_code, reason = "only playback reads it")
40 )]
41 cancel: CancelToken,
42 backend: &'static str,
43}
44
45impl<T: Engine> Synthesis<T> {
46 pub fn start(&self, text: &SpeechText) -> Result<Session, SynthesisError> {
48 self.start_with(text, None, SpeakingRate::standard(), &CancelToken::never())
49 }
50
51 pub fn start_with(
56 &self,
57 text: &SpeechText,
58 voice: Option<&Voice>,
59 rate: SpeakingRate,
60 cancel: &CancelToken,
61 ) -> Result<Session, SynthesisError> {
62 if cancel.is_cancelled() {
63 return Err(SynthesisError::Cancelled);
64 }
65 if self.engine.plays_audio_itself() {
66 return Err(SynthesisError::PcmUnavailable { backend: T::NAME });
67 }
68
69 let (writer, speech) = stream(self.engine.sample_rate());
70 let stop = CancelToken::new();
71 let requested = cancel.clone();
72 let stopped = stop.clone();
73 let synthesis_cancel =
74 CancelToken::from_fn(move || requested.is_cancelled() || stopped.is_cancelled());
75
76 let engine = Arc::clone(&self.engine);
77 let text = text.clone();
78 let voice = voice.cloned();
79 let worker_cancel = synthesis_cancel.clone();
80 let worker = thread::Builder::new()
81 .name(format!("{}-synthesis", T::NAME))
82 .spawn(move || {
83 engine.synthesize_with(&text, voice.as_ref(), rate, writer, &worker_cancel)
84 })
85 .map_err(|source| SynthesisError::Inference {
86 backend: T::NAME,
87 source: BackendError::engine(source),
88 })?;
89
90 Ok(Session {
91 stream: Some(speech),
92 worker: Some(worker),
93 stop,
94 cancel: synthesis_cancel,
95 backend: T::NAME,
96 })
97 }
98}
99
100impl Session {
101 pub fn sample_rate(&self) -> u32 {
102 self.stream
103 .as_ref()
104 .expect("the stream exists until the session is consumed")
105 .sample_rate()
106 }
107
108 pub fn read(&mut self, out: &mut [f32]) -> Result<usize, SynthesisError> {
111 if out.is_empty() {
113 return Err(SynthesisError::EmptyReadBuffer);
114 }
115 let result = self
116 .stream
117 .as_mut()
118 .expect("the stream exists until the session is consumed")
119 .read(out);
120 if matches!(result, Ok(0) | Err(_))
121 && let Err(error) = self.join()
122 {
123 return Err(error);
124 }
125 result
126 }
127
128 pub fn into_clip(mut self) -> Result<Clip, SynthesisError> {
130 let sample_rate = self.sample_rate();
131 let mut samples = Vec::new();
132 let mut block = [0.0; CLIP_READ_SAMPLES];
133 loop {
134 let read = self.read(&mut block)?;
135 if read == 0 {
136 break;
137 }
138 samples.extend_from_slice(&block[..read]);
139 }
140 Clip::new(sample_rate, samples).map_err(|source| SynthesisError::Inference {
141 backend: self.backend,
142 source: BackendError::engine(source),
143 })
144 }
145
146 #[cfg(feature = "playback")]
149 pub fn play_on(
150 mut self,
151 output: &SpeakerOutput,
152 started: impl FnOnce(),
153 ) -> Result<(), SynthesisError> {
154 let speech = self
155 .stream
156 .take()
157 .expect("the stream exists until the session is consumed");
158 let played = output.play_with(speech, &self.cancel, started);
159 let synthesized = self.join();
160
161 if let Err(error) = synthesized
162 && !matches!(error, SynthesisError::Cancelled)
163 {
164 return Err(error);
165 }
166 match played {
167 Ok(()) => Ok(()),
168 Err(PlaybackError::Cancelled) => Err(SynthesisError::Cancelled),
169 Err(source) => Err(SynthesisError::Playback { source }),
170 }
171 }
172
173 fn join(&mut self) -> Result<(), SynthesisError> {
174 let Some(worker) = self.worker.take() else {
175 return Ok(());
176 };
177 worker.join().unwrap_or(Err(SynthesisError::WorkerPanicked {
178 backend: self.backend,
179 }))
180 }
181}
182
183impl Drop for Session {
184 fn drop(&mut self) {
185 self.stop.cancel();
186 drop(self.stream.take());
187 let _finished = self.join();
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use std::path::PathBuf;
194 use std::sync::atomic::{AtomicBool, Ordering};
195
196 use super::*;
197 use crate::synthesis::{SpeechWriter, Synthesizer};
198
199 enum Behavior {
200 Samples,
201 WaitForCancel(Arc<AtomicBool>),
202 UnsupportedVoice,
203 Panic,
204 Direct,
205 }
206
207 struct Fake {
208 behavior: Behavior,
209 }
210
211 impl Fake {
212 fn with(behavior: Behavior) -> Self {
213 Self { behavior }
214 }
215 }
216
217 impl Engine for Fake {
218 const NAME: &'static str = "fake";
219 const MODEL: &'static [crate::backend::ModelDownload] = &[];
220
221 type Config = Behavior;
222
223 fn check_from(_config: &Self::Config) -> Result<PathBuf, SynthesisError> {
224 Ok(PathBuf::from("fake"))
225 }
226
227 fn load_from(config: Self::Config) -> Result<Self, SynthesisError> {
228 Ok(Self::with(config))
229 }
230 }
231
232 impl Synthesizer for Fake {
233 fn sample_rate(&self) -> u32 {
234 24_000
235 }
236
237 fn plays_audio_itself(&self) -> bool {
238 matches!(self.behavior, Behavior::Direct)
239 }
240
241 fn synthesize_with(
242 &self,
243 _text: &SpeechText,
244 _voice: Option<&Voice>,
245 _rate: SpeakingRate,
246 mut out: SpeechWriter,
247 cancel: &CancelToken,
248 ) -> Result<(), SynthesisError> {
249 match &self.behavior {
250 Behavior::Samples => {
251 out.write(&[0.1, 0.2, 0.3])
252 .expect("the session is listening");
253 out.finish();
254 Ok(())
255 }
256 Behavior::WaitForCancel(stopped) => {
257 while !cancel.is_cancelled() {
258 std::thread::yield_now();
259 }
260 stopped.store(true, Ordering::Release);
261 out.fail(SynthesisError::Cancelled);
262 Err(SynthesisError::Cancelled)
263 }
264 Behavior::UnsupportedVoice => {
265 out.fail(SynthesisError::UnsupportedVoice {
266 backend: Self::NAME,
267 voice: "unheard".to_owned(),
268 });
269 Err(SynthesisError::UnsupportedVoice {
270 backend: Self::NAME,
271 voice: "original".to_owned(),
272 })
273 }
274 Behavior::Panic => panic!("scripted worker panic"),
275 Behavior::Direct => {
276 out.finish();
277 Ok(())
278 }
279 }
280 }
281 }
282
283 fn text() -> SpeechText {
284 SpeechText::new("hello").expect("test text is valid")
285 }
286
287 #[test]
288 fn a_session_streams_pcm_and_its_sample_rate() {
289 let synthesis = Synthesis::new(Fake::with(Behavior::Samples));
290 let mut session = synthesis.start(&text()).expect("the session starts");
291 assert_eq!(session.sample_rate(), 24_000);
292
293 let mut heard = Vec::new();
294 let mut samples = [0.0; 2];
295 loop {
296 let copied = session.read(&mut samples).expect("synthesis succeeds");
297 if copied == 0 {
298 break;
299 }
300 heard.extend_from_slice(&samples[..copied]);
301 }
302
303 assert_eq!(heard, [0.1, 0.2, 0.3]);
304 }
305
306 #[test]
307 fn a_session_reads_to_one_clip_at_the_engine_rate() {
308 let synthesis = Synthesis::new(Fake::with(Behavior::Samples));
309 let session = synthesis.start(&text()).expect("the session starts");
310
311 let clip = session.into_clip().expect("synthesis succeeds");
312
313 assert_eq!(clip.sample_rate(), 24_000);
314 assert_eq!(clip.samples(), [0.1, 0.2, 0.3]);
315 }
316
317 #[test]
318 fn dropping_a_session_cancels_and_joins_its_worker() {
319 let stopped = Arc::new(AtomicBool::new(false));
320 let synthesis = Synthesis::new(Fake::with(Behavior::WaitForCancel(Arc::clone(&stopped))));
321 let session = synthesis.start(&text()).expect("the session starts");
322
323 drop(session);
324
325 assert!(stopped.load(Ordering::Acquire));
326 }
327
328 #[test]
329 fn a_terminal_read_returns_the_worker_cause() {
330 let synthesis = Synthesis::new(Fake::with(Behavior::UnsupportedVoice));
331 let mut session = synthesis.start(&text()).expect("the session starts");
332
333 let error = session
334 .read(&mut [0.0; 1])
335 .expect_err("the engine rejects the voice");
336
337 assert!(matches!(
338 error,
339 SynthesisError::UnsupportedVoice { voice, .. } if voice == "original"
340 ));
341 }
342
343 #[test]
344 fn a_panicked_worker_has_a_typed_terminal_error() {
345 let synthesis = Synthesis::new(Fake::with(Behavior::Panic));
346 let mut session = synthesis.start(&text()).expect("the session starts");
347
348 let error = session.read(&mut [0.0; 1]).expect_err("the worker panics");
349
350 assert!(matches!(
351 error,
352 SynthesisError::WorkerPanicked { backend: "fake" }
353 ));
354 }
355
356 #[test]
357 fn a_direct_playback_engine_refuses_a_pcm_session() {
358 let synthesis = Synthesis::new(Fake::with(Behavior::Direct));
359
360 let error = synthesis
361 .start(&text())
362 .err()
363 .expect("a direct engine has no PCM");
364
365 assert!(matches!(
366 error,
367 SynthesisError::PcmUnavailable { backend: "fake" }
368 ));
369 }
370
371 #[test]
372 fn a_boxed_or_shared_engine_still_plays_audio_itself() {
373 let boxed: Box<dyn Synthesizer> = Box::new(Fake::with(Behavior::Direct));
374 let shared = Arc::new(Fake::with(Behavior::Direct));
375
376 assert!(Synthesizer::plays_audio_itself(&boxed));
377 assert!(Synthesizer::plays_audio_itself(&shared));
378 }
379
380 #[cfg(feature = "playback")]
381 #[test]
382 fn a_direct_engine_speaks_without_a_pcm_session() {
383 let synthesis = Synthesis::new(Fake::with(Behavior::Direct));
384
385 synthesis
386 .speak_with(
387 &text(),
388 None,
389 SpeakingRate::standard(),
390 &CancelToken::never(),
391 )
392 .expect("the engine plays the speech itself");
393 }
394
395 #[test]
396 fn an_empty_read_buffer_is_rejected() {
397 let synthesis = Synthesis::new(Fake::with(Behavior::Samples));
398 let mut session = synthesis.start(&text()).expect("the session starts");
399
400 assert!(matches!(
401 session.read(&mut []),
402 Err(SynthesisError::EmptyReadBuffer)
403 ));
404 assert_eq!(
405 session
406 .read(&mut [0.0; 4])
407 .expect("the session can still read"),
408 3
409 );
410 }
411}