Skip to main content

aria_kit/synthesis/
session.rs

1//! One live synthesis run and the PCM it produces.
2
3use 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
16/// A loaded synthesis engine ready to start utterances.
17pub 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
29/// One PCM-producing utterance.
30///
31/// The session owns its synthesis worker. Dropping it cancels and joins that
32/// worker, so an abandoned reader cannot leave model inference running.
33pub 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    /// Starts an utterance with the engine's default voice and natural pace.
47    pub fn start(&self, text: &SpeechText) -> Result<Session, SynthesisError> {
48        self.start_with(text, None, SpeakingRate::standard(), &CancelToken::never())
49    }
50
51    /// Starts an utterance and returns its PCM as it is produced.
52    ///
53    /// An engine that owns playback cannot provide a PCM session. Use
54    /// `Synthesis::speak_with` for that engine instead.
55    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    /// Copies the next PCM samples into `out`, waiting for samples or the
109    /// terminal synthesis result. Zero means the utterance completed.
110    pub fn read(&mut self, out: &mut [f32]) -> Result<usize, SynthesisError> {
111        // Checked before the stream so a bad buffer never joins the worker.
112        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    /// Reads the rest of the utterance as one clip at the engine's rate.
129    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    /// Plays this utterance and reports when its first source sample reaches
147    /// the output callback.
148    #[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}