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::backend::BackendError;
9use crate::cancel::CancelToken;
10
11use super::{Engine, SpeakingRate, SpeechStream, SpeechText, SynthesisError, Voice, stream};
12
13/// A loaded synthesis engine ready to start utterances.
14pub 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
26/// One PCM-producing utterance.
27///
28/// The session owns its synthesis worker. Dropping it cancels and joins that
29/// worker, so an abandoned reader cannot leave model inference running.
30pub 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    /// Starts an utterance with the engine's default voice and natural pace.
44    pub fn start(&self, text: &SpeechText) -> Result<Session, SynthesisError> {
45        self.start_with(text, None, SpeakingRate::standard(), &CancelToken::never())
46    }
47
48    /// Starts an utterance and returns its PCM as it is produced.
49    ///
50    /// An engine that owns playback cannot provide a PCM session. Use
51    /// `Synthesis::speak_with` for that engine instead.
52    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    /// Copies the next PCM samples into `out`, waiting for samples or the
106    /// terminal synthesis result. Zero means the utterance completed.
107    pub fn read(&mut self, out: &mut [f32]) -> Result<usize, SynthesisError> {
108        // Checked before the stream so a bad buffer never joins the worker.
109        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    /// Plays this utterance and reports when its first source sample reaches
126    /// the output callback.
127    #[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}