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