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
220 type Config = Behavior;
221
222 fn check_from(_config: &Self::Config) -> Result<PathBuf, SynthesisError> {
223 Ok(PathBuf::from("fake"))
224 }
225
226 fn load_from(config: Self::Config) -> Result<Self, SynthesisError> {
227 Ok(Self::with(config))
228 }
229 }
230
231 impl Synthesizer for Fake {
232 fn sample_rate(&self) -> u32 {
233 24_000
234 }
235
236 fn plays_audio_itself(&self) -> bool {
237 matches!(self.behavior, Behavior::Direct)
238 }
239
240 fn synthesize_with(
241 &self,
242 _text: &SpeechText,
243 _voice: Option<&Voice>,
244 _rate: SpeakingRate,
245 mut out: SpeechWriter,
246 cancel: &CancelToken,
247 ) -> Result<(), SynthesisError> {
248 match &self.behavior {
249 Behavior::Samples => {
250 out.write(&[0.1, 0.2, 0.3])
251 .expect("the session is listening");
252 out.finish();
253 Ok(())
254 }
255 Behavior::WaitForCancel(stopped) => {
256 while !cancel.is_cancelled() {
257 std::thread::yield_now();
258 }
259 stopped.store(true, Ordering::Release);
260 out.fail(SynthesisError::Cancelled);
261 Err(SynthesisError::Cancelled)
262 }
263 Behavior::UnsupportedVoice => {
264 out.fail(SynthesisError::UnsupportedVoice {
265 backend: Self::NAME,
266 voice: "unheard".to_owned(),
267 });
268 Err(SynthesisError::UnsupportedVoice {
269 backend: Self::NAME,
270 voice: "original".to_owned(),
271 })
272 }
273 Behavior::Panic => panic!("scripted worker panic"),
274 Behavior::Direct => {
275 out.finish();
276 Ok(())
277 }
278 }
279 }
280 }
281
282 fn text() -> SpeechText {
283 SpeechText::new("hello").expect("test text is valid")
284 }
285
286 #[test]
287 fn a_session_streams_pcm_and_its_sample_rate() {
288 let synthesis = Synthesis::new(Fake::with(Behavior::Samples));
289 let mut session = synthesis.start(&text()).expect("the session starts");
290 assert_eq!(session.sample_rate(), 24_000);
291
292 let mut heard = Vec::new();
293 let mut samples = [0.0; 2];
294 loop {
295 let copied = session.read(&mut samples).expect("synthesis succeeds");
296 if copied == 0 {
297 break;
298 }
299 heard.extend_from_slice(&samples[..copied]);
300 }
301
302 assert_eq!(heard, [0.1, 0.2, 0.3]);
303 }
304
305 #[test]
306 fn a_session_reads_to_one_clip_at_the_engine_rate() {
307 let synthesis = Synthesis::new(Fake::with(Behavior::Samples));
308 let session = synthesis.start(&text()).expect("the session starts");
309
310 let clip = session.into_clip().expect("synthesis succeeds");
311
312 assert_eq!(clip.sample_rate(), 24_000);
313 assert_eq!(clip.samples(), [0.1, 0.2, 0.3]);
314 }
315
316 #[test]
317 fn dropping_a_session_cancels_and_joins_its_worker() {
318 let stopped = Arc::new(AtomicBool::new(false));
319 let synthesis = Synthesis::new(Fake::with(Behavior::WaitForCancel(Arc::clone(&stopped))));
320 let session = synthesis.start(&text()).expect("the session starts");
321
322 drop(session);
323
324 assert!(stopped.load(Ordering::Acquire));
325 }
326
327 #[test]
328 fn a_terminal_read_returns_the_worker_cause() {
329 let synthesis = Synthesis::new(Fake::with(Behavior::UnsupportedVoice));
330 let mut session = synthesis.start(&text()).expect("the session starts");
331
332 let error = session
333 .read(&mut [0.0; 1])
334 .expect_err("the engine rejects the voice");
335
336 assert!(matches!(
337 error,
338 SynthesisError::UnsupportedVoice { voice, .. } if voice == "original"
339 ));
340 }
341
342 #[test]
343 fn a_panicked_worker_has_a_typed_terminal_error() {
344 let synthesis = Synthesis::new(Fake::with(Behavior::Panic));
345 let mut session = synthesis.start(&text()).expect("the session starts");
346
347 let error = session.read(&mut [0.0; 1]).expect_err("the worker panics");
348
349 assert!(matches!(
350 error,
351 SynthesisError::WorkerPanicked { backend: "fake" }
352 ));
353 }
354
355 #[test]
356 fn a_direct_playback_engine_refuses_a_pcm_session() {
357 let synthesis = Synthesis::new(Fake::with(Behavior::Direct));
358
359 let error = synthesis
360 .start(&text())
361 .err()
362 .expect("a direct engine has no PCM");
363
364 assert!(matches!(
365 error,
366 SynthesisError::PcmUnavailable { backend: "fake" }
367 ));
368 }
369
370 #[test]
371 fn a_boxed_or_shared_engine_still_plays_audio_itself() {
372 let boxed: Box<dyn Synthesizer> = Box::new(Fake::with(Behavior::Direct));
373 let shared = Arc::new(Fake::with(Behavior::Direct));
374
375 assert!(Synthesizer::plays_audio_itself(&boxed));
376 assert!(Synthesizer::plays_audio_itself(&shared));
377 }
378
379 #[cfg(feature = "playback")]
380 #[test]
381 fn a_direct_engine_speaks_without_a_pcm_session() {
382 let synthesis = Synthesis::new(Fake::with(Behavior::Direct));
383
384 synthesis
385 .speak_with(
386 &text(),
387 None,
388 SpeakingRate::standard(),
389 &CancelToken::never(),
390 )
391 .expect("the engine plays the speech itself");
392 }
393
394 #[test]
395 fn an_empty_read_buffer_is_rejected() {
396 let synthesis = Synthesis::new(Fake::with(Behavior::Samples));
397 let mut session = synthesis.start(&text()).expect("the session starts");
398
399 assert!(matches!(
400 session.read(&mut []),
401 Err(SynthesisError::EmptyReadBuffer)
402 ));
403 assert_eq!(
404 session
405 .read(&mut [0.0; 4])
406 .expect("the session can still read"),
407 3
408 );
409 }
410}