From 143147036c19529f94647316ac054dd8615363fe Mon Sep 17 00:00:00 2001 From: Malloc Voidstar <1284317+AlyoshaVasilieva@users.noreply.github.com> Date: Thu, 2 Dec 2021 04:56:10 -0800 Subject: [PATCH] Add support for listing voices Rebased on top of the result changes. --- speech-dispatcher/examples/list_voices.rs | 29 ++++++++ speech-dispatcher/src/lib.rs | 86 +++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 speech-dispatcher/examples/list_voices.rs diff --git a/speech-dispatcher/examples/list_voices.rs b/speech-dispatcher/examples/list_voices.rs new file mode 100644 index 0000000..e4b60f2 --- /dev/null +++ b/speech-dispatcher/examples/list_voices.rs @@ -0,0 +1,29 @@ +use speech_dispatcher::*; + +fn main() -> Result<(), Box> { + let connection = Connection::open("list_voices", "list_voices", "list_voices", Mode::Threaded)?; + + let modules = connection.list_output_modules()?; + println!("Modules available: {:?}", modules); + for module in modules.into_iter() { + if connection.set_output_module(&module).is_ok() { + println!("Listing voices for module {}", module); + } else { + println!("Failed to set output module to {}", module); + continue; + }; + let voices = connection.list_synthesis_voices()?; + for voice in voices.into_iter() { + if let Some(variant) = voice.variant.as_deref() { + println!( + " Name: {} / Language: {} / Variant: {}", + voice.name, voice.language, variant + ); + } else { + println!(" Name: {} / Language: {}", voice.name, voice.language); + } + } + } + // Use connection.set_synthesis_voice(voice.name) to set the voice to use. + Ok(()) +} diff --git a/speech-dispatcher/src/lib.rs b/speech-dispatcher/src/lib.rs index 3f34f85..fb436ba 100644 --- a/speech-dispatcher/src/lib.rs +++ b/speech-dispatcher/src/lib.rs @@ -42,6 +42,38 @@ pub enum VoiceType { ChildFemale = SPDVoiceType::SPD_CHILD_FEMALE, } +#[derive(Clone, Debug, Hash, PartialEq)] +pub struct Voice { + /// The name of this voice. Unique with regards to the output module it came from. Pass this to + /// [`Connection::set_synthesis_voice`] to use this voice. + pub name: String, + /// The language of this voice. Probably a BCP 47 language tag. + pub language: String, + /// The variant of this language, if present. Loosely defined. + pub variant: Option, +} + +impl Voice { + /// Convert a SPDVoice to a Voice. Only fails if the fields are non-Unicode. + /// Does not check that the pointers are non-null, caller must ensure that. + unsafe fn try_from(v: &SPDVoice) -> Result { + // SPDVoice fields appear to all be ASCII. + let name = CStr::from_ptr(v.name).to_str()?.to_owned(); + let language = CStr::from_ptr(v.language).to_str()?.to_owned(); + let variant = CStr::from_ptr(v.variant).to_str()?; + let variant = if variant == "none" { + None + } else { + Some(variant.to_owned()) + }; + Ok(Self { + name, + language, + variant, + }) + } +} + #[derive(Clone, Debug)] pub struct Connection(pub *mut SPDConnection, u64); @@ -705,11 +737,65 @@ impl Connection { } } + pub fn list_synthesis_voices(&self) -> Result, Error> { + let start = unsafe { spd_list_synthesis_voices(self.0) }; + let slice = unsafe { null_term_array_ptr_to_slice(start) }.ok_or(Error::OperationFailed)?; + let voices = unsafe { + slice + .iter() + .map(|v| v.read()) + .flat_map(|v| { + if v.name.is_null() || v.language.is_null() || v.variant.is_null() { + None + } else { + Voice::try_from(&v).ok() + } + }) + .collect() + }; + unsafe { + free_spd_voices(start); + } + Ok(voices) + } + + pub fn list_output_modules(&self) -> Result, Error> { + let start = unsafe { spd_list_modules(self.0) }; + let slice = unsafe { null_term_array_ptr_to_slice(start) }.ok_or(Error::OperationFailed)?; + let modules = unsafe { + slice + .iter() + .flat_map(|v| CStr::from_ptr(*v).to_str().ok().map(String::from)) + .collect() + }; + unsafe { + free_spd_modules(start); + } + Ok(modules) + } + pub fn client_id(&self) -> u64 { self.1 } } +/// Interpret a null-terminated array of pointers as a slice of pointers. +/// None of the pointers will be null if this returns Some. +unsafe fn null_term_array_ptr_to_slice<'a, T>(start: *mut *mut T) -> Option<&'a [*mut T]> { + if start.is_null() { + return None; + } + let mut current = start; + let mut len = 0; + + while !current.read().is_null() { + len += 1; + current = current.add(1); + } + + Some(std::slice::from_raw_parts(start, len)) +} + unsafe impl Send for Connection {} impl Drop for Connection {