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#[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#[derive(Debug, EnumString, IntoStaticStr)]
23#[strum(ascii_case_insensitive)]
24#[allow(missing_docs)]
25pub enum VoiceGender {
26 Female,
27 Male,
28 Neutral,
29}
30
31pub struct Voice {
33 pub(crate) token: Token,
34}
35
36impl Voice {
37 pub fn name(&self) -> Option<OsString> {
39 self.token.attr("name").ok()
40 }
41
42 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 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 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
70pub struct VoiceSelector {
72 sapi_expr: String,
73}
74
75impl VoiceSelector {
76 pub fn new() -> Self {
78 Self {
79 sapi_expr: String::new(),
80 }
81 }
82
83 pub fn name_eq<S: AsRef<str>>(self, name: S) -> Self {
86 self.append_condition("name=", name.as_ref())
87 }
88
89 pub fn name_ne<S: AsRef<str>>(self, name: S) -> Self {
92 self.append_condition("name!=", name.as_ref())
93 }
94
95 pub fn age_eq(self, age: VoiceAge) -> Self {
98 self.append_condition("age=", age.into())
99 }
100
101 pub fn age_ne(self, age: VoiceAge) -> Self {
104 self.append_condition("age!=", age.into())
105 }
106
107 pub fn gender_eq(self, gender: VoiceGender) -> Self {
110 self.append_condition("gender=", gender.into())
111 }
112
113 pub fn gender_ne(self, gender: VoiceGender) -> Self {
116 self.append_condition("gender!=", gender.into())
117 }
118
119 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 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
153pub 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}