flite/
lib.rs

1#![warn(clippy::all, clippy::cargo, clippy::pedantic, clippy::nursery)]
2use std::ffi::{CString, NulError};
3
4/// This function accepts a string of text to speak, and a voice to speak it with.
5/// # Errors
6/// This function returns an error when the voice is invalid, if the returned waveform is null,
7/// or if the voice or text are not valid `CStrings`.
8/// # Panics
9/// This function panics if flite returns a negative length for the sample count.
10pub fn text_to_wave<'a>(
11    text: impl Into<String>,
12    voice: impl Into<String>,
13) -> Result<WaveformAudio<'a>, Error> {
14    let unsafe_wf = unsafe {
15        flite_sys::flite_init();
16        let c_text = CString::new(text.into())?.as_ptr();
17        let c_voice = flite_sys::flite_voice_select(CString::new(voice.into())?.as_ptr());
18        if c_voice.is_null() {
19            return Err(Error::NoVoice);
20        }
21        flite_sys::flite_text_to_wave(c_text, c_voice)
22    };
23    let wf = if unsafe_wf.is_null() {
24        return Err(Error::TextToWaveNull);
25    } else {
26        unsafe { *unsafe_wf }
27    };
28    // flite should never return a negative length
29    let sample_count: usize = wf.num_samples.try_into().unwrap();
30    if wf.samples.is_null() {
31        return Err(Error::SamplesNull);
32    }
33    if wf.samples.is_null() {
34        return Err(Error::SamplesNull);
35    }
36    let samples = unsafe { std::slice::from_raw_parts(wf.samples, sample_count) };
37    Ok(WaveformAudio {
38        channels: wf.num_channels,
39        sample_rate: wf.sample_rate,
40        kind: unsafe { CString::from_raw(wf.type_ as *mut i8) },
41        samples,
42    })
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct WaveformAudio<'a> {
47    channels: i32,
48    sample_rate: i32,
49    kind: CString,
50    samples: &'a [i16],
51}
52
53impl WaveformAudio<'_> {
54    #[must_use]
55    pub const fn channels(&self) -> i32 {
56        self.channels
57    }
58    #[must_use]
59    pub const fn sample_rate(&self) -> i32 {
60        self.sample_rate
61    }
62    #[must_use]
63    pub const fn kind(&self) -> &CString {
64        &self.kind
65    }
66    #[must_use]
67    pub const fn samples(&self) -> &[i16] {
68        self.samples
69    }
70}
71
72#[derive(Debug)]
73pub enum Error {
74    CStringConversion(NulError),
75    NoVoice,
76    TextToWaveNull,
77    SamplesNull,
78    TypeNull,
79}
80
81impl From<NulError> for Error {
82    fn from(err: NulError) -> Self {
83        Self::CStringConversion(err)
84    }
85}
86
87impl std::fmt::Display for Error {
88    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
89        match self {
90            Self::CStringConversion(e) => write!(f, "CString Conversion Error: {}", e),
91            Self::NoVoice => write!(f, "There was no voice with the selected name!"),
92            Self::TextToWaveNull => write!(f, "Text-to-Wave return value was NULL"),
93            Self::SamplesNull => write!(f, "Samples in waveform are NULL"),
94            Self::TypeNull => write!(f, "Type in waveform is NULL"),
95        }
96    }
97}
98
99impl std::error::Error for Error {}
100
101#[cfg(test)]
102mod test {
103    use super::*;
104    #[test]
105    #[should_panic]
106    fn invalid_voice() {
107        text_to_wave("Example text", "NAGIGADJG").unwrap();
108    }
109}