sapi_lite/tts/
voice.rs

1use std::ffi::OsString;
2use std::str::FromStr;
3
4use strum_macros::{EnumString, IntoStaticStr};
5
6use crate::com_util::Locale;
7use crate::token::{Category, Token};
8use crate::Result;
9
10/// Specifies the age of a voice.
11#[derive(Debug, EnumString, IntoStaticStr)]
12#[strum(ascii_case_insensitive)]
13#[allow(missing_docs)]
14pub enum VoiceAge {
15    Adult,
16    Child,
17    Senior,
18    Teen,
19}
20
21/// Specifies the gender of a voice.
22#[derive(Debug, EnumString, IntoStaticStr)]
23#[strum(ascii_case_insensitive)]
24#[allow(missing_docs)]
25pub enum VoiceGender {
26    Female,
27    Male,
28    Neutral,
29}
30
31/// A voice installed on the system.
32pub struct Voice {
33    pub(crate) token: Token,
34}
35
36impl Voice {
37    /// Returns the name of this voice.
38    pub fn name(&self) -> Option<OsString> {
39        self.token.attr("name").ok()
40    }
41
42    /// Returns the age of this voice.
43    pub fn age(&self) -> Option<VoiceAge> {
44        self.token
45            .attr("age")
46            .ok()
47            .as_ref()
48            .and_then(|s| s.to_str())
49            .and_then(|s| VoiceAge::from_str(s).ok())
50    }
51
52    /// Returns the gender of this voice.
53    pub fn gender(&self) -> Option<VoiceGender> {
54        self.token
55            .attr("gender")
56            .ok()
57            .as_ref()
58            .and_then(|s| s.to_str())
59            .and_then(|s| VoiceGender::from_str(s).ok())
60    }
61
62    /// Returns the language of this voice.
63    pub fn language(&self) -> Option<OsString> {
64        let lcid = self.token.attr("language").ok()?;
65        let lcid = u32::from_str_radix(lcid.to_str()?, 16).ok()?;
66        Some(Locale::new(lcid).name())
67    }
68}
69
70/// Encapsulates the criteria for selecting a voice.
71pub struct VoiceSelector {
72    sapi_expr: String,
73}
74
75impl VoiceSelector {
76    /// Creates a new, empty selector.
77    pub fn new() -> Self {
78        Self {
79            sapi_expr: String::new(),
80        }
81    }
82
83    /// Returns a selector that requires the voice to have the given name, along with all the
84    /// previously specified conditions.
85    pub fn name_eq<S: AsRef<str>>(self, name: S) -> Self {
86        self.append_condition("name=", name.as_ref())
87    }
88
89    /// Returns a selector that requires the voice to have a name different from the one given here,
90    /// along with all the previously specified conditions.
91    pub fn name_ne<S: AsRef<str>>(self, name: S) -> Self {
92        self.append_condition("name!=", name.as_ref())
93    }
94
95    /// Returns a selector that requires the voice to have the given age, along with all the
96    /// previously specified conditions.
97    pub fn age_eq(self, age: VoiceAge) -> Self {
98        self.append_condition("age=", age.into())
99    }
100
101    /// Returns a selector that requires the voice to have an age different from the one given here,
102    /// along with all the previously specified conditions.
103    pub fn age_ne(self, age: VoiceAge) -> Self {
104        self.append_condition("age!=", age.into())
105    }
106
107    /// Returns a selector that requires the voice to have the given gender, along with all the
108    /// previously specified conditions.
109    pub fn gender_eq(self, gender: VoiceGender) -> Self {
110        self.append_condition("gender=", gender.into())
111    }
112
113    /// Returns a selector that requires the voice to have a gender different from the one given
114    /// here, along with all the previously specified conditions.
115    pub fn gender_ne(self, gender: VoiceGender) -> Self {
116        self.append_condition("gender!=", gender.into())
117    }
118
119    /// Returns a selector that requires the voice to have the given language, along with all the
120    /// previously specified conditions.
121    pub fn language_eq<S: AsRef<str>>(self, language: S) -> Self {
122        if let Ok(locale) = language.as_ref().parse::<Locale>() {
123            self.append_condition("language=", &format!("{:X}", locale.lcid()))
124        } else {
125            self
126        }
127    }
128
129    /// Returns a selector that requires the voice to have a language different from the one given
130    /// here, along with all the previously specified conditions.
131    pub fn language_ne<S: AsRef<str>>(self, language: S) -> Self {
132        if let Ok(locale) = language.as_ref().parse::<Locale>() {
133            self.append_condition("language!=", &format!("{:X}", locale.lcid()))
134        } else {
135            self
136        }
137    }
138
139    fn append_condition(mut self, prefix: &str, val: &str) -> Self {
140        if !self.sapi_expr.is_empty() {
141            self.sapi_expr.push(';')
142        }
143        self.sapi_expr.push_str(prefix);
144        self.sapi_expr.push_str(val);
145        self
146    }
147
148    pub(crate) fn into_sapi_expr(self) -> String {
149        self.sapi_expr
150    }
151}
152
153/// If successful, returns an iterator enumerating all the installed voices that satisfy the given
154/// criteria.
155///
156/// All returned voices will satisfy the `required` criteria. The voices that satisfy the
157/// `optional` criteria will be returned before the rest.
158pub fn installed_voices(
159    required: Option<VoiceSelector>,
160    optional: Option<VoiceSelector>,
161) -> Result<impl Iterator<Item = Voice>> {
162    let category = Category::new(r"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech\Voices")?;
163    let tokens = category.enum_tokens(
164        required.map(VoiceSelector::into_sapi_expr),
165        optional.map(VoiceSelector::into_sapi_expr),
166    )?;
167
168    Ok(tokens.map(|token| Voice { token }))
169}