From 5b0d1b662137f24119eb873bc0a16c7e34982175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Caddet?= Date: Thu, 3 Sep 2020 16:50:11 +0200 Subject: [PATCH 01/98] Add voices feature Implemented for AVFoundation backend but set_voice has no effect for now Warning: does not build on Linux or windows for now --- Cargo.toml | 1 + src/backends/appkit.rs | 12 +++++++++ src/backends/av_foundation.rs | 18 +++++++++++++ src/backends/av_foundation/voices.rs | 31 ++++++++++++++++++++++ src/lib.rs | 39 ++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+) create mode 100644 src/backends/av_foundation/voices.rs diff --git a/Cargo.toml b/Cargo.toml index af088f3..7afb5e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ speech-dispatcher = "0.6" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] cocoa-foundation = "0.1" +core-foundation = "0.9" libc = "0.2" objc = "0.2" diff --git a/src/backends/appkit.rs b/src/backends/appkit.rs index 8584188..ec32729 100644 --- a/src/backends/appkit.rs +++ b/src/backends/appkit.rs @@ -195,6 +195,18 @@ impl Backend for AppKit { let is_speaking: i8 = unsafe { msg_send![self.0, isSpeaking] }; Ok(is_speaking == YES) } + + fn voice(&self) -> Result { + unimplemented!() + } + + fn list_voices(&self) -> Vec { + unimplemented!() + } + + fn set_voice(&self, voice: String) -> Result<(),Error> { + unimplemented!() + } } impl Drop for AppKit { diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index a04eaea..70e3f33 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -8,11 +8,15 @@ use objc::*; use crate::{Backend, Error, Features}; +mod voices; +use voices::AVSpeechSynthesisVoice; + pub struct AvFoundation { synth: *mut Object, rate: f32, volume: f32, pitch: f32, + voice: AVSpeechSynthesisVoice, } impl AvFoundation { @@ -25,6 +29,7 @@ impl AvFoundation { rate: 0.5, volume: 1., pitch: 1., + voice: AVSpeechSynthesisVoice::new(), } } } @@ -38,6 +43,7 @@ impl Backend for AvFoundation { pitch: true, volume: true, is_speaking: true, + voices: true, } } @@ -134,6 +140,18 @@ impl Backend for AvFoundation { let is_speaking: i8 = unsafe { msg_send![self.synth, isSpeaking] }; Ok(is_speaking == 1) } + + fn voice(&self) -> Result { + Ok(self.voice.identifier()) + } + + fn list_voices(&self) -> Vec { + AVSpeechSynthesisVoice::list().iter().map(|v| {v.identifier()}).collect() + } + + fn set_voice(&self, voice: String) -> Result<(),Error> { + Ok(()) + } } impl Drop for AvFoundation { diff --git a/src/backends/av_foundation/voices.rs b/src/backends/av_foundation/voices.rs new file mode 100644 index 0000000..98c6036 --- /dev/null +++ b/src/backends/av_foundation/voices.rs @@ -0,0 +1,31 @@ + +use objc::runtime::*; +use objc::*; +use core_foundation::array::CFArray; +use core_foundation::string::CFString; + +#[derive(Copy,Clone)] +pub struct AVSpeechSynthesisVoice(*const Object); + +impl AVSpeechSynthesisVoice { + pub fn new() -> Self { + Self::list()[0] + } + + pub fn list() -> Vec { + let voices: CFArray = unsafe{msg_send![class!(AVSpeechSynthesisVoice), speechVoices]}; + voices.iter().map(|v| { + AVSpeechSynthesisVoice{0: *v as *mut Object} + }).collect() + } + + pub fn name(self) -> String { + let name: CFString = unsafe{msg_send![self.0, name]}; + name.to_string() + } + + pub fn identifier(self) -> String { + let identifier: CFString = unsafe{msg_send![self.0, identifier]}; + identifier.to_string() + } +} diff --git a/src/lib.rs b/src/lib.rs index 56b9f32..af6db3f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,6 +46,7 @@ pub struct Features { pub pitch: bool, pub volume: bool, pub is_speaking: bool, + pub voices: bool, } impl Default for Features { @@ -56,6 +57,7 @@ impl Default for Features { pitch: false, volume: false, is_speaking: false, + voices: false, } } } @@ -98,6 +100,9 @@ pub trait Backend { fn get_volume(&self) -> Result; fn set_volume(&mut self, volume: f32) -> Result<(), Error>; fn is_speaking(&self) -> Result; + fn voice(&self) -> Result; + fn list_voices(&self) -> Vec; + fn set_voice(&self, voice: String) -> Result<(),Error>; } pub struct TTS(Box); @@ -371,4 +376,38 @@ impl TTS { Err(Error::UnsupportedFeature) } } + + /** + * Returns list of available voices. + */ + pub fn list_voices(&self) -> Vec { + self.0.list_voices() + } + + /** + * Return the current speaking voice. + */ + pub fn voice(&self) -> Result { + let Features { voices, .. } = self.supported_features(); + if voices { + self.0.voice() + } else { + Err(Error::UnsupportedFeature) + } + } + + /** + * Set speaking voice. + */ + pub fn set_voice(&self, voice: String) -> Result<(),Error> { + let Features { + voices: voices_feature, + .. + } = self.0.supported_features(); + if voices_feature { + self.0.set_voice(voice) + } else { + Err(Error::UnsupportedFeature) + } + } } From 6ed94686f3acccc095a1157b230693212a8e7e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Caddet?= Date: Thu, 3 Sep 2020 18:40:32 +0200 Subject: [PATCH 02/98] implement set_voice for AVFoundation backend - TODO: test the implementation - fixed: set_voice mutability of self parameter --- src/backends/appkit.rs | 2 +- src/backends/av_foundation.rs | 6 ++++-- src/backends/av_foundation/voices.rs | 10 ++++++++-- src/lib.rs | 4 ++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/backends/appkit.rs b/src/backends/appkit.rs index ec32729..164cf53 100644 --- a/src/backends/appkit.rs +++ b/src/backends/appkit.rs @@ -204,7 +204,7 @@ impl Backend for AppKit { unimplemented!() } - fn set_voice(&self, voice: String) -> Result<(),Error> { + fn set_voice(&mut self, voice: String) -> Result<(),Error> { unimplemented!() } } diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 70e3f33..c51988a 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -29,7 +29,7 @@ impl AvFoundation { rate: 0.5, volume: 1., pitch: 1., - voice: AVSpeechSynthesisVoice::new(), + voice: AVSpeechSynthesisVoice::default(), } } } @@ -59,6 +59,7 @@ impl Backend for AvFoundation { let _: () = msg_send![utterance, setRate: self.rate]; let _: () = msg_send![utterance, setVolume: self.volume]; let _: () = msg_send![utterance, setPitchMultiplier: self.pitch]; + let _: () = msg_send![utterance, setVoice: self.voice]; let _: () = msg_send![self.synth, speakUtterance: utterance]; } Ok(()) @@ -149,7 +150,8 @@ impl Backend for AvFoundation { AVSpeechSynthesisVoice::list().iter().map(|v| {v.identifier()}).collect() } - fn set_voice(&self, voice: String) -> Result<(),Error> { + fn set_voice(&mut self, voice: String) -> Result<(),Error> { + self.voice = AVSpeechSynthesisVoice::new(voice); Ok(()) } } diff --git a/src/backends/av_foundation/voices.rs b/src/backends/av_foundation/voices.rs index 98c6036..a23c14a 100644 --- a/src/backends/av_foundation/voices.rs +++ b/src/backends/av_foundation/voices.rs @@ -8,8 +8,14 @@ use core_foundation::string::CFString; pub struct AVSpeechSynthesisVoice(*const Object); impl AVSpeechSynthesisVoice { - pub fn new() -> Self { - Self::list()[0] + pub fn new(identifier: String) -> Self { + let i: CFString = CFString::from(identifier.as_str()); + let voice: *const Object = unsafe{msg_send![class!(AVSpeechSynthesisVoice).metaclass(), voiceWithIdentifier: i]}; + AVSpeechSynthesisVoice{0: voice} + } + + pub fn default() -> Self { + AVSpeechSynthesisVoice::list()[0] } pub fn list() -> Vec { diff --git a/src/lib.rs b/src/lib.rs index af6db3f..61a47ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,7 +102,7 @@ pub trait Backend { fn is_speaking(&self) -> Result; fn voice(&self) -> Result; fn list_voices(&self) -> Vec; - fn set_voice(&self, voice: String) -> Result<(),Error>; + fn set_voice(&mut self, voice: String) -> Result<(),Error>; } pub struct TTS(Box); @@ -399,7 +399,7 @@ impl TTS { /** * Set speaking voice. */ - pub fn set_voice(&self, voice: String) -> Result<(),Error> { + pub fn set_voice(&mut self, voice: String) -> Result<(),Error> { let Features { voices: voices_feature, .. From 0fb6c62d838ee2b07c8c1df64fa5e4f2f4161b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Caddet?= Date: Fri, 4 Sep 2020 15:48:56 +0200 Subject: [PATCH 03/98] fix some parameters types and implement set_voice We have an ilegal hardware instruction in backend::av_foundation::voices::AVSpeechSynthesisVoice::new(identifier) when sending voiceWithIdentifier. Is it because the runLoop is not runing when it's called? --- examples/hello_world.rs | 14 ++++++++++++++ src/backends/appkit.rs | 2 +- src/backends/av_foundation.rs | 2 +- src/backends/av_foundation/voices.rs | 11 +++++++---- src/lib.rs | 6 +++--- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index e95c53c..f3e054e 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -47,6 +47,20 @@ fn main() -> Result<(), Error> { tts.speak("This is normal volume.", false)?; tts.set_volume(original_volume)?; } + let Features { voices, .. } = tts.supported_features(); + if voices { + let original_voice = tts.voice()?; + let voices_list = tts.list_voices(); + println!("Available voices:\n==="); + for v in voices_list.iter() { + println!("{}",v); + tts.set_voice(v)?; + println!("voice set"); + println!("{}", tts.voice()?); + tts.speak(v,false)?; + } + tts.set_voice(original_voice)?; + } tts.speak("Goodbye.", false)?; let mut _input = String::new(); #[cfg(target_os = "macos")] diff --git a/src/backends/appkit.rs b/src/backends/appkit.rs index 164cf53..dd8caa3 100644 --- a/src/backends/appkit.rs +++ b/src/backends/appkit.rs @@ -204,7 +204,7 @@ impl Backend for AppKit { unimplemented!() } - fn set_voice(&mut self, voice: String) -> Result<(),Error> { + fn set_voice(&mut self, voice: &str) -> Result<(),Error> { unimplemented!() } } diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index c51988a..b5bfd32 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -150,7 +150,7 @@ impl Backend for AvFoundation { AVSpeechSynthesisVoice::list().iter().map(|v| {v.identifier()}).collect() } - fn set_voice(&mut self, voice: String) -> Result<(),Error> { + fn set_voice(&mut self, voice: &str) -> Result<(),Error> { self.voice = AVSpeechSynthesisVoice::new(voice); Ok(()) } diff --git a/src/backends/av_foundation/voices.rs b/src/backends/av_foundation/voices.rs index a23c14a..1f5b1d5 100644 --- a/src/backends/av_foundation/voices.rs +++ b/src/backends/av_foundation/voices.rs @@ -2,16 +2,19 @@ use objc::runtime::*; use objc::*; use core_foundation::array::CFArray; +use cocoa_foundation::foundation::NSString; +use cocoa_foundation::base::{nil,id}; use core_foundation::string::CFString; #[derive(Copy,Clone)] pub struct AVSpeechSynthesisVoice(*const Object); impl AVSpeechSynthesisVoice { - pub fn new(identifier: String) -> Self { - let i: CFString = CFString::from(identifier.as_str()); - let voice: *const Object = unsafe{msg_send![class!(AVSpeechSynthesisVoice).metaclass(), voiceWithIdentifier: i]}; - AVSpeechSynthesisVoice{0: voice} + pub fn new(identifier: &str) -> Self { + unsafe{ + let i: id = NSString::alloc(nil).init_str(identifier); + msg_send![class!(AVSpeechSynthesisVoice), voiceWithIdentifier:i] + } } pub fn default() -> Self { diff --git a/src/lib.rs b/src/lib.rs index 61a47ae..daebd23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,7 +102,7 @@ pub trait Backend { fn is_speaking(&self) -> Result; fn voice(&self) -> Result; fn list_voices(&self) -> Vec; - fn set_voice(&mut self, voice: String) -> Result<(),Error>; + fn set_voice(&mut self, voice: &str) -> Result<(),Error>; } pub struct TTS(Box); @@ -399,13 +399,13 @@ impl TTS { /** * Set speaking voice. */ - pub fn set_voice(&mut self, voice: String) -> Result<(),Error> { + pub fn set_voice>(&mut self, voice: S) -> Result<(),Error> { let Features { voices: voices_feature, .. } = self.0.supported_features(); if voices_feature { - self.0.set_voice(voice) + self.0.set_voice(voice.into().as_str()) } else { Err(Error::UnsupportedFeature) } From 1b8809aaeb7061ca0e08c7bc1d35ef13fae8984c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Caddet?= Date: Sat, 5 Sep 2020 10:55:23 +0200 Subject: [PATCH 04/98] remove the example changing voice. the default() voice working properly for av_foundation --- examples/hello_world.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index f3e054e..a34f5f8 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -47,7 +47,7 @@ fn main() -> Result<(), Error> { tts.speak("This is normal volume.", false)?; tts.set_volume(original_volume)?; } - let Features { voices, .. } = tts.supported_features(); +/* let Features { voices, .. } = tts.supported_features(); if voices { let original_voice = tts.voice()?; let voices_list = tts.list_voices(); @@ -60,7 +60,7 @@ fn main() -> Result<(), Error> { tts.speak(v,false)?; } tts.set_voice(original_voice)?; - } + }*/ tts.speak("Goodbye.", false)?; let mut _input = String::new(); #[cfg(target_os = "macos")] From b238c8c9382877c9e8756d021be5f2f6de69c3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Caddet?= Date: Sat, 5 Sep 2020 11:30:11 +0200 Subject: [PATCH 05/98] fix return type of AVSpeechSynthesisVoice:new --- src/backends/av_foundation/voices.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/backends/av_foundation/voices.rs b/src/backends/av_foundation/voices.rs index 1f5b1d5..14e7974 100644 --- a/src/backends/av_foundation/voices.rs +++ b/src/backends/av_foundation/voices.rs @@ -11,10 +11,12 @@ pub struct AVSpeechSynthesisVoice(*const Object); impl AVSpeechSynthesisVoice { pub fn new(identifier: &str) -> Self { + let voice: *const Object; unsafe{ let i: id = NSString::alloc(nil).init_str(identifier); - msg_send![class!(AVSpeechSynthesisVoice), voiceWithIdentifier:i] - } + voice = msg_send![class!(AVSpeechSynthesisVoice), voiceWithIdentifier:i]; + }; + AVSpeechSynthesisVoice{0:voice} } pub fn default() -> Self { @@ -24,7 +26,7 @@ impl AVSpeechSynthesisVoice { pub fn list() -> Vec { let voices: CFArray = unsafe{msg_send![class!(AVSpeechSynthesisVoice), speechVoices]}; voices.iter().map(|v| { - AVSpeechSynthesisVoice{0: *v as *mut Object} + AVSpeechSynthesisVoice{0: *v as *const Object} }).collect() } From 335ac710a61680897fcfcc30794097972a5857da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Caddet?= Date: Sat, 5 Sep 2020 12:07:51 +0200 Subject: [PATCH 06/98] add unimplemented functions forvoices feature on every backends --- src/backends/speech_dispatcher.rs | 12 ++++++++++++ src/backends/tolk.rs | 12 ++++++++++++ src/backends/web.rs | 12 ++++++++++++ src/backends/winrt.rs | 12 ++++++++++++ 4 files changed, 48 insertions(+) diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 009a8fb..89ab24c 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -149,6 +149,18 @@ impl Backend for SpeechDispatcher { let is_speaking = speaking.get(&self.0.client_id()).unwrap(); Ok(*is_speaking) } + + fn voice(&self) -> Result { + unimplemented!() + } + + fn list_voices(&self) -> Vec { + unimplemented!() + } + + fn set_voice(&mut self, voice: &str) -> Result<(),Error> { + unimplemented!() + } } impl Drop for SpeechDispatcher { diff --git a/src/backends/tolk.rs b/src/backends/tolk.rs index 370da65..e6d0f07 100644 --- a/src/backends/tolk.rs +++ b/src/backends/tolk.rs @@ -101,4 +101,16 @@ impl Backend for Tolk { fn is_speaking(&self) -> Result { unimplemented!() } + + fn voice(&self) -> Result { + unimplemented!() + } + + fn list_voices(&self) -> Vec { + unimplemented!() + } + + fn set_voice(&mut self, voice: &str) -> Result<(),Error> { + unimplemented!() + } } diff --git a/src/backends/web.rs b/src/backends/web.rs index 664793c..a8be3c2 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -131,4 +131,16 @@ impl Backend for Web { Err(Error::NoneError) } } + + fn voice(&self) -> Result { + unimplemented!() + } + + fn list_voices(&self) -> Vec { + unimplemented!() + } + + fn set_voice(&mut self, voice: &str) -> Result<(),Error> { + unimplemented!() + } } diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 7464f58..2e8af98 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -156,4 +156,16 @@ impl Backend for WinRT { let playing = state == MediaPlaybackState::Opening || state == MediaPlaybackState::Playing; Ok(playing) } + + fn voice(&self) -> Result { + unimplemented!() + } + + fn list_voices(&self) -> Vec { + unimplemented!() + } + + fn set_voice(&mut self, voice: &str) -> Result<(),Error> { + unimplemented!() + } } From 8c8dc0ae9f01f58bb0301519046746c9ff5732a6 Mon Sep 17 00:00:00 2001 From: Francois Caddet Date: Sat, 26 Sep 2020 23:03:56 +0200 Subject: [PATCH 07/98] add voices value returned by the backends --- src/backends/speech_dispatcher.rs | 1 + src/backends/web.rs | 1 + src/backends/winrt.rs | 1 + 3 files changed, 3 insertions(+) diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index afbbba3..7fa1fbe 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -75,6 +75,7 @@ impl Backend for SpeechDispatcher { pitch: true, volume: true, is_speaking: true, + voices: false, utterance_callbacks: true, } } diff --git a/src/backends/web.rs b/src/backends/web.rs index a43b2c8..c2d6ddd 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -47,6 +47,7 @@ impl Backend for Web { pitch: true, volume: true, is_speaking: true, + voices: true, utterance_callbacks: true, } } diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 98826e6..d22d6a3 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -140,6 +140,7 @@ impl Backend for WinRT { pitch: true, volume: true, is_speaking: true, + voices: true, utterance_callbacks: true, } } From 008662c940c74d32dc288b8df27ef44fddd33fff Mon Sep 17 00:00:00 2001 From: Francois Caddet Date: Sat, 26 Sep 2020 23:16:10 +0200 Subject: [PATCH 08/98] temporary fix to a build issue with the crate speech-dispatcher --- Cargo.toml | 9 +++++++++ speech-dispatcher.patch | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 speech-dispatcher.patch diff --git a/Cargo.toml b/Cargo.toml index 549745f..8d3757f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,15 @@ license = "MIT" exclude = ["*.cfg", "*.yml"] edition = "2018" +[package.metadata.patch.speech-dispatcher] +version = "0.7.0" +patches = [ + "speech-dispatcher.patch" +] + +[patch.crates-io] +speech-dispatcher = { path = './target/patch/speech-dispatcher-0.7.0'} + [lib] crate-type = ["lib", "cdylib", "staticlib"] diff --git a/speech-dispatcher.patch b/speech-dispatcher.patch new file mode 100644 index 0000000..65ebeab --- /dev/null +++ b/speech-dispatcher.patch @@ -0,0 +1,22 @@ +diff --git src/lib.rs src/lib.rs +index 26ba271..180513e 100644 +--- src/lib.rs ++++ src/lib.rs +@@ -127,7 +127,7 @@ unsafe extern "C" fn cb(msg_id: u64, client_id: u64, state: u32) { + } + } + +-unsafe extern "C" fn cb_im(msg_id: u64, client_id: u64, state: u32, index_mark: *mut i8) { ++unsafe extern "C" fn cb_im(msg_id: u64, client_id: u64, state: u32, index_mark: *mut u8) { + let index_mark = CStr::from_ptr(index_mark); + let index_mark = index_mark.to_string_lossy().to_string(); + let state = match state { +@@ -325,7 +325,7 @@ impl Connection { + i32_to_bool(v) + } + +- pub fn wchar(&self, priority: Priority, wchar: i32) -> bool { ++ pub fn wchar(&self, priority: Priority, wchar: u32) -> bool { + let v = unsafe { spd_wchar(self.0, priority as u32, wchar) }; + i32_to_bool(v) + } From f78aed211fa2ff2a2684c5ad20939851efddbe62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Caddet?= Date: Sat, 26 Sep 2020 23:36:15 +0200 Subject: [PATCH 09/98] fix conflicts --- src/backends/av_foundation.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index c1f1c7f..6940e07 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -120,11 +120,8 @@ impl Backend for AvFoundation { pitch: true, volume: true, is_speaking: true, -<<<<<<< HEAD voices: true, -======= utterance_callbacks: true, ->>>>>>> develop } } From f7297e18fd77b99157b0dfc4782e8593ac9f4a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Caddet?= Date: Sat, 26 Sep 2020 23:39:30 +0200 Subject: [PATCH 10/98] add condition for macOS 11 and greater for default backend --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 67dea00..bc29f4b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -230,7 +230,8 @@ impl TTS { let version = version[1]; let version_parts: Vec<&str> = version.split(".").collect(); let minor_version: i8 = version_parts[1].parse().unwrap(); - if minor_version >= 14 { + let major_version: i8 = version_parts[0].parse().unwrap(); + if minor_version >= 14 || major_version >= 11 { TTS::new(Backends::AvFoundation) } else { TTS::new(Backends::AppKit) From e19eb56169e7491deb0bcf35d2eaabc15ae3dbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Caddet?= Date: Sun, 27 Sep 2020 20:04:12 +0200 Subject: [PATCH 11/98] first implementation of a voice trait for macOS WARN: not tested --- Cargo.toml | 1 + examples/hello_world.rs | 4 ++-- src/backends/av_foundation/voices.rs | 30 ++++++++++++++++++++++------ src/lib.rs | 1 + 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8d3757f..cc236ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ crate-type = ["lib", "cdylib", "staticlib"] lazy_static = "1" log = "0.4" thiserror = "1" +unic-langid = "0.9.0" [dev-dependencies] env_logger = "0.7" diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 255cae2..264d9b8 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -59,7 +59,7 @@ fn main() -> Result<(), Error> { tts.speak("This is normal volume.", false)?; tts.set_volume(original_volume)?; } -/* let Features { voices, .. } = tts.supported_features(); + let Features { voices, .. } = tts.supported_features(); if voices { let original_voice = tts.voice()?; let voices_list = tts.list_voices(); @@ -72,7 +72,7 @@ fn main() -> Result<(), Error> { tts.speak(v,false)?; } tts.set_voice(original_voice)?; - }*/ + } tts.speak("Goodbye.", false)?; let mut _input = String::new(); // The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context. diff --git a/src/backends/av_foundation/voices.rs b/src/backends/av_foundation/voices.rs index 14e7974..b8ee960 100644 --- a/src/backends/av_foundation/voices.rs +++ b/src/backends/av_foundation/voices.rs @@ -6,6 +6,10 @@ use cocoa_foundation::foundation::NSString; use cocoa_foundation::base::{nil,id}; use core_foundation::string::CFString; +use crate::backends::AvFoundation; +use crate::voices; +use crate::voices::Gender; + #[derive(Copy,Clone)] pub struct AVSpeechSynthesisVoice(*const Object); @@ -18,25 +22,39 @@ impl AVSpeechSynthesisVoice { }; AVSpeechSynthesisVoice{0:voice} } +} - pub fn default() -> Self { - AVSpeechSynthesisVoice::list()[0] - } +impl voices::Backend for AVSpeechSynthesisVoice { + type Backend = AvFoundation; - pub fn list() -> Vec { + fn list() -> Vec { let voices: CFArray = unsafe{msg_send![class!(AVSpeechSynthesisVoice), speechVoices]}; voices.iter().map(|v| { AVSpeechSynthesisVoice{0: *v as *const Object} }).collect() } - pub fn name(self) -> String { + fn name(self) -> String { let name: CFString = unsafe{msg_send![self.0, name]}; name.to_string() } - pub fn identifier(self) -> String { + fn gender(self) -> Gender { + let gender: i64 = unsafe{ msg_send![self.0, gender] }; + match gender { + 1 => Gender::Male, + 2 => Gender::Female, + _ => Gender::Other, + } + } + + fn id(self) -> String { let identifier: CFString = unsafe{msg_send![self.0, identifier]}; identifier.to_string() } + + fn language(self) -> voices::LanguageIdentifier { + let lang: CFString = unsafe{msg_send![self.0, language]}; + lang.to_string().parse().unwrap() + } } diff --git a/src/lib.rs b/src/lib.rs index bc29f4b..91f13c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,7 @@ use web_sys::SpeechSynthesisUtterance; use tts_winrt_bindings::windows::media::playback::MediaPlaybackItem; mod backends; +mod voices; pub enum Backends { #[cfg(target_os = "linux")] From 3294a82485d94e22138b444e84d0a791d094c20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Caddet?= Date: Sun, 27 Sep 2020 20:35:40 +0200 Subject: [PATCH 12/98] some fixes now build on macOS --- src/backends/av_foundation.rs | 9 +++++---- src/backends/av_foundation/voices.rs | 2 +- src/voices.rs | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 src/voices.rs diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 6940e07..445b3c9 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -10,9 +10,10 @@ use objc::runtime::{Object, Sel}; use objc::{class, declare::ClassDecl, msg_send, sel, sel_impl}; use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; +use crate::voices::Backend as VoiceBackend; mod voices; -use voices::AVSpeechSynthesisVoice; +use voices::*; pub(crate) struct AvFoundation { id: BackendId, @@ -100,7 +101,7 @@ impl AvFoundation { rate: 0.5, volume: 1., pitch: 1., - voice: AVSpeechSynthesisVoice::default(), + voice: AVSpeechSynthesisVoice::new(""), } }; *backend_id += 1; @@ -222,11 +223,11 @@ impl Backend for AvFoundation { } fn voice(&self) -> Result { - Ok(self.voice.identifier()) + Ok(self.voice.id()) } fn list_voices(&self) -> Vec { - AVSpeechSynthesisVoice::list().iter().map(|v| {v.identifier()}).collect() + AVSpeechSynthesisVoice::list().iter().map(|v| {v.id()}).collect() } fn set_voice(&mut self, voice: &str) -> Result<(),Error> { diff --git a/src/backends/av_foundation/voices.rs b/src/backends/av_foundation/voices.rs index b8ee960..32ae9a2 100644 --- a/src/backends/av_foundation/voices.rs +++ b/src/backends/av_foundation/voices.rs @@ -11,7 +11,7 @@ use crate::voices; use crate::voices::Gender; #[derive(Copy,Clone)] -pub struct AVSpeechSynthesisVoice(*const Object); +pub(crate) struct AVSpeechSynthesisVoice(*const Object); impl AVSpeechSynthesisVoice { pub fn new(identifier: &str) -> Self { diff --git a/src/voices.rs b/src/voices.rs new file mode 100644 index 0000000..031fcdd --- /dev/null +++ b/src/voices.rs @@ -0,0 +1,17 @@ + +pub use unic_langid::LanguageIdentifier; + +pub enum Gender { + Other, + Male, + Female, +} + +pub trait Backend: Sized { + type Backend: crate::Backend; + fn list() -> Vec; + fn name(self) -> String; + fn gender(self) -> Gender; + fn id(self) -> String; + fn language(self) -> LanguageIdentifier; +} From d2c42d97f5313f4c2ca5972b02f6f309374a635b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Caddet?= Date: Mon, 28 Sep 2020 11:18:54 +0200 Subject: [PATCH 13/98] the voices::Backend trait is almost stable --- src/backends/av_foundation.rs | 4 ++-- src/backends/av_foundation/voices.rs | 13 ++++++++++--- src/voices.rs | 4 ++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 445b3c9..7e3590f 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -101,7 +101,7 @@ impl AvFoundation { rate: 0.5, volume: 1., pitch: 1., - voice: AVSpeechSynthesisVoice::new(""), + voice: AVSpeechSynthesisVoice::new(), } }; *backend_id += 1; @@ -231,7 +231,7 @@ impl Backend for AvFoundation { } fn set_voice(&mut self, voice: &str) -> Result<(),Error> { - self.voice = AVSpeechSynthesisVoice::new(voice); + self.voice = AVSpeechSynthesisVoice::new(); Ok(()) } } diff --git a/src/backends/av_foundation/voices.rs b/src/backends/av_foundation/voices.rs index 32ae9a2..71bf8c1 100644 --- a/src/backends/av_foundation/voices.rs +++ b/src/backends/av_foundation/voices.rs @@ -14,11 +14,10 @@ use crate::voices::Gender; pub(crate) struct AVSpeechSynthesisVoice(*const Object); impl AVSpeechSynthesisVoice { - pub fn new(identifier: &str) -> Self { + pub fn new() -> Self { let voice: *const Object; unsafe{ - let i: id = NSString::alloc(nil).init_str(identifier); - voice = msg_send![class!(AVSpeechSynthesisVoice), voiceWithIdentifier:i]; + voice = msg_send![class!(AVSpeechSynthesisVoice), new]; }; AVSpeechSynthesisVoice{0:voice} } @@ -27,6 +26,14 @@ impl AVSpeechSynthesisVoice { impl voices::Backend for AVSpeechSynthesisVoice { type Backend = AvFoundation; + fn from_id(id: String) -> Self { + unimplemented!() + } + + fn from_language(lang: voices::LanguageIdentifier) -> Self { + unimplemented!() + } + fn list() -> Vec { let voices: CFArray = unsafe{msg_send![class!(AVSpeechSynthesisVoice), speechVoices]}; voices.iter().map(|v| { diff --git a/src/voices.rs b/src/voices.rs index 031fcdd..8ca927d 100644 --- a/src/voices.rs +++ b/src/voices.rs @@ -9,9 +9,13 @@ pub enum Gender { pub trait Backend: Sized { type Backend: crate::Backend; + fn from_id(id: String) -> Self; + fn from_language(lang: LanguageIdentifier) -> Self; fn list() -> Vec; fn name(self) -> String; fn gender(self) -> Gender; fn id(self) -> String; fn language(self) -> LanguageIdentifier; } + +pub struct Voice(Box); From 92538fbdb869eeb29d8114f8e7338bb662308679 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 1 Nov 2021 10:36:15 -0500 Subject: [PATCH 14/98] Upgrade windows-rs to 0.23. --- Cargo.toml | 14 ++++++++++---- build.rs | 10 ---------- src/backends/{winrt/mod.rs => winrt.rs} | 9 +++------ src/backends/winrt/bindings.rs | 1 - src/lib.rs | 2 +- 5 files changed, 14 insertions(+), 22 deletions(-) rename src/backends/{winrt/mod.rs => winrt.rs} (98%) delete mode 100644 src/backends/winrt/bindings.rs diff --git a/Cargo.toml b/Cargo.toml index fcf5bfd..1b52894 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,12 +20,18 @@ thiserror = "1" [dev-dependencies] env_logger = "0.8" +[dependencies.windows] +version = "0.23" +features = [ + "Foundation", + "Media_Core", + "Media_Playback", + "Media_SpeechSynthesis", + "Storage_Streams", +] + [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = "0.9" - -[target.'cfg(windows)'.build-dependencies] -windows = "0.9" [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.7" diff --git a/build.rs b/build.rs index e2e2035..8b1edc2 100644 --- a/build.rs +++ b/build.rs @@ -1,14 +1,4 @@ fn main() { - #[cfg(windows)] - if std::env::var("TARGET").unwrap().contains("windows") { - windows::build!( - Windows::Foundation::{EventRegistrationToken, IAsyncOperation, TypedEventHandler}, - Windows::Media::Core::MediaSource, - Windows::Media::Playback::{MediaPlaybackSession, MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory}, - Windows::Media::SpeechSynthesis::{SpeechSynthesisStream, SpeechSynthesizer, SpeechSynthesizerOptions}, - Windows::Storage::Streams::IRandomAccessStream, - ); - } if std::env::var("TARGET").unwrap().contains("-apple") { println!("cargo:rustc-link-lib=framework=AVFoundation"); if !std::env::var("CARGO_CFG_TARGET_OS") diff --git a/src/backends/winrt/mod.rs b/src/backends/winrt.rs similarity index 98% rename from src/backends/winrt/mod.rs rename to src/backends/winrt.rs index af9ca34..00ccc51 100644 --- a/src/backends/winrt/mod.rs +++ b/src/backends/winrt.rs @@ -4,10 +4,7 @@ use std::sync::Mutex; use lazy_static::lazy_static; use log::{info, trace}; - -mod bindings; - -use bindings::Windows::{ +use windows::{ Foundation::TypedEventHandler, Media::{ Core::MediaSource, @@ -18,8 +15,8 @@ use bindings::Windows::{ use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; -impl From for Error { - fn from(e: windows::Error) -> Self { +impl From for Error { + fn from(e: windows::runtime::Error) -> Self { Error::WinRt(e) } } diff --git a/src/backends/winrt/bindings.rs b/src/backends/winrt/bindings.rs deleted file mode 100644 index 7915760..0000000 --- a/src/backends/winrt/bindings.rs +++ /dev/null @@ -1 +0,0 @@ -::windows::include_bindings!(); diff --git a/src/lib.rs b/src/lib.rs index d62c245..f91ceac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,7 +117,7 @@ pub enum Error { JavaScriptError(wasm_bindgen::JsValue), #[cfg(windows)] #[error("WinRT error")] - WinRt(windows::Error), + WinRt(windows::runtime::Error), #[error("Unsupported feature")] UnsupportedFeature, #[error("Out of range")] From a703e790ecfaf872fac1a9f1df01fee2b9297ac9 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 1 Nov 2021 10:38:26 -0500 Subject: [PATCH 15/98] Bump edition and version. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1b52894..976f5fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "tts" -version = "0.17.3" +version = "0.18.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" license = "MIT" exclude = ["*.cfg", "*.yml"] -edition = "2018" +edition = "2021" [lib] crate-type = ["lib", "cdylib", "staticlib"] From f8dbc04c36e834d225acab4331b7a36ce4a4a470 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 1 Nov 2021 10:39:58 -0500 Subject: [PATCH 16/98] Bump dependencies. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 976f5fe..b95a3d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ log = "0.4" thiserror = "1" [dev-dependencies] -env_logger = "0.8" +env_logger = "0.9" [dependencies.windows] version = "0.23" @@ -47,4 +47,4 @@ web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "Spee [target.'cfg(target_os="android")'.dependencies] jni = "0.19" -ndk-glue = "0.3" \ No newline at end of file +ndk-glue = "0.4" \ No newline at end of file From c12f328cf278b180988d78c935d5c37780e8c9f4 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 8 Nov 2021 07:27:35 -0600 Subject: [PATCH 17/98] Bump dependency. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b95a3d0..07086bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ thiserror = "1" env_logger = "0.9" [dependencies.windows] -version = "0.23" +version = "0.25" features = [ "Foundation", "Media_Core", From 562489e5afc120313908966d1271cbd5bab12327 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 8 Nov 2021 07:28:07 -0600 Subject: [PATCH 18/98] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 07086bf..97a4456 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.18.0" +version = "0.18.1" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From d5bdb9f4981a024b92decfb161d4d406ac14790c Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 15 Nov 2021 08:16:00 -0600 Subject: [PATCH 19/98] Bump version and dependency. --- Cargo.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 97a4456..3eac5e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.18.1" +version = "0.18.2" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -21,8 +21,9 @@ thiserror = "1" env_logger = "0.9" [dependencies.windows] -version = "0.25" +version = "0.26" features = [ + "std", "Foundation", "Media_Core", "Media_Playback", From 119678ae559e03a9728497fc273ebee6b502aab3 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 16 Nov 2021 11:13:31 -0600 Subject: [PATCH 20/98] Update to windows 0.27 and bump version. --- Cargo.toml | 5 +++-- src/backends/winrt.rs | 4 ++-- src/lib.rs | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3eac5e1..db2b561 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.18.2" +version = "0.18.3" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -21,8 +21,9 @@ thiserror = "1" env_logger = "0.9" [dependencies.windows] -version = "0.26" +version = "0.27" features = [ + "alloc", "std", "Foundation", "Media_Core", diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 00ccc51..e7c8634 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -15,8 +15,8 @@ use windows::{ use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; -impl From for Error { - fn from(e: windows::runtime::Error) -> Self { +impl From for Error { + fn from(e: windows::core::Error) -> Self { Error::WinRt(e) } } diff --git a/src/lib.rs b/src/lib.rs index f91ceac..4247435 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,7 +117,7 @@ pub enum Error { JavaScriptError(wasm_bindgen::JsValue), #[cfg(windows)] #[error("WinRT error")] - WinRt(windows::runtime::Error), + WinRt(windows::core::Error), #[error("Unsupported feature")] UnsupportedFeature, #[error("Out of range")] From 57ffbf0e4fa17de7810168894e1088d90e26991a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 16 Nov 2021 11:36:24 -0600 Subject: [PATCH 21/98] Make windows dependency platform-specific and add alloc feature. --- Cargo.toml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index db2b561..c2542a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,20 +20,9 @@ thiserror = "1" [dev-dependencies] env_logger = "0.9" -[dependencies.windows] -version = "0.27" -features = [ - "alloc", - "std", - "Foundation", - "Media_Core", - "Media_Playback", - "Media_SpeechSynthesis", - "Storage_Streams", -] - [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } +windows = { version = "0.27", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.7" From 47e164a0c8267c03384e5fe256ec00ab95c58917 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 19 Nov 2021 09:22:05 -0600 Subject: [PATCH 22/98] Support Speech Dispatcher initialization failures, and bump version. --- Cargo.toml | 4 ++-- src/backends/speech_dispatcher.rs | 9 ++++----- src/lib.rs | 11 +++++++++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c2542a9..fc478ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.18.3" +version = "0.19.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -25,7 +25,7 @@ tolk = { version = "0.5", optional = true } windows = { version = "0.27", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] -speech-dispatcher = "0.7" +speech-dispatcher = "0.8" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] cocoa-foundation = "0.1" diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 39c7fd8..bd373c4 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -1,6 +1,5 @@ #[cfg(target_os = "linux")] -use std::collections::HashMap; -use std::sync::Mutex; +use std::{collections::HashMap, sync::Mutex}; use lazy_static::*; use log::{info, trace}; @@ -19,9 +18,9 @@ lazy_static! { } impl SpeechDispatcher { - pub(crate) fn new() -> Self { + pub(crate) fn new() -> std::result::Result { info!("Initializing SpeechDispatcher backend"); - let connection = speech_dispatcher::Connection::open("tts", "tts", "tts", Mode::Threaded); + let connection = speech_dispatcher::Connection::open("tts", "tts", "tts", Mode::Threaded)?; let sd = SpeechDispatcher(connection); let mut speaking = SPEAKING.lock().unwrap(); speaking.insert(sd.0.client_id(), false); @@ -66,7 +65,7 @@ impl SpeechDispatcher { let mut speaking = SPEAKING.lock().unwrap(); speaking.insert(client_id, true); }))); - sd + Ok(sd) } } diff --git a/src/lib.rs b/src/lib.rs index 4247435..9488daa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,7 @@ use lazy_static::lazy_static; use libc::c_char; #[cfg(target_os = "macos")] use objc::{class, msg_send, sel, sel_impl}; +use speech_dispatcher::SpeechDispatcherError; use thiserror::Error; #[cfg(all(windows, feature = "tolk"))] use tolk::Tolk; @@ -113,8 +114,11 @@ pub enum Error { #[error("Operation failed")] OperationFailed, #[cfg(target_arch = "wasm32")] - #[error("JavaScript error: [0])]")] + #[error("JavaScript error: [0]")] JavaScriptError(wasm_bindgen::JsValue), + #[cfg(target_os = "linux")] + #[error("Speech Dispatcher error: {0}")] + SpeechDispatcher(#[from] SpeechDispatcherError), #[cfg(windows)] #[error("WinRT error")] WinRt(windows::core::Error), @@ -183,7 +187,10 @@ impl Tts { pub fn new(backend: Backends) -> Result { let backend = match backend { #[cfg(target_os = "linux")] - Backends::SpeechDispatcher => Ok(Tts(Box::new(backends::SpeechDispatcher::new()))), + Backends::SpeechDispatcher => { + let tts = backends::SpeechDispatcher::new()?; + Ok(Tts(Box::new(tts))) + } #[cfg(target_arch = "wasm32")] Backends::Web => { let tts = backends::Web::new()?; From 89fd14d957529b5690ac4ffd0b1bff3fbe3a7c0a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 19 Nov 2021 09:24:58 -0600 Subject: [PATCH 23/98] Bump windows crate dependency. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index fc478ea..22d5acf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.27", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.28", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.8" From 94417b5351b6ba933c07ec16f6cc5793a2500c4d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 19 Nov 2021 09:25:37 -0600 Subject: [PATCH 24/98] Only import from speech_dispatcher when building for Linux. --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 9488daa..1b7b5db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,7 @@ use lazy_static::lazy_static; use libc::c_char; #[cfg(target_os = "macos")] use objc::{class, msg_send, sel, sel_impl}; +#[cfg(target_os = "linux")] use speech_dispatcher::SpeechDispatcherError; use thiserror::Error; #[cfg(all(windows, feature = "tolk"))] From d24d1a6a152eb3945e1ec491de4d4c52756d17bc Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 2 Dec 2021 09:24:22 -0600 Subject: [PATCH 25/98] Bump version and dependencies. --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 22d5acf..ef4beef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.19.0" +version = "0.19.1" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -25,7 +25,7 @@ tolk = { version = "0.5", optional = true } windows = { version = "0.28", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] -speech-dispatcher = "0.8" +speech-dispatcher = "0.9" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] cocoa-foundation = "0.1" @@ -38,4 +38,4 @@ web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "Spee [target.'cfg(target_os="android")'.dependencies] jni = "0.19" -ndk-glue = "0.4" \ No newline at end of file +ndk-glue = "0.5" \ No newline at end of file From ee8ec97ab4cefa824ec01303cb51ae5be18d6d36 Mon Sep 17 00:00:00 2001 From: Malloc Voidstar <1284317+AlyoshaVasilieva@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:10:39 -0800 Subject: [PATCH 26/98] Bump ndk-glue version in Android example --- examples/android/cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/android/cargo.toml b/examples/android/cargo.toml index a7ff289..3585cca 100644 --- a/examples/android/cargo.toml +++ b/examples/android/cargo.toml @@ -10,5 +10,5 @@ edition = "2018" crate-type = ["dylib"] [dependencies] -ndk-glue = "0.2" +ndk-glue = "0.5" tts = { path = "../.." } \ No newline at end of file From 5e9c98b0639cf03c1896f7164d75e5c2870ce49a Mon Sep 17 00:00:00 2001 From: Malloc Voidstar <1284317+AlyoshaVasilieva@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:31:21 -0800 Subject: [PATCH 27/98] Also bump gradle.plugin.com.github.willir.rust:plugin 0.3.3 doesn't work with cargo-ndk 2+ --- examples/android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/android/build.gradle b/examples/android/build.gradle index dd9a15c..27b39c3 100644 --- a/examples/android/build.gradle +++ b/examples/android/build.gradle @@ -11,7 +11,7 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:4.1.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "gradle.plugin.com.github.willir.rust:plugin:0.3.3" + classpath "gradle.plugin.com.github.willir.rust:plugin:0.3.4" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } From bed6cfa2066c8e0d1b552387d7ac2620422d60cb Mon Sep 17 00:00:00 2001 From: Malloc Voidstar <1284317+AlyoshaVasilieva@users.noreply.github.com> Date: Fri, 10 Dec 2021 10:47:12 -0800 Subject: [PATCH 28/98] Exit Android initialization loop with error when stuck 500ms is fairly arbitrary; my emulator took 35 to run that loop. --- src/backends/android.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/backends/android.rs b/src/backends/android.rs index 55449c3..c47d86a 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -4,7 +4,7 @@ use std::ffi::{CStr, CString}; use std::os::raw::c_void; use std::sync::{Mutex, RwLock}; use std::thread; -use std::time::Duration; +use std::time::{Duration, Instant}; use jni::objects::{GlobalRef, JObject, JString}; use jni::sys::{jfloat, jint, JNI_VERSION_1_6}; @@ -198,12 +198,18 @@ impl Android { } let tts = env.new_global_ref(tts)?; // This hack makes my brain bleed. + const MAX_WAIT_TIME: Duration = Duration::from_millis(500); + let start = Instant::now(); + // Wait a max of 500ms for initialization, then return an error to avoid hanging. loop { { let pending = PENDING_INITIALIZATIONS.read().unwrap(); if !(*pending).contains(&bid) { break; } + if start.elapsed() > MAX_WAIT_TIME { + return Err(Error::OperationFailed); + } } thread::sleep(Duration::from_millis(5)); } From 9ed03753c25264094e0b98c14a92f2012f89abcf Mon Sep 17 00:00:00 2001 From: Raimundo Saona <37874270+saona-raimundo@users.noreply.github.com> Date: Wed, 22 Dec 2021 13:28:00 +0100 Subject: [PATCH 29/98] Common traits for Features --- Cargo.toml | 1 + src/lib.rs | 23 +++++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ef4beef..4ed7590 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ dyn-clonable = "0.9" lazy_static = "1" log = "0.4" thiserror = "1" +serde = { version = "1.0", optional = true, features = ["derive"] } [dev-dependencies] env_logger = "0.9" diff --git a/src/lib.rs b/src/lib.rs index 1b7b5db..0dbdea4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ use std::boxed::Box; use std::collections::HashMap; #[cfg(target_os = "macos")] use std::ffi::CStr; +use std::fmt; use std::sync::Mutex; #[cfg(any(target_os = "macos", target_os = "ios"))] @@ -84,13 +85,15 @@ unsafe impl Send for UtteranceId {} unsafe impl Sync for UtteranceId {} +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Features { - pub stop: bool, - pub rate: bool, - pub pitch: bool, - pub volume: bool, pub is_speaking: bool, + pub pitch: bool, + pub rate: bool, + pub stop: bool, pub utterance_callbacks: bool, + pub volume: bool, } impl Default for Features { @@ -106,6 +109,18 @@ impl Default for Features { } } +impl fmt::Display for Features { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + writeln!(f, "{:#?}", self) + } +} + +impl Features { + pub fn new() -> Self { + Self::default() + } +} + #[derive(Debug, Error)] pub enum Error { #[error("IO error: {0}")] From e20170583dbb404999617bf064608eab7024aa69 Mon Sep 17 00:00:00 2001 From: Raimundo Saona <37874270+saona-raimundo@users.noreply.github.com> Date: Wed, 22 Dec 2021 15:38:11 +0100 Subject: [PATCH 30/98] Common traits for other structs --- src/lib.rs | 111 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 85 insertions(+), 26 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0dbdea4..8bb1141 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,50 +35,108 @@ use tolk::Tolk; mod backends; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Backends { - #[cfg(target_os = "linux")] - SpeechDispatcher, - #[cfg(target_arch = "wasm32")] - Web, - #[cfg(all(windows, feature = "tolk"))] - Tolk, - #[cfg(windows)] - WinRt, + #[cfg(target_os = "android")] + Android, #[cfg(target_os = "macos")] AppKit, #[cfg(any(target_os = "macos", target_os = "ios"))] AvFoundation, - #[cfg(target_os = "android")] - Android, + #[cfg(target_os = "linux")] + SpeechDispatcher, + #[cfg(all(windows, feature = "tolk"))] + Tolk, + #[cfg(target_arch = "wasm32")] + Web, + #[cfg(windows)] + WinRt, } -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +impl fmt::Display for Backends { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + #[cfg(target_os = "android")] + Backends::Android => writeln!(f, "Android"), + #[cfg(target_os = "macos")] + Backends::AppKit => writeln!(f, "AppKit"), + #[cfg(any(target_os = "macos", target_os = "ios"))] + Backends::AvFoundation => writeln!(f, "AVFoundation"), + #[cfg(target_os = "linux")] + Backends::SpeechDispatcher => writeln!(f, "Speech Dispatcher"), + #[cfg(all(windows, feature = "tolk"))] + Backends::Tolk => writeln!(f, "Tolk"), + #[cfg(target_arch = "wasm32")] + Backends::Web => writeln!(f, "Web"), + #[cfg(windows)] + Backends::WinRt => writeln!(f, "Windows Runtime"), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum BackendId { - #[cfg(target_os = "linux")] - SpeechDispatcher(u64), - #[cfg(target_arch = "wasm32")] - Web(u64), - #[cfg(windows)] - WinRt(u64), + #[cfg(target_os = "android")] + Android(u64), #[cfg(any(target_os = "macos", target_os = "ios"))] AvFoundation(u64), - #[cfg(target_os = "android")] - Android(u64), -} - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum UtteranceId { #[cfg(target_os = "linux")] SpeechDispatcher(u64), #[cfg(target_arch = "wasm32")] Web(u64), #[cfg(windows)] WinRt(u64), - #[cfg(any(target_os = "macos", target_os = "ios"))] - AvFoundation(id), +} + +impl fmt::Display for BackendId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + #[cfg(target_os = "android")] + BackendId::Android(id) => writeln!(f, "{}", id), + #[cfg(any(target_os = "macos", target_os = "ios"))] + BackendId::AvFoundation(id) => writeln!(f, "{}", id), + #[cfg(target_os = "linux")] + BackendId::SpeechDispatcher(id) => writeln!(f, "{}", id), + #[cfg(target_arch = "wasm32")] + BackendId::Web(id) => writeln!(f, "Web({})", id), + #[cfg(windows)] + BackendId::WinRt(id) => writeln!(f, "{}", id), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum UtteranceId { #[cfg(target_os = "android")] Android(u64), + #[cfg(any(target_os = "macos", target_os = "ios"))] + AvFoundation(id), + #[cfg(target_os = "linux")] + SpeechDispatcher(u64), + #[cfg(target_arch = "wasm32")] + Web(u64), + #[cfg(windows)] + WinRt(u64), +} + +impl fmt::Display for UtteranceId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + #[cfg(target_os = "android")] + UtteranceId::Android(id) => writeln!(f, "{}", id), + #[cfg(any(target_os = "macos", target_os = "ios"))] + UtteranceId::AvFoundation(id) => writeln!(f, "{}", id), + #[cfg(target_os = "linux")] + UtteranceId::SpeechDispatcher(id) => writeln!(f, "{}", id), + #[cfg(target_arch = "wasm32")] + UtteranceId::Web(id) => writeln!(f, "Web({})", id), + #[cfg(windows)] + UtteranceId::WinRt(id) => writeln!(f, "{}", id), + } + } } unsafe impl Send for UtteranceId {} @@ -122,6 +180,7 @@ impl Features { } #[derive(Debug, Error)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Error { #[error("IO error: {0}")] Io(#[from] std::io::Error), From 5331bc8daf0bc431676a9c7369d2ee36602e49f1 Mon Sep 17 00:00:00 2001 From: Raimundo Saona <37874270+saona-raimundo@users.noreply.github.com> Date: Thu, 23 Dec 2021 11:12:35 +0100 Subject: [PATCH 31/98] Undo implementation of serde traits for Error --- src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 8bb1141..9284ccb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -180,7 +180,6 @@ impl Features { } #[derive(Debug, Error)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Error { #[error("IO error: {0}")] Io(#[from] std::io::Error), From 2ea472e196fb93a52254a44ef0e956683df4f87d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 28 Dec 2021 10:09:54 -0600 Subject: [PATCH 32/98] Bump Windows dependency, crate version, and remove `Debug` derive. --- Cargo.toml | 4 ++-- src/backends/winrt.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ef4beef..68332a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.19.1" +version = "0.19.2" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -22,7 +22,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.28", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.29", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.9" diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index e7c8634..943a78c 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -21,7 +21,7 @@ impl From for Error { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct WinRt { id: BackendId, synth: SpeechSynthesizer, From 114fb55fc98f18cfb0a83a3fa92ec605d24142e5 Mon Sep 17 00:00:00 2001 From: Raimundo Saona <37874270+saona-raimundo@users.noreply.github.com> Date: Tue, 4 Jan 2022 13:43:32 +0100 Subject: [PATCH 33/98] Fixing macos and ios restrictions --- src/lib.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9284ccb..0d35ba0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -107,8 +107,19 @@ impl fmt::Display for BackendId { } } -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +// # Note +// +// Most trait implementations are blocked by cocoa_foundation::base::id; +// which is a type alias for objc::runtime::Object, which only implements Debug. +#[derive(Debug)] +#[cfg_attr( + not(any(target_os = "macos", target_os = "ios")), + derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord) +)] +#[cfg_attr( + all(feature = "serde", not(any(target_os = "macos", target_os = "ios"))), + derive(serde::Serialize, serde::Deserialize) +)] pub enum UtteranceId { #[cfg(target_os = "android")] Android(u64), @@ -122,13 +133,16 @@ pub enum UtteranceId { WinRt(u64), } +// # Note +// +// Display is not implemented by cocoa_foundation::base::id; +// which is a type alias for objc::runtime::Object, which only implements Debug. +#[cfg(not(any(target_os = "macos", target_os = "ios")))] impl fmt::Display for UtteranceId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { #[cfg(target_os = "android")] UtteranceId::Android(id) => writeln!(f, "{}", id), - #[cfg(any(target_os = "macos", target_os = "ios"))] - UtteranceId::AvFoundation(id) => writeln!(f, "{}", id), #[cfg(target_os = "linux")] UtteranceId::SpeechDispatcher(id) => writeln!(f, "{}", id), #[cfg(target_arch = "wasm32")] From cdc225418e82135b781ceb2374432667027bb0f2 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 10 Jan 2022 10:40:24 -0600 Subject: [PATCH 34/98] Bump Android dependencies. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2859f0a..59b6da4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,4 +39,4 @@ web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "Spee [target.'cfg(target_os="android")'.dependencies] jni = "0.19" -ndk-glue = "0.5" \ No newline at end of file +ndk-glue = "0.6" \ No newline at end of file From 9066d2b00510f2d9f2059760b79347fefcb16cee Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 10 Jan 2022 10:51:18 -0600 Subject: [PATCH 35/98] Make formatting more consistent. --- src/backends/android.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/backends/android.rs b/src/backends/android.rs index c47d86a..5856890 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -1,14 +1,18 @@ #[cfg(target_os = "android")] -use std::collections::HashSet; -use std::ffi::{CStr, CString}; -use std::os::raw::c_void; -use std::sync::{Mutex, RwLock}; -use std::thread; -use std::time::{Duration, Instant}; +use std::{ + collections::HashSet, + ffi::{CStr, CString}, + os::raw::c_void, + sync::{Mutex, RwLock}, + thread, + time::{Duration, Instant}, +}; -use jni::objects::{GlobalRef, JObject, JString}; -use jni::sys::{jfloat, jint, JNI_VERSION_1_6}; -use jni::{JNIEnv, JavaVM}; +use jni::{ + objects::{GlobalRef, JObject, JString}, + sys::{jfloat, jint, JNI_VERSION_1_6}, + JNIEnv, JavaVM, +}; use lazy_static::lazy_static; use log::{error, info}; From 1f466275cfa365acc400616cae010625aceeb19e Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 10 Jan 2022 10:52:44 -0600 Subject: [PATCH 36/98] Bump ndk-glue version once more. --- examples/android/cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/android/cargo.toml b/examples/android/cargo.toml index 3585cca..0b565e8 100644 --- a/examples/android/cargo.toml +++ b/examples/android/cargo.toml @@ -10,5 +10,5 @@ edition = "2018" crate-type = ["dylib"] [dependencies] -ndk-glue = "0.5" +ndk-glue = "0.6" tts = { path = "../.." } \ No newline at end of file From dc00aa427f3ec4382dc7af18ab7494476f706723 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 10 Jan 2022 11:02:12 -0600 Subject: [PATCH 37/98] Bump speech-dispatcher dependency and update for new return types. --- Cargo.toml | 2 +- src/backends/speech_dispatcher.rs | 12 ++++++------ src/lib.rs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 59b6da4..5f081d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ tolk = { version = "0.5", optional = true } windows = { version = "0.29", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] -speech-dispatcher = "0.9" +speech-dispatcher = "0.10" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] cocoa-foundation = "0.1" diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index bd373c4..57ce7b2 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -92,11 +92,11 @@ impl Backend for SpeechDispatcher { } let single_char = text.to_string().capacity() == 1; if single_char { - self.0.set_punctuation(Punctuation::All); + self.0.set_punctuation(Punctuation::All)?; } let id = self.0.say(Priority::Important, text); if single_char { - self.0.set_punctuation(Punctuation::None); + self.0.set_punctuation(Punctuation::None)?; } if let Some(id) = id { Ok(Some(UtteranceId::SpeechDispatcher(id))) @@ -107,7 +107,7 @@ impl Backend for SpeechDispatcher { fn stop(&mut self) -> Result<(), Error> { trace!("stop()"); - self.0.cancel(); + self.0.cancel()?; Ok(()) } @@ -128,7 +128,7 @@ impl Backend for SpeechDispatcher { } fn set_rate(&mut self, rate: f32) -> Result<(), Error> { - self.0.set_voice_rate(rate as i32); + self.0.set_voice_rate(rate as i32)?; Ok(()) } @@ -149,7 +149,7 @@ impl Backend for SpeechDispatcher { } fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> { - self.0.set_voice_pitch(pitch as i32); + self.0.set_voice_pitch(pitch as i32)?; Ok(()) } @@ -170,7 +170,7 @@ impl Backend for SpeechDispatcher { } fn set_volume(&mut self, volume: f32) -> Result<(), Error> { - self.0.set_volume(volume as i32); + self.0.set_volume(volume as i32)?; Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 0d35ba0..0962396 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,7 +28,7 @@ use libc::c_char; #[cfg(target_os = "macos")] use objc::{class, msg_send, sel, sel_impl}; #[cfg(target_os = "linux")] -use speech_dispatcher::SpeechDispatcherError; +use speech_dispatcher::{Error as SpeechDispatcherError}; use thiserror::Error; #[cfg(all(windows, feature = "tolk"))] use tolk::Tolk; From 050b97fde1f19aa03c96457fd9dfe2fb062e70a8 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 10 Jan 2022 11:03:18 -0600 Subject: [PATCH 38/98] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5f081d5..3c9ff98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.19.2" +version = "0.20.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 1e1c04d4e51231ca0d7beadc5f4789faf0f0f55e Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 10 Jan 2022 11:10:18 -0600 Subject: [PATCH 39/98] Fix rustfmt error. --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 0962396..b157cf7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,7 +28,7 @@ use libc::c_char; #[cfg(target_os = "macos")] use objc::{class, msg_send, sel, sel_impl}; #[cfg(target_os = "linux")] -use speech_dispatcher::{Error as SpeechDispatcherError}; +use speech_dispatcher::Error as SpeechDispatcherError; use thiserror::Error; #[cfg(all(windows, feature = "tolk"))] use tolk::Tolk; From 660072809db9863ba44ec30215d8df99d76f98e0 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 13 Jan 2022 14:28:22 -0600 Subject: [PATCH 40/98] Bump version and windows-rs dependency. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3c9ff98..173f027 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.20.0" +version = "0.20.1" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -23,7 +23,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.29", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.30", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.10" From 44669236204e8c80b5662456be9bbaa87df76f0d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 27 Jan 2022 10:56:11 -0600 Subject: [PATCH 41/98] Bump speech-dispatcher dependency. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 173f027..0de8652 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ tolk = { version = "0.5", optional = true } windows = { version = "0.30", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] -speech-dispatcher = "0.10" +speech-dispatcher = "0.11" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] cocoa-foundation = "0.1" From cd2216390c68851aa68a072fc00f62fa7dbb7fcb Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Sat, 5 Feb 2022 09:48:25 -0600 Subject: [PATCH 42/98] Bump dependency and version. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0de8652..710ec4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.20.1" +version = "0.20.2" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -26,7 +26,7 @@ tolk = { version = "0.5", optional = true } windows = { version = "0.30", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] -speech-dispatcher = "0.11" +speech-dispatcher = "0.12" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] cocoa-foundation = "0.1" From 9f8b670fe036d47d410af00a243bc56d1cf5f090 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 11 Feb 2022 09:17:46 -0600 Subject: [PATCH 43/98] Bump dependency. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 710ec4a..9c83f2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.30", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.32", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.12" From f310306508f274b92a42f99fee942b10c172e642 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 11 Feb 2022 09:20:22 -0600 Subject: [PATCH 44/98] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9c83f2b..b4cf23e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.20.2" +version = "0.20.3" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 90f7dae6a119f96f54ae4bc469d3d723dd961a80 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 7 Mar 2022 10:31:37 -0600 Subject: [PATCH 45/98] Bump windows dependency and version. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b4cf23e..a15c441 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.20.3" +version = "0.20.4" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -23,7 +23,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.32", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.33", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.12" From 00bd5e62ff419bfa02d4e471afbe8cefd55ec571 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 7 Mar 2022 17:54:26 -0600 Subject: [PATCH 46/98] Switch `TTS` to use `Arc>>` to address soundness issues. --- src/lib.rs | 68 +++++++++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b157cf7..bfda4bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,12 +12,12 @@ * * WebAssembly */ -use std::boxed::Box; use std::collections::HashMap; #[cfg(target_os = "macos")] use std::ffi::CStr; use std::fmt; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; +use std::{boxed::Box, sync::RwLock}; #[cfg(any(target_os = "macos", target_os = "ios"))] use cocoa_foundation::base::id; @@ -262,7 +262,7 @@ lazy_static! { } #[derive(Clone)] -pub struct Tts(Box); +pub struct Tts(Arc>>); unsafe impl Send for Tts {} @@ -296,7 +296,7 @@ impl Tts { #[cfg(windows)] Backends::WinRt => { let tts = backends::WinRt::new()?; - Ok(Tts(Box::new(tts))) + Ok(Tts(Arc::new(RwLock::new(Box::new(tts))))) } #[cfg(target_os = "macos")] Backends::AppKit => Ok(Tts(Box::new(backends::AppKit::new()))), @@ -309,7 +309,7 @@ impl Tts { } }; if let Ok(backend) = backend { - if let Some(id) = backend.0.id() { + if let Some(id) = backend.0.read().unwrap().id() { let mut callbacks = CALLBACKS.lock().unwrap(); callbacks.insert(id, Callbacks::default()); } @@ -362,7 +362,7 @@ impl Tts { * Returns the features supported by this TTS engine */ pub fn supported_features(&self) -> Features { - self.0.supported_features() + self.0.read().unwrap().supported_features() } /** @@ -373,7 +373,10 @@ impl Tts { text: S, interrupt: bool, ) -> Result, Error> { - self.0.speak(text.into().as_str(), interrupt) + self.0 + .write() + .unwrap() + .speak(text.into().as_str(), interrupt) } /** @@ -382,7 +385,7 @@ impl Tts { pub fn stop(&mut self) -> Result<&Self, Error> { let Features { stop, .. } = self.supported_features(); if stop { - self.0.stop()?; + self.0.write().unwrap().stop()?; Ok(self) } else { Err(Error::UnsupportedFeature) @@ -393,21 +396,21 @@ impl Tts { * Returns the minimum rate for this speech synthesizer. */ pub fn min_rate(&self) -> f32 { - self.0.min_rate() + self.0.read().unwrap().min_rate() } /** * Returns the maximum rate for this speech synthesizer. */ pub fn max_rate(&self) -> f32 { - self.0.max_rate() + self.0.read().unwrap().max_rate() } /** * Returns the normal rate for this speech synthesizer. */ pub fn normal_rate(&self) -> f32 { - self.0.normal_rate() + self.0.read().unwrap().normal_rate() } /** @@ -416,7 +419,7 @@ impl Tts { pub fn get_rate(&self) -> Result { let Features { rate, .. } = self.supported_features(); if rate { - self.0.get_rate() + self.0.read().unwrap().get_rate() } else { Err(Error::UnsupportedFeature) } @@ -430,10 +433,11 @@ impl Tts { rate: rate_feature, .. } = self.supported_features(); if rate_feature { - if rate < self.0.min_rate() || rate > self.0.max_rate() { + let mut backend = self.0.write().unwrap(); + if rate < backend.min_rate() || rate > backend.max_rate() { Err(Error::OutOfRange) } else { - self.0.set_rate(rate)?; + backend.set_rate(rate)?; Ok(self) } } else { @@ -445,21 +449,21 @@ impl Tts { * Returns the minimum pitch for this speech synthesizer. */ pub fn min_pitch(&self) -> f32 { - self.0.min_pitch() + self.0.read().unwrap().min_pitch() } /** * Returns the maximum pitch for this speech synthesizer. */ pub fn max_pitch(&self) -> f32 { - self.0.max_pitch() + self.0.read().unwrap().max_pitch() } /** * Returns the normal pitch for this speech synthesizer. */ pub fn normal_pitch(&self) -> f32 { - self.0.normal_pitch() + self.0.read().unwrap().normal_pitch() } /** @@ -468,7 +472,7 @@ impl Tts { pub fn get_pitch(&self) -> Result { let Features { pitch, .. } = self.supported_features(); if pitch { - self.0.get_pitch() + self.0.read().unwrap().get_pitch() } else { Err(Error::UnsupportedFeature) } @@ -483,10 +487,11 @@ impl Tts { .. } = self.supported_features(); if pitch_feature { - if pitch < self.0.min_pitch() || pitch > self.0.max_pitch() { + let mut backend = self.0.write().unwrap(); + if pitch < backend.min_pitch() || pitch > backend.max_pitch() { Err(Error::OutOfRange) } else { - self.0.set_pitch(pitch)?; + backend.set_pitch(pitch)?; Ok(self) } } else { @@ -498,21 +503,21 @@ impl Tts { * Returns the minimum volume for this speech synthesizer. */ pub fn min_volume(&self) -> f32 { - self.0.min_volume() + self.0.read().unwrap().min_volume() } /** * Returns the maximum volume for this speech synthesizer. */ pub fn max_volume(&self) -> f32 { - self.0.max_volume() + self.0.read().unwrap().max_volume() } /** * Returns the normal volume for this speech synthesizer. */ pub fn normal_volume(&self) -> f32 { - self.0.normal_volume() + self.0.read().unwrap().normal_volume() } /** @@ -521,7 +526,7 @@ impl Tts { pub fn get_volume(&self) -> Result { let Features { volume, .. } = self.supported_features(); if volume { - self.0.get_volume() + self.0.read().unwrap().get_volume() } else { Err(Error::UnsupportedFeature) } @@ -536,10 +541,11 @@ impl Tts { .. } = self.supported_features(); if volume_feature { - if volume < self.0.min_volume() || volume > self.0.max_volume() { + let mut backend = self.0.write().unwrap(); + if volume < backend.min_volume() || volume > backend.max_volume() { Err(Error::OutOfRange) } else { - self.0.set_volume(volume)?; + backend.set_volume(volume)?; Ok(self) } } else { @@ -553,7 +559,7 @@ impl Tts { pub fn is_speaking(&self) -> Result { let Features { is_speaking, .. } = self.supported_features(); if is_speaking { - self.0.is_speaking() + self.0.read().unwrap().is_speaking() } else { Err(Error::UnsupportedFeature) } @@ -572,7 +578,7 @@ impl Tts { } = self.supported_features(); if utterance_callbacks { let mut callbacks = CALLBACKS.lock().unwrap(); - let id = self.0.id().unwrap(); + let id = self.0.read().unwrap().id().unwrap(); let mut callbacks = callbacks.get_mut(&id).unwrap(); callbacks.utterance_begin = callback; Ok(()) @@ -594,7 +600,7 @@ impl Tts { } = self.supported_features(); if utterance_callbacks { let mut callbacks = CALLBACKS.lock().unwrap(); - let id = self.0.id().unwrap(); + let id = self.0.read().unwrap().id().unwrap(); let mut callbacks = callbacks.get_mut(&id).unwrap(); callbacks.utterance_end = callback; Ok(()) @@ -616,7 +622,7 @@ impl Tts { } = self.supported_features(); if utterance_callbacks { let mut callbacks = CALLBACKS.lock().unwrap(); - let id = self.0.id().unwrap(); + let id = self.0.read().unwrap().id().unwrap(); let mut callbacks = callbacks.get_mut(&id).unwrap(); callbacks.utterance_stop = callback; Ok(()) @@ -646,7 +652,7 @@ impl Tts { impl Drop for Tts { fn drop(&mut self) { - if let Some(id) = self.0.id() { + if let Some(id) = self.0.read().unwrap().id() { let mut callbacks = CALLBACKS.lock().unwrap(); callbacks.remove(&id); } From 31309553bb9933b645fd2ee48a7ed578fac0f423 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 7 Mar 2022 17:56:28 -0600 Subject: [PATCH 47/98] Ditto for Linux. --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index bfda4bd..c8abae7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -277,7 +277,7 @@ impl Tts { #[cfg(target_os = "linux")] Backends::SpeechDispatcher => { let tts = backends::SpeechDispatcher::new()?; - Ok(Tts(Box::new(tts))) + Ok(Tts(Arc::new(RwLock::new(Box::new(tts))))) } #[cfg(target_arch = "wasm32")] Backends::Web => { From 594479498039929021dfbe70f3f72cae63c14bb8 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 7 Mar 2022 18:31:25 -0600 Subject: [PATCH 48/98] Add `Arc>` for remaining platforms. --- src/lib.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c8abae7..62649fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -282,13 +282,13 @@ impl Tts { #[cfg(target_arch = "wasm32")] Backends::Web => { let tts = backends::Web::new()?; - Ok(Tts(Box::new(tts))) + Ok(Tts(Arc::new(RwLock::new(Box::new(tts))))) } #[cfg(all(windows, feature = "tolk"))] Backends::Tolk => { let tts = backends::Tolk::new(); if let Some(tts) = tts { - Ok(Tts(Box::new(tts))) + Ok(Tts(Arc::new(RwLock::new(Box::new(tts))))) } else { Err(Error::NoneError) } @@ -299,13 +299,17 @@ impl Tts { Ok(Tts(Arc::new(RwLock::new(Box::new(tts))))) } #[cfg(target_os = "macos")] - Backends::AppKit => Ok(Tts(Box::new(backends::AppKit::new()))), + Backends::AppKit => Ok(Tts(Arc::new(RwLock::new( + Box::new(backends::AppKit::new()), + )))), #[cfg(any(target_os = "macos", target_os = "ios"))] - Backends::AvFoundation => Ok(Tts(Box::new(backends::AvFoundation::new()))), + Backends::AvFoundation => Ok(Tts(Arc::new(RwLock::new(Box::new( + backends::AvFoundation::new(), + ))))), #[cfg(target_os = "android")] Backends::Android => { let tts = backends::Android::new()?; - Ok(Tts(Box::new(tts))) + Ok(Tts(Arc::new(RwLock::new(Box::new(tts))))) } }; if let Ok(backend) = backend { From 888e6a3dfac3f30ba4cf4ad3db7fa9a0b5623cd9 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 11:12:46 -0600 Subject: [PATCH 49/98] Correctly clean up callback references based on whether `Arc` remains cloned on drop. --- examples/clone_drop.rs | 89 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 8 ++-- 2 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 examples/clone_drop.rs diff --git a/examples/clone_drop.rs b/examples/clone_drop.rs new file mode 100644 index 0000000..9c466ef --- /dev/null +++ b/examples/clone_drop.rs @@ -0,0 +1,89 @@ +use std::io; + +#[cfg(target_os = "macos")] +use cocoa_foundation::base::id; +#[cfg(target_os = "macos")] +use cocoa_foundation::foundation::NSRunLoop; +#[cfg(target_os = "macos")] +use objc::{msg_send, sel, sel_impl}; + +use tts::*; + +fn main() -> Result<(), Error> { + env_logger::init(); + let tts = Tts::default()?; + let mut tts_clone = tts.clone(); + drop(tts); + if Tts::screen_reader_available() { + println!("A screen reader is available on this platform."); + } else { + println!("No screen reader is available on this platform."); + } + let Features { + utterance_callbacks, + .. + } = tts_clone.supported_features(); + if utterance_callbacks { + tts_clone.on_utterance_begin(Some(Box::new(|utterance| { + println!("Started speaking {:?}", utterance) + })))?; + tts_clone.on_utterance_end(Some(Box::new(|utterance| { + println!("Finished speaking {:?}", utterance) + })))?; + tts_clone.on_utterance_stop(Some(Box::new(|utterance| { + println!("Stopped speaking {:?}", utterance) + })))?; + } + let Features { is_speaking, .. } = tts_clone.supported_features(); + if is_speaking { + println!("Are we speaking? {}", tts_clone.is_speaking()?); + } + tts_clone.speak("Hello, world.", false)?; + let Features { rate, .. } = tts_clone.supported_features(); + if rate { + let original_rate = tts_clone.get_rate()?; + tts_clone.speak(format!("Current rate: {}", original_rate), false)?; + tts_clone.set_rate(tts_clone.max_rate())?; + tts_clone.speak("This is very fast.", false)?; + tts_clone.set_rate(tts_clone.min_rate())?; + tts_clone.speak("This is very slow.", false)?; + tts_clone.set_rate(tts_clone.normal_rate())?; + tts_clone.speak("This is the normal rate.", false)?; + tts_clone.set_rate(original_rate)?; + } + let Features { pitch, .. } = tts_clone.supported_features(); + if pitch { + let original_pitch = tts_clone.get_pitch()?; + tts_clone.set_pitch(tts_clone.max_pitch())?; + tts_clone.speak("This is high-pitch.", false)?; + tts_clone.set_pitch(tts_clone.min_pitch())?; + tts_clone.speak("This is low pitch.", false)?; + tts_clone.set_pitch(tts_clone.normal_pitch())?; + tts_clone.speak("This is normal pitch.", false)?; + tts_clone.set_pitch(original_pitch)?; + } + let Features { volume, .. } = tts_clone.supported_features(); + if volume { + let original_volume = tts_clone.get_volume()?; + tts_clone.set_volume(tts_clone.max_volume())?; + tts_clone.speak("This is loud!", false)?; + tts_clone.set_volume(tts_clone.min_volume())?; + tts_clone.speak("This is quiet.", false)?; + tts_clone.set_volume(tts_clone.normal_volume())?; + tts_clone.speak("This is normal volume.", false)?; + tts_clone.set_volume(original_volume)?; + } + tts_clone.speak("Goodbye.", false)?; + let mut _input = String::new(); + // The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context. + // It shouldn't be needed in an app or game that almost certainly has one already. + #[cfg(target_os = "macos")] + { + let run_loop: id = unsafe { NSRunLoop::currentRunLoop() }; + unsafe { + let _: () = msg_send![run_loop, run]; + } + } + io::stdin().read_line(&mut _input)?; + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 62649fa..4e3f47e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -656,9 +656,11 @@ impl Tts { impl Drop for Tts { fn drop(&mut self) { - if let Some(id) = self.0.read().unwrap().id() { - let mut callbacks = CALLBACKS.lock().unwrap(); - callbacks.remove(&id); + if Arc::strong_count(&self.0) <= 1 { + if let Some(id) = self.0.read().unwrap().id() { + let mut callbacks = CALLBACKS.lock().unwrap(); + callbacks.remove(&id); + } } } } From cc8fd91c8691ca3fde7fa3bd245304ab8ad211c4 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 11:44:59 -0600 Subject: [PATCH 50/98] Set event callbacks on pre-clone value to ensure that they remain alive. --- examples/clone_drop.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/clone_drop.rs b/examples/clone_drop.rs index 9c466ef..5b3dd0f 100644 --- a/examples/clone_drop.rs +++ b/examples/clone_drop.rs @@ -12,8 +12,6 @@ use tts::*; fn main() -> Result<(), Error> { env_logger::init(); let tts = Tts::default()?; - let mut tts_clone = tts.clone(); - drop(tts); if Tts::screen_reader_available() { println!("A screen reader is available on this platform."); } else { @@ -22,18 +20,20 @@ fn main() -> Result<(), Error> { let Features { utterance_callbacks, .. - } = tts_clone.supported_features(); + } = tts.supported_features(); if utterance_callbacks { - tts_clone.on_utterance_begin(Some(Box::new(|utterance| { + tts.on_utterance_begin(Some(Box::new(|utterance| { println!("Started speaking {:?}", utterance) })))?; - tts_clone.on_utterance_end(Some(Box::new(|utterance| { + tts.on_utterance_end(Some(Box::new(|utterance| { println!("Finished speaking {:?}", utterance) })))?; - tts_clone.on_utterance_stop(Some(Box::new(|utterance| { + tts.on_utterance_stop(Some(Box::new(|utterance| { println!("Stopped speaking {:?}", utterance) })))?; } + let mut tts_clone = tts.clone(); + drop(tts); let Features { is_speaking, .. } = tts_clone.supported_features(); if is_speaking { println!("Are we speaking? {}", tts_clone.is_speaking()?); From ef0a78c745f8eebcf6e488d9d3ce4b8bc5e28a5a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 13:46:14 -0600 Subject: [PATCH 51/98] Bump speech-dispatcher, and support building with multiple versions. --- Cargo.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a15c441..d94fe04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,10 @@ edition = "2021" [lib] crate-type = ["lib", "cdylib", "staticlib"] +[features] +speech_dispatcher_0_10 = ["speech-dispatcher/0_10"] +default = ["speech_dispatcher_0_10"] + [dependencies] dyn-clonable = "0.9" lazy_static = "1" @@ -26,7 +30,7 @@ tolk = { version = "0.5", optional = true } windows = { version = "0.33", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] -speech-dispatcher = "0.12" +speech-dispatcher = { version = "0.13", default-features = false } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] cocoa-foundation = "0.1" From f275e506df85a1ee7b07c757dd599e991a51c1ab Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 13:57:05 -0600 Subject: [PATCH 52/98] Disable default-features on Linux since runners don't have speech-dispatcher 0.10 or greater. --- .github/workflows/test.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ff68b02..6cdf4d7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,10 +26,16 @@ jobs: with: command: check args: --all-features --examples + if: ${{ runner.os != 'Linux' }} + - uses: actions-rs/cargo@v1 + with: + command: check + args: --no-default-features --examples + if: ${{ runner.os == 'Linux' }} - uses: actions-rs/cargo@v1 with: command: fmt - args: --all -- --check + args: --all --check - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -55,7 +61,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: fmt - args: --all -- --check + args: --all --check - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} From 5c9c6495056d2c8f07f13cdfb615718038a7640a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 14:07:18 -0600 Subject: [PATCH 53/98] Also disable default features for Linux when running Clippy. --- .github/workflows/test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6cdf4d7..0471d3c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,6 +40,12 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} args: --all-features + if: ${{ runner.os != 'Linux' }} + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --no-default-features + if: ${{ runner.os == 'Linux' }} check_web: name: Check Web From 539003205e378406344a0b107b0fe8184b2259f5 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 14:08:13 -0600 Subject: [PATCH 54/98] Appease Clippy. --- src/lib.rs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4e3f47e..af041d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -157,7 +157,7 @@ unsafe impl Send for UtteranceId {} unsafe impl Sync for UtteranceId {} -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Features { pub is_speaking: bool, @@ -168,19 +168,6 @@ pub struct Features { pub volume: bool, } -impl Default for Features { - fn default() -> Self { - Self { - stop: false, - rate: false, - pitch: false, - volume: false, - is_speaking: false, - utterance_callbacks: false, - } - } -} - impl fmt::Display for Features { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { writeln!(f, "{:#?}", self) From 3366f93e2b27d70e469692259f11e191a24f56b8 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 14:14:03 -0600 Subject: [PATCH 55/98] Fix release workflow to not build default features. --- .github/workflows/release.yml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 06b847b..89e04ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,14 +27,32 @@ jobs: with: command: check args: --all-features --examples + if: ${{ runner.os != 'Linux' }} + - uses: actions-rs/cargo@v1 + with: + command: check + args: --no-default-features --examples + if: ${{ runner.os == 'Linux' }} - uses: actions-rs/cargo@v1 with: command: fmt - args: --all -- --check + args: --all --check + if: ${{ runner.os == 'Linux' }} + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all --no-default-features --check + if: ${{ runner.os == 'Linux' }} - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} args: --all-features + if: ${{ runner.os != 'Linux' }} + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --no-default-features + if: ${{ runner.os == 'Linux' }} check_web: name: Check Web @@ -88,4 +106,4 @@ jobs: sudo apt-get update sudo apt-get install -y libspeechd-dev cargo login $CARGO_TOKEN - cargo publish + cargo publish --no-default-features From b435c8923975a1f55c89914d06ce5d66dc76955d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 14:14:47 -0600 Subject: [PATCH 56/98] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d94fe04..476e954 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.20.4" +version = "0.21.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From fd9f5ae60a37cb1536a450f1031b725b4487493f Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 14:35:46 -0600 Subject: [PATCH 57/98] Branch not needed. --- .github/workflows/release.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89e04ee..a77851c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,12 +37,6 @@ jobs: with: command: fmt args: --all --check - if: ${{ runner.os == 'Linux' }} - - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all --no-default-features --check - if: ${{ runner.os == 'Linux' }} - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} From acecb1f3628f81f1545c846a44cf82ed26fdecc3 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 18 Mar 2022 08:47:06 -0500 Subject: [PATCH 58/98] Bump windows dependency and crate version. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 476e954..c3d5adc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.21.0" +version = "0.21.1" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -27,7 +27,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.33", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.34", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = { version = "0.13", default-features = false } From c222c087b289e403b743f892764969e5c0551c9e Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Mar 2022 10:18:22 -0500 Subject: [PATCH 59/98] cargo fmt --- examples/hello_world.rs | 4 ++-- src/backends/appkit.rs | 4 ++-- src/backends/av_foundation.rs | 11 +++++---- src/backends/av_foundation/voices.rs | 34 +++++++++++++++------------- src/backends/speech_dispatcher.rs | 4 ++-- src/backends/tolk.rs | 4 ++-- src/backends/web.rs | 4 ++-- src/backends/winrt.rs | 4 ++-- src/lib.rs | 8 +++---- src/voices.rs | 1 - 10 files changed, 41 insertions(+), 37 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 18f5869..3885ede 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -77,11 +77,11 @@ fn main() -> Result<(), Error> { let voices_list = tts.list_voices(); println!("Available voices:\n==="); for v in voices_list.iter() { - println!("{}",v); + println!("{}", v); tts.set_voice(v)?; println!("voice set"); println!("{}", tts.voice()?); - tts.speak(v,false)?; + tts.speak(v, false)?; } tts.set_voice(original_voice)?; } diff --git a/src/backends/appkit.rs b/src/backends/appkit.rs index 44658d7..389418d 100644 --- a/src/backends/appkit.rs +++ b/src/backends/appkit.rs @@ -201,7 +201,7 @@ impl Backend for AppKit { Ok(is_speaking != NO as i8) } - fn voice(&self) -> Result { + fn voice(&self) -> Result { unimplemented!() } @@ -209,7 +209,7 @@ impl Backend for AppKit { unimplemented!() } - fn set_voice(&mut self, voice: &str) -> Result<(),Error> { + fn set_voice(&mut self, voice: &str) -> Result<(), Error> { unimplemented!() } } diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index fcedf6f..9e79a47 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -9,8 +9,8 @@ use log::{info, trace}; use objc::runtime::{Object, Sel}; use objc::{class, declare::ClassDecl, msg_send, sel, sel_impl}; -use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; use crate::voices::Backend as VoiceBackend; +use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; mod voices; use voices::*; @@ -280,15 +280,18 @@ impl Backend for AvFoundation { Ok(is_speaking != NO as i8) } - fn voice(&self) -> Result { + fn voice(&self) -> Result { Ok(self.voice.id()) } fn list_voices(&self) -> Vec { - AVSpeechSynthesisVoice::list().iter().map(|v| {v.id()}).collect() + AVSpeechSynthesisVoice::list() + .iter() + .map(|v| v.id()) + .collect() } - fn set_voice(&mut self, voice: &str) -> Result<(),Error> { + fn set_voice(&mut self, voice: &str) -> Result<(), Error> { self.voice = AVSpeechSynthesisVoice::new(); Ok(()) } diff --git a/src/backends/av_foundation/voices.rs b/src/backends/av_foundation/voices.rs index e6ae49a..836adc7 100644 --- a/src/backends/av_foundation/voices.rs +++ b/src/backends/av_foundation/voices.rs @@ -1,25 +1,24 @@ - +use cocoa_foundation::base::{id, nil}; +use cocoa_foundation::foundation::NSString; +use core_foundation::array::CFArray; +use core_foundation::string::CFString; use objc::runtime::*; use objc::*; -use core_foundation::array::CFArray; -use cocoa_foundation::foundation::NSString; -use cocoa_foundation::base::{nil,id}; -use core_foundation::string::CFString; use crate::backends::AvFoundation; use crate::voices; use crate::voices::Gender; -#[derive(Copy,Clone, Debug)] +#[derive(Copy, Clone, Debug)] pub(crate) struct AVSpeechSynthesisVoice(*const Object); impl AVSpeechSynthesisVoice { pub fn new() -> Self { let voice: *const Object; - unsafe{ + unsafe { voice = msg_send![class!(AVSpeechSynthesisVoice), new]; }; - AVSpeechSynthesisVoice{0:voice} + AVSpeechSynthesisVoice { 0: voice } } } @@ -35,19 +34,22 @@ impl voices::Backend for AVSpeechSynthesisVoice { } fn list() -> Vec { - let voices: CFArray = unsafe{msg_send![class!(AVSpeechSynthesisVoice), speechVoices]}; - voices.iter().map(|v| { - AVSpeechSynthesisVoice{0: *v as *const Object} - }).collect() + let voices: CFArray = unsafe { msg_send![class!(AVSpeechSynthesisVoice), speechVoices] }; + voices + .iter() + .map(|v| AVSpeechSynthesisVoice { + 0: *v as *const Object, + }) + .collect() } fn name(self) -> String { - let name: CFString = unsafe{msg_send![self.0, name]}; + let name: CFString = unsafe { msg_send![self.0, name] }; name.to_string() } fn gender(self) -> Gender { - let gender: i64 = unsafe{ msg_send![self.0, gender] }; + let gender: i64 = unsafe { msg_send![self.0, gender] }; match gender { 1 => Gender::Male, 2 => Gender::Female, @@ -56,12 +58,12 @@ impl voices::Backend for AVSpeechSynthesisVoice { } fn id(self) -> String { - let identifier: CFString = unsafe{msg_send![self.0, identifier]}; + let identifier: CFString = unsafe { msg_send![self.0, identifier] }; identifier.to_string() } fn language(self) -> voices::LanguageIdentifier { - let lang: CFString = unsafe{msg_send![self.0, language]}; + let lang: CFString = unsafe { msg_send![self.0, language] }; lang.to_string().parse().unwrap() } } diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 15ec937..4dc1565 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -181,7 +181,7 @@ impl Backend for SpeechDispatcher { Ok(*is_speaking) } - fn voice(&self) -> Result { + fn voice(&self) -> Result { unimplemented!() } @@ -189,7 +189,7 @@ impl Backend for SpeechDispatcher { unimplemented!() } - fn set_voice(&mut self, voice: &str) -> Result<(),Error> { + fn set_voice(&mut self, voice: &str) -> Result<(), Error> { unimplemented!() } } diff --git a/src/backends/tolk.rs b/src/backends/tolk.rs index e324525..45e2f0b 100644 --- a/src/backends/tolk.rs +++ b/src/backends/tolk.rs @@ -109,7 +109,7 @@ impl Backend for Tolk { unimplemented!() } - fn voice(&self) -> Result { + fn voice(&self) -> Result { unimplemented!() } @@ -117,7 +117,7 @@ impl Backend for Tolk { unimplemented!() } - fn set_voice(&mut self, voice: &str) -> Result<(),Error> { + fn set_voice(&mut self, voice: &str) -> Result<(), Error> { unimplemented!() } } diff --git a/src/backends/web.rs b/src/backends/web.rs index 3bc3c2e..57a8af3 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -198,7 +198,7 @@ impl Backend for Web { } } - fn voice(&self) -> Result { + fn voice(&self) -> Result { unimplemented!() } @@ -206,7 +206,7 @@ impl Backend for Web { unimplemented!() } - fn set_voice(&mut self, voice: &str) -> Result<(),Error> { + fn set_voice(&mut self, voice: &str) -> Result<(), Error> { unimplemented!() } } diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index c573766..dbacbc6 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -291,7 +291,7 @@ impl Backend for WinRt { Ok(!utterances.is_empty()) } - fn voice(&self) -> Result { + fn voice(&self) -> Result { unimplemented!() } @@ -299,7 +299,7 @@ impl Backend for WinRt { unimplemented!() } - fn set_voice(&mut self, voice: &str) -> Result<(),Error> { + fn set_voice(&mut self, voice: &str) -> Result<(), Error> { unimplemented!() } } diff --git a/src/lib.rs b/src/lib.rs index 82ad5bb..72e1ecd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -232,7 +232,7 @@ pub trait Backend: Clone { fn is_speaking(&self) -> Result; fn voice(&self) -> Result; fn list_voices(&self) -> Vec; - fn set_voice(&mut self, voice: &str) -> Result<(),Error>; + fn set_voice(&mut self, voice: &str) -> Result<(), Error>; } #[derive(Default)] @@ -569,9 +569,9 @@ impl Tts { } /** - * Return the current speaking voice. + * Return the current speaking voice. */ - pub fn voice(&self) -> Result { + pub fn voice(&self) -> Result { let Features { voices, .. } = self.supported_features(); if voices { self.0.read().unwrap().voice() @@ -583,7 +583,7 @@ impl Tts { /** * Set speaking voice. */ - pub fn set_voice>(&mut self, voice: S) -> Result<(),Error> { + pub fn set_voice>(&mut self, voice: S) -> Result<(), Error> { let Features { voices: voices_feature, .. diff --git a/src/voices.rs b/src/voices.rs index 8ca927d..f4b3490 100644 --- a/src/voices.rs +++ b/src/voices.rs @@ -1,4 +1,3 @@ - pub use unic_langid::LanguageIdentifier; pub enum Gender { From 55f841d8878bd411ec5b7349bca76a5835e5f3a2 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Mar 2022 10:54:30 -0500 Subject: [PATCH 60/98] Merge extra module into main module. --- src/lib.rs | 21 ++++++++++++++++++++- src/voices.rs | 20 -------------------- 2 files changed, 20 insertions(+), 21 deletions(-) delete mode 100644 src/voices.rs diff --git a/src/lib.rs b/src/lib.rs index 72e1ecd..8328edb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ use std::{boxed::Box, sync::RwLock}; #[cfg(any(target_os = "macos", target_os = "ios"))] use cocoa_foundation::base::id; use dyn_clonable::*; +pub use unic_langid::LanguageIdentifier; use lazy_static::lazy_static; #[cfg(target_os = "macos")] use libc::c_char; @@ -34,7 +35,6 @@ use thiserror::Error; use tolk::Tolk; mod backends; -mod voices; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -690,3 +690,22 @@ impl Drop for Tts { } } } + +pub enum Gender { + Other, + Male, + Female, +} + +pub trait VoiceImpl: Sized { + type Backend: crate::Backend; + fn from_id(id: String) -> Self; + fn from_language(lang: LanguageIdentifier) -> Self; + fn list() -> Vec; + fn name(self) -> String; + fn gender(self) -> Gender; + fn id(self) -> String; + fn language(self) -> LanguageIdentifier; +} + +pub struct Voice(Box); diff --git a/src/voices.rs b/src/voices.rs deleted file mode 100644 index f4b3490..0000000 --- a/src/voices.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub use unic_langid::LanguageIdentifier; - -pub enum Gender { - Other, - Male, - Female, -} - -pub trait Backend: Sized { - type Backend: crate::Backend; - fn from_id(id: String) -> Self; - fn from_language(lang: LanguageIdentifier) -> Self; - fn list() -> Vec; - fn name(self) -> String; - fn gender(self) -> Gender; - fn id(self) -> String; - fn language(self) -> LanguageIdentifier; -} - -pub struct Voice(Box); From e56a0da2e52ff5d4dc1c252c48e694648c018763 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Mar 2022 12:07:59 -0500 Subject: [PATCH 61/98] WIP: Reorganize, and try to get working with Speech Dispatcher. --- Cargo.toml | 10 -------- src/backends/speech_dispatcher.rs | 41 ++++++++++++++++++++++++++----- src/lib.rs | 36 +++++++++++++-------------- 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3245b48..326476a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,21 +8,11 @@ license = "MIT" exclude = ["*.cfg", "*.yml"] edition = "2021" -[package.metadata.patch.speech-dispatcher] -version = "0.7.0" -#patches = [ -# "speech-dispatcher.patch" -#] - -#[patch.crates-io] -#speech-dispatcher = { path = './target/patch/speech-dispatcher-0.7.0'} - [lib] crate-type = ["lib", "cdylib", "staticlib"] [features] speech_dispatcher_0_10 = ["speech-dispatcher/0_10"] -default = ["speech_dispatcher_0_10"] [dependencies] dyn-clonable = "0.9" diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 4dc1565..2ccea1d 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -1,11 +1,15 @@ +use std::str::FromStr; #[cfg(target_os = "linux")] use std::{collections::HashMap, sync::Mutex}; use lazy_static::*; use log::{info, trace}; -use speech_dispatcher::*; +use speech_dispatcher::{Voice as SpdVoice, *}; +use unic_langid::{LanguageIdentifier, LanguageIdentifierError}; -use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; +use crate::{ + Backend, BackendId, Error, Features, Gender, UtteranceId, Voice, VoiceImpl, CALLBACKS, +}; #[derive(Clone, Debug)] pub(crate) struct SpeechDispatcher(Connection); @@ -17,6 +21,24 @@ lazy_static! { }; } +impl VoiceImpl for SpdVoice { + fn id(self) -> String { + self.name + } + + fn name(self) -> String { + self.name + } + + fn gender(self) -> Gender { + Gender::Other + } + + fn language(self) -> Result { + LanguageIdentifier::from_str(&self.language) + } +} + impl SpeechDispatcher { pub(crate) fn new() -> std::result::Result { info!("Initializing SpeechDispatcher backend"); @@ -69,7 +91,7 @@ impl SpeechDispatcher { } } -impl Backend for SpeechDispatcher { +impl Backend for SpeechDispatcher { fn id(&self) -> Option { Some(BackendId::SpeechDispatcher(self.0.client_id())) } @@ -181,11 +203,18 @@ impl Backend for SpeechDispatcher { Ok(*is_speaking) } - fn voice(&self) -> Result { - unimplemented!() + fn voices(&self) -> Result>, Error> { + let rv = self + .0 + .list_synthesis_voices()? + .iter() + .cloned() + .map(|v| Voice(Box::new(v))) + .collect::>>(); + Ok(rv) } - fn list_voices(&self) -> Vec { + fn voice(&self) -> Result { unimplemented!() } diff --git a/src/lib.rs b/src/lib.rs index 8328edb..3945c0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,13 +16,13 @@ use std::collections::HashMap; #[cfg(target_os = "macos")] use std::ffi::CStr; use std::fmt; +use std::marker::PhantomData; use std::sync::{Arc, Mutex}; use std::{boxed::Box, sync::RwLock}; #[cfg(any(target_os = "macos", target_os = "ios"))] use cocoa_foundation::base::id; use dyn_clonable::*; -pub use unic_langid::LanguageIdentifier; use lazy_static::lazy_static; #[cfg(target_os = "macos")] use libc::c_char; @@ -33,6 +33,8 @@ use speech_dispatcher::Error as SpeechDispatcherError; use thiserror::Error; #[cfg(all(windows, feature = "tolk"))] use tolk::Tolk; +pub use unic_langid::LanguageIdentifier; +use unic_langid::LanguageIdentifierError; mod backends; @@ -209,7 +211,7 @@ pub enum Error { } #[clonable] -pub trait Backend: Clone { +pub trait Backend: Clone { fn id(&self) -> Option; fn supported_features(&self) -> Features; fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error>; @@ -230,8 +232,8 @@ pub trait Backend: Clone { fn get_volume(&self) -> Result; fn set_volume(&mut self, volume: f32) -> Result<(), Error>; fn is_speaking(&self) -> Result; + fn voices(&self) -> Result>, Error>; fn voice(&self) -> Result; - fn list_voices(&self) -> Vec; fn set_voice(&mut self, voice: &str) -> Result<(), Error>; } @@ -254,22 +256,22 @@ lazy_static! { } #[derive(Clone)] -pub struct Tts(Arc>>); +pub struct Tts(Arc>>>, PhantomData); -unsafe impl Send for Tts {} +unsafe impl Send for Tts {} -unsafe impl Sync for Tts {} +unsafe impl Sync for Tts {} -impl Tts { +impl Tts { /** * Create a new `TTS` instance with the specified backend. */ - pub fn new(backend: Backends) -> Result { + pub fn new(backend: Backends) -> Result, Error> { let backend = match backend { #[cfg(target_os = "linux")] Backends::SpeechDispatcher => { let tts = backends::SpeechDispatcher::new()?; - Ok(Tts(Arc::new(RwLock::new(Box::new(tts))))) + Ok(Tts(Arc::new(RwLock::new(Box::new(tts))), PhantomData)) } #[cfg(target_arch = "wasm32")] Backends::Web => { @@ -315,7 +317,7 @@ impl Tts { } } - pub fn default() -> Result { + pub fn default() -> Result, Error> { #[cfg(target_os = "linux")] let tts = Tts::new(Backends::SpeechDispatcher); #[cfg(all(windows, feature = "tolk"))] @@ -564,8 +566,8 @@ impl Tts { /** * Returns list of available voices. */ - pub fn list_voices(&self) -> Vec { - self.0.read().unwrap().list_voices() + pub fn voices(&self) -> Result>, Error> { + self.0.read().unwrap().voices() } /** @@ -680,7 +682,7 @@ impl Tts { } } -impl Drop for Tts { +impl Drop for Tts { fn drop(&mut self) { if Arc::strong_count(&self.0) <= 1 { if let Some(id) = self.0.read().unwrap().id() { @@ -698,14 +700,10 @@ pub enum Gender { } pub trait VoiceImpl: Sized { - type Backend: crate::Backend; - fn from_id(id: String) -> Self; - fn from_language(lang: LanguageIdentifier) -> Self; - fn list() -> Vec; + fn id(self) -> String; fn name(self) -> String; fn gender(self) -> Gender; - fn id(self) -> String; - fn language(self) -> LanguageIdentifier; + fn language(self) -> Result; } pub struct Voice(Box); From 1e55c43153d9fe32b06e5201fdb21582e139edf1 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Mar 2022 15:13:28 -0500 Subject: [PATCH 62/98] WIP: Use correct type in backend implementation. --- src/backends/speech_dispatcher.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 2ccea1d..cf50145 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -91,7 +91,7 @@ impl SpeechDispatcher { } } -impl Backend for SpeechDispatcher { +impl Backend for SpeechDispatcher { fn id(&self) -> Option { Some(BackendId::SpeechDispatcher(self.0.client_id())) } @@ -203,14 +203,14 @@ impl Backend for SpeechDispatcher { Ok(*is_speaking) } - fn voices(&self) -> Result>, Error> { + fn voices(&self) -> Result>, Error> { let rv = self .0 .list_synthesis_voices()? .iter() .cloned() .map(|v| Voice(Box::new(v))) - .collect::>>(); + .collect::>>(); Ok(rv) } From 142f2e6b3a683e5333c3a1e050761f14b225de75 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Mar 2022 18:07:08 -0500 Subject: [PATCH 63/98] Use plain 'ol struct. --- src/backends/speech_dispatcher.rs | 43 ++++++++++--------------------- src/lib.rs | 38 ++++++++++++--------------- 2 files changed, 30 insertions(+), 51 deletions(-) diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index cf50145..f3581d5 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -1,15 +1,12 @@ -use std::str::FromStr; #[cfg(target_os = "linux")] -use std::{collections::HashMap, sync::Mutex}; +use std::{collections::HashMap, str::FromStr, sync::Mutex}; use lazy_static::*; use log::{info, trace}; -use speech_dispatcher::{Voice as SpdVoice, *}; -use unic_langid::{LanguageIdentifier, LanguageIdentifierError}; +use speech_dispatcher::*; +use unic_langid::LanguageIdentifier; -use crate::{ - Backend, BackendId, Error, Features, Gender, UtteranceId, Voice, VoiceImpl, CALLBACKS, -}; +use crate::{Backend, BackendId, Error, Features, Gender, UtteranceId, Voice, CALLBACKS}; #[derive(Clone, Debug)] pub(crate) struct SpeechDispatcher(Connection); @@ -21,24 +18,6 @@ lazy_static! { }; } -impl VoiceImpl for SpdVoice { - fn id(self) -> String { - self.name - } - - fn name(self) -> String { - self.name - } - - fn gender(self) -> Gender { - Gender::Other - } - - fn language(self) -> Result { - LanguageIdentifier::from_str(&self.language) - } -} - impl SpeechDispatcher { pub(crate) fn new() -> std::result::Result { info!("Initializing SpeechDispatcher backend"); @@ -91,7 +70,7 @@ impl SpeechDispatcher { } } -impl Backend for SpeechDispatcher { +impl Backend for SpeechDispatcher { fn id(&self) -> Option { Some(BackendId::SpeechDispatcher(self.0.client_id())) } @@ -203,14 +182,18 @@ impl Backend for SpeechDispatcher { Ok(*is_speaking) } - fn voices(&self) -> Result>, Error> { + fn voices(&self) -> Result, Error> { let rv = self .0 .list_synthesis_voices()? .iter() - .cloned() - .map(|v| Voice(Box::new(v))) - .collect::>>(); + .map(|v| Voice { + id: v.name.clone(), + name: v.name.clone(), + gender: Gender::Unspecified, + language: LanguageIdentifier::from_str(&v.language).unwrap(), + }) + .collect::>(); Ok(rv) } diff --git a/src/lib.rs b/src/lib.rs index 3945c0a..256a608 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,6 @@ use std::collections::HashMap; #[cfg(target_os = "macos")] use std::ffi::CStr; use std::fmt; -use std::marker::PhantomData; use std::sync::{Arc, Mutex}; use std::{boxed::Box, sync::RwLock}; @@ -34,7 +33,6 @@ use thiserror::Error; #[cfg(all(windows, feature = "tolk"))] use tolk::Tolk; pub use unic_langid::LanguageIdentifier; -use unic_langid::LanguageIdentifierError; mod backends; @@ -211,7 +209,7 @@ pub enum Error { } #[clonable] -pub trait Backend: Clone { +pub trait Backend: Clone { fn id(&self) -> Option; fn supported_features(&self) -> Features; fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error>; @@ -232,7 +230,7 @@ pub trait Backend: Clone { fn get_volume(&self) -> Result; fn set_volume(&mut self, volume: f32) -> Result<(), Error>; fn is_speaking(&self) -> Result; - fn voices(&self) -> Result>, Error>; + fn voices(&self) -> Result, Error>; fn voice(&self) -> Result; fn set_voice(&mut self, voice: &str) -> Result<(), Error>; } @@ -256,22 +254,22 @@ lazy_static! { } #[derive(Clone)] -pub struct Tts(Arc>>>, PhantomData); +pub struct Tts(Arc>>); -unsafe impl Send for Tts {} +unsafe impl Send for Tts {} -unsafe impl Sync for Tts {} +unsafe impl Sync for Tts {} -impl Tts { +impl Tts { /** * Create a new `TTS` instance with the specified backend. */ - pub fn new(backend: Backends) -> Result, Error> { + pub fn new(backend: Backends) -> Result { let backend = match backend { #[cfg(target_os = "linux")] Backends::SpeechDispatcher => { let tts = backends::SpeechDispatcher::new()?; - Ok(Tts(Arc::new(RwLock::new(Box::new(tts))), PhantomData)) + Ok(Tts(Arc::new(RwLock::new(Box::new(tts))))) } #[cfg(target_arch = "wasm32")] Backends::Web => { @@ -317,7 +315,7 @@ impl Tts { } } - pub fn default() -> Result, Error> { + pub fn default() -> Result { #[cfg(target_os = "linux")] let tts = Tts::new(Backends::SpeechDispatcher); #[cfg(all(windows, feature = "tolk"))] @@ -566,7 +564,7 @@ impl Tts { /** * Returns list of available voices. */ - pub fn voices(&self) -> Result>, Error> { + pub fn voices(&self) -> Result, Error> { self.0.read().unwrap().voices() } @@ -682,7 +680,7 @@ impl Tts { } } -impl Drop for Tts { +impl Drop for Tts { fn drop(&mut self) { if Arc::strong_count(&self.0) <= 1 { if let Some(id) = self.0.read().unwrap().id() { @@ -694,16 +692,14 @@ impl Drop for Tts { } pub enum Gender { - Other, + Unspecified, Male, Female, } -pub trait VoiceImpl: Sized { - fn id(self) -> String; - fn name(self) -> String; - fn gender(self) -> Gender; - fn language(self) -> Result; +pub struct Voice { + pub id: String, + pub name: String, + pub gender: Gender, + pub language: LanguageIdentifier, } - -pub struct Voice(Box); From 51cd84a6cd830e20de47a9d7b2560182aee55e0d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Mar 2022 18:38:25 -0500 Subject: [PATCH 64/98] Support setting voice with Speech Dispatcher, and clarify features to indicate where getting current voice isn't supported. --- src/backends/speech_dispatcher.rs | 13 ++++++++++--- src/lib.rs | 24 +++++++++++++++--------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index f3581d5..23e1e32 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -82,7 +82,8 @@ impl Backend for SpeechDispatcher { pitch: true, volume: true, is_speaking: true, - voices: false, + voice: true, + get_voice: false, utterance_callbacks: true, } } @@ -201,8 +202,14 @@ impl Backend for SpeechDispatcher { unimplemented!() } - fn set_voice(&mut self, voice: &str) -> Result<(), Error> { - unimplemented!() + fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { + for v in self.0.list_synthesis_voices()? { + if v.name == voice.name { + self.0.set_synthesis_voice(&v)?; + return Ok(()); + } + } + Err(Error::OperationFailed) } } diff --git a/src/lib.rs b/src/lib.rs index 256a608..ee86440 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -166,7 +166,8 @@ pub struct Features { pub rate: bool, pub stop: bool, pub utterance_callbacks: bool, - pub voices: bool, + pub voice: bool, + pub get_voice: bool, pub volume: bool, } @@ -232,7 +233,7 @@ pub trait Backend: Clone { fn is_speaking(&self) -> Result; fn voices(&self) -> Result, Error>; fn voice(&self) -> Result; - fn set_voice(&mut self, voice: &str) -> Result<(), Error>; + fn set_voice(&mut self, voice: &Voice) -> Result<(), Error>; } #[derive(Default)] @@ -565,15 +566,20 @@ impl Tts { * Returns list of available voices. */ pub fn voices(&self) -> Result, Error> { - self.0.read().unwrap().voices() + let Features { voice, .. } = self.supported_features(); + if voice { + self.0.read().unwrap().voices() + } else { + Err(Error::UnsupportedFeature) + } } /** * Return the current speaking voice. */ pub fn voice(&self) -> Result { - let Features { voices, .. } = self.supported_features(); - if voices { + let Features { get_voice, .. } = self.supported_features(); + if get_voice { self.0.read().unwrap().voice() } else { Err(Error::UnsupportedFeature) @@ -583,13 +589,13 @@ impl Tts { /** * Set speaking voice. */ - pub fn set_voice>(&mut self, voice: S) -> Result<(), Error> { + pub fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { let Features { - voices: voices_feature, + voice: voice_feature, .. } = self.supported_features(); - if voices_feature { - self.0.write().unwrap().set_voice(voice.into().as_str()) + if voice_feature { + self.0.write().unwrap().set_voice(voice) } else { Err(Error::UnsupportedFeature) } From b1f60811bf9369914ebbe057a93bcf7ea942d25c Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Mar 2022 20:13:27 -0500 Subject: [PATCH 65/98] Add voice support to WinRT backend. --- Cargo.toml | 2 +- examples/hello_world.rs | 25 ++++++------ src/backends/speech_dispatcher.rs | 2 +- src/backends/winrt.rs | 65 +++++++++++++++++++++++++------ src/lib.rs | 10 ++++- 5 files changed, 78 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 326476a..f47ab74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.34", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.34", features = ["alloc", "Foundation", "Foundation_Collections", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = { version = "0.13", default-features = false } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 3885ede..dc70110 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -71,19 +71,22 @@ fn main() -> Result<(), Error> { tts.speak("This is normal volume.", false)?; tts.set_volume(original_volume)?; } - let Features { voices, .. } = tts.supported_features(); - if voices { - let original_voice = tts.voice()?; - let voices_list = tts.list_voices(); + let Features { voice, .. } = tts.supported_features(); + if voice { + let voices = tts.voices()?; println!("Available voices:\n==="); - for v in voices_list.iter() { - println!("{}", v); - tts.set_voice(v)?; - println!("voice set"); - println!("{}", tts.voice()?); - tts.speak(v, false)?; + for v in &voices { + println!("{:?}", v); + } + let Features { get_voice, .. } = tts.supported_features(); + if get_voice { + let original_voice = tts.voice()?; + for v in &voices { + tts.set_voice(v)?; + tts.speak(format!("This is {}.", v.name), false)?; + } + tts.set_voice(&original_voice)?; } - tts.set_voice(original_voice)?; } tts.speak("Goodbye.", false)?; let mut _input = String::new(); diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 23e1e32..1da28d9 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -198,7 +198,7 @@ impl Backend for SpeechDispatcher { Ok(rv) } - fn voice(&self) -> Result { + fn voice(&self) -> Result { unimplemented!() } diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index dbacbc6..22b992e 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -1,19 +1,23 @@ #[cfg(windows)] -use std::collections::{HashMap, VecDeque}; -use std::sync::Mutex; +use std::{ + collections::{HashMap, VecDeque}, + str::FromStr, + sync::Mutex, +}; use lazy_static::lazy_static; use log::{info, trace}; +use unic_langid::LanguageIdentifier; use windows::{ Foundation::TypedEventHandler, Media::{ Core::MediaSource, Playback::{MediaPlayer, MediaPlayerAudioCategory}, - SpeechSynthesis::SpeechSynthesizer, + SpeechSynthesis::{SpeechSynthesizer, VoiceGender, VoiceInformation}, }, }; -use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; +use crate::{Backend, BackendId, Error, Features, Gender, UtteranceId, Voice, CALLBACKS}; impl From for Error { fn from(e: windows::core::Error) -> Self { @@ -29,6 +33,7 @@ pub struct WinRt { rate: f32, pitch: f32, volume: f32, + voice: VoiceInformation, } struct Utterance { @@ -37,6 +42,7 @@ struct Utterance { rate: f32, pitch: f32, volume: f32, + voice: VoiceInformation, } lazy_static! { @@ -102,6 +108,7 @@ impl WinRt { tts.Options()?.SetSpeakingRate(utterance.rate.into())?; tts.Options()?.SetAudioPitch(utterance.pitch.into())?; tts.Options()?.SetAudioVolume(utterance.volume.into())?; + tts.SetVoice(utterance.voice.clone())?; let stream = tts .SynthesizeTextToStreamAsync(utterance.text.as_str())? .get()?; @@ -129,6 +136,7 @@ impl WinRt { rate: 1., pitch: 1., volume: 1., + voice: SpeechSynthesizer::DefaultVoice()?, }) } } @@ -145,7 +153,8 @@ impl Backend for WinRt { pitch: true, volume: true, is_speaking: true, - voices: true, + voice: true, + get_voice: true, utterance_callbacks: true, } } @@ -175,6 +184,7 @@ impl Backend for WinRt { rate: self.rate, pitch: self.pitch, volume: self.volume, + voice: self.voice.clone(), }; utterances.push_back(utterance); } @@ -291,16 +301,28 @@ impl Backend for WinRt { Ok(!utterances.is_empty()) } - fn voice(&self) -> Result { - unimplemented!() + fn voice(&self) -> Result { + let voice = self.synth.Voice()?; + voice.try_into() } - fn list_voices(&self) -> Vec { - unimplemented!() + fn voices(&self) -> Result, Error> { + let mut rv: Vec = vec![]; + for voice in SpeechSynthesizer::AllVoices()? { + rv.push(voice.try_into()?); + } + Ok(rv) } - fn set_voice(&mut self, voice: &str) -> Result<(), Error> { - unimplemented!() + fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { + for v in SpeechSynthesizer::AllVoices()? { + let vid: String = v.Id()?.try_into()?; + if vid == voice.id { + self.voice = v.clone(); + return Ok(()); + } + } + Err(Error::OperationFailed) } } @@ -315,3 +337,24 @@ impl Drop for WinRt { utterances.remove(&id); } } + +impl TryInto for VoiceInformation { + type Error = Error; + + fn try_into(self) -> Result { + let gender = self.Gender()?; + let gender = if gender == VoiceGender::Male { + Gender::Male + } else { + Gender::Female + }; + let language: String = self.Language()?.try_into()?; + let language = LanguageIdentifier::from_str(&language).unwrap(); + Ok(Voice { + id: self.Id()?.try_into()?, + name: self.DisplayName()?.try_into()?, + gender, + language, + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index ee86440..dfb9a8d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ use std::collections::HashMap; #[cfg(target_os = "macos")] use std::ffi::CStr; use std::fmt; +use std::string::FromUtf16Error; use std::sync::{Arc, Mutex}; use std::{boxed::Box, sync::RwLock}; @@ -200,6 +201,9 @@ pub enum Error { #[cfg(windows)] #[error("WinRT error")] WinRt(windows::core::Error), + #[cfg(windows)] + #[error("UTF string conversion failed")] + UtfStringConversionFailed(#[from] FromUtf16Error), #[error("Unsupported feature")] UnsupportedFeature, #[error("Out of range")] @@ -232,7 +236,7 @@ pub trait Backend: Clone { fn set_volume(&mut self, volume: f32) -> Result<(), Error>; fn is_speaking(&self) -> Result; fn voices(&self) -> Result, Error>; - fn voice(&self) -> Result; + fn voice(&self) -> Result; fn set_voice(&mut self, voice: &Voice) -> Result<(), Error>; } @@ -577,7 +581,7 @@ impl Tts { /** * Return the current speaking voice. */ - pub fn voice(&self) -> Result { + pub fn voice(&self) -> Result { let Features { get_voice, .. } = self.supported_features(); if get_voice { self.0.read().unwrap().voice() @@ -697,12 +701,14 @@ impl Drop for Tts { } } +#[derive(Debug)] pub enum Gender { Unspecified, Male, Female, } +#[derive(Debug)] pub struct Voice { pub id: String, pub name: String, From d4b913908cef187fce9dd84a8821feb5e611535f Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Mar 2022 20:18:10 -0500 Subject: [PATCH 66/98] Eliminate a warning. --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index dfb9a8d..b3249d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ use std::collections::HashMap; #[cfg(target_os = "macos")] use std::ffi::CStr; use std::fmt; +#[cfg(windows)] use std::string::FromUtf16Error; use std::sync::{Arc, Mutex}; use std::{boxed::Box, sync::RwLock}; From e4c6f6f23a54ca4569fe420552a2410d6b1c2d2b Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Mar 2022 20:22:37 -0500 Subject: [PATCH 67/98] Add voice stubs to Tolk backend. --- src/backends/tolk.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backends/tolk.rs b/src/backends/tolk.rs index 45e2f0b..55da024 100644 --- a/src/backends/tolk.rs +++ b/src/backends/tolk.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use log::{info, trace}; use tolk::Tolk as TolkPtr; -use crate::{Backend, BackendId, Error, Features, UtteranceId}; +use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice}; #[derive(Clone, Debug)] pub(crate) struct Tolk(Arc); @@ -109,15 +109,15 @@ impl Backend for Tolk { unimplemented!() } - fn voice(&self) -> Result { + fn voice(&self) -> Result { unimplemented!() } - fn list_voices(&self) -> Vec { + fn voices(&self) -> Result, Error> { unimplemented!() } - fn set_voice(&mut self, voice: &str) -> Result<(), Error> { + fn set_voice(&mut self, _voice: &Voice) -> Result<(), Error> { unimplemented!() } } From 3f9e7c22db8b7b06be0f6169fb2df7369d28eea1 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Mar 2022 20:24:40 -0500 Subject: [PATCH 68/98] Restore default features. --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index f47ab74..68c4a1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["lib", "cdylib", "staticlib"] [features] speech_dispatcher_0_10 = ["speech-dispatcher/0_10"] +default = ["speech_dispatcher_0_10"] [dependencies] dyn-clonable = "0.9" From e699f7e5e56e33943211727bb0559320091fe031 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 31 Mar 2022 10:39:39 -0500 Subject: [PATCH 69/98] Add voices support to web platform. --- Cargo.toml | 2 +- examples/web/Cargo.toml | 2 ++ examples/web/index.html | 2 +- examples/web/src/main.rs | 50 ++++++++++++++++++++++++-- src/backends/web.rs | 76 +++++++++++++++++++++++++++++++++++----- src/lib.rs | 32 +++++++++++++---- 6 files changed, 144 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 68c4a1d..eccc258 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ objc = { version = "0.2", features = ["exception"] } [target.wasm32-unknown-unknown.dependencies] wasm-bindgen = "0.2" -web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "SpeechSynthesisErrorCode", "SpeechSynthesisErrorEvent", "SpeechSynthesisEvent", "SpeechSynthesisUtterance", "Window", ] } +web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "SpeechSynthesisErrorCode", "SpeechSynthesisErrorEvent", "SpeechSynthesisEvent", "SpeechSynthesisUtterance", "SpeechSynthesisVoice", "Window", ] } [target.'cfg(target_os="android")'.dependencies] jni = "0.19" diff --git a/examples/web/Cargo.toml b/examples/web/Cargo.toml index e93d316..745c74e 100644 --- a/examples/web/Cargo.toml +++ b/examples/web/Cargo.toml @@ -7,5 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +console_log = "0.2" +log = "0.4" seed = "0.8" tts = { path = "../.." } \ No newline at end of file diff --git a/examples/web/index.html b/examples/web/index.html index 15e486f..1ec233a 100644 --- a/examples/web/index.html +++ b/examples/web/index.html @@ -1,5 +1,5 @@ - + Example diff --git a/examples/web/src/main.rs b/examples/web/src/main.rs index fb03c38..a2c258e 100644 --- a/examples/web/src/main.rs +++ b/examples/web/src/main.rs @@ -15,13 +15,20 @@ enum Msg { RateChanged(String), PitchChanged(String), VolumeChanged(String), + VoiceChanged(String), Speak, } fn init(_: Url, _: &mut impl Orders) -> Model { - let tts = Tts::default().unwrap(); + let mut tts = Tts::default().unwrap(); + if tts.voices().unwrap().iter().len() > 0 { + if tts.voice().unwrap().is_none() { + tts.set_voice(tts.voices().unwrap().first().unwrap()) + .expect("Failed to set voice"); + } + } Model { - text: Default::default(), + text: "Hello, world. This is a test of the current text-to-speech values.".into(), tts, } } @@ -42,6 +49,13 @@ fn update(msg: Msg, model: &mut Model, _: &mut impl Orders) { let volume = volume.parse::().unwrap(); model.tts.set_volume(volume).unwrap(); } + VoiceChanged(voice) => { + for v in model.tts.voices().unwrap() { + if v.id() == voice { + model.tts.set_voice(&v).unwrap(); + } + } + } Speak => { model.tts.speak(&model.text, false).unwrap(); } @@ -49,6 +63,7 @@ fn update(msg: Msg, model: &mut Model, _: &mut impl Orders) { } fn view(model: &Model) -> Node { + let should_show_voices = model.tts.voices().unwrap().iter().len() > 0; form![ div![label![ "Text to speak", @@ -96,6 +111,36 @@ fn view(model: &Model) -> Node { input_ev(Ev::Input, Msg::VolumeChanged) ], ],], + if should_show_voices { + div![ + label!["Voice"], + select![ + model.tts.voices().unwrap().iter().map(|v| { + let selected = if let Some(voice) = model.tts.voice().unwrap() { + voice.id() == v.id() + } else { + false + }; + option![ + attrs! { + At::Value => v.id() + }, + if selected { + attrs! { + At::Selected => selected + } + } else { + attrs! {} + }, + v.name() + ] + }), + input_ev(Ev::Change, Msg::VoiceChanged) + ] + ] + } else { + div!["Your browser does not seem to support selecting voices."] + }, button![ "Speak", ev(Ev::Click, |e| { @@ -107,5 +152,6 @@ fn view(model: &Model) -> Node { } fn main() { + console_log::init().expect("Error initializing logger"); App::start("app", init, update, view); } diff --git a/src/backends/web.rs b/src/backends/web.rs index 57a8af3..a39dff2 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -1,15 +1,18 @@ #[cfg(target_arch = "wasm32")] -use std::sync::Mutex; +use std::{str::FromStr, sync::Mutex}; use lazy_static::lazy_static; use log::{info, trace}; +use unic_langid::LanguageIdentifier; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use web_sys::{ SpeechSynthesisErrorCode, SpeechSynthesisErrorEvent, SpeechSynthesisEvent, - SpeechSynthesisUtterance, + SpeechSynthesisUtterance, SpeechSynthesisVoice, }; +use crate::Gender; +use crate::Voice; use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; #[derive(Clone, Debug)] @@ -18,6 +21,7 @@ pub struct Web { rate: f32, pitch: f32, volume: f32, + voice: Option, } lazy_static! { @@ -35,6 +39,7 @@ impl Web { rate: 1., pitch: 1., volume: 1., + voice: None, }; *backend_id += 1; Ok(rv) @@ -53,7 +58,8 @@ impl Backend for Web { pitch: true, volume: true, is_speaking: true, - voices: true, + voice: true, + get_voice: true, utterance_callbacks: true, } } @@ -64,6 +70,9 @@ impl Backend for Web { utterance.set_rate(self.rate); utterance.set_pitch(self.pitch); utterance.set_volume(self.volume); + if self.voice.is_some() { + utterance.set_voice(self.voice.as_ref()); + } let id = self.id().unwrap(); let mut uid = NEXT_UTTERANCE_ID.lock().unwrap(); let utterance_id = UtteranceId::Web(*uid); @@ -198,16 +207,53 @@ impl Backend for Web { } } - fn voice(&self) -> Result { - unimplemented!() + fn voice(&self) -> Result, Error> { + if let Some(voice) = &self.voice { + Ok(Some(voice.clone().into())) + } else { + if let Some(window) = web_sys::window() { + let speech_synthesis = window.speech_synthesis().unwrap(); + for voice in speech_synthesis.get_voices().iter() { + let voice: SpeechSynthesisVoice = voice.into(); + if voice.default() { + return Ok(Some(voice.into())); + } + } + } else { + return Err(Error::NoneError); + } + Ok(None) + } } - fn list_voices(&self) -> Vec { - unimplemented!() + fn voices(&self) -> Result, Error> { + if let Some(window) = web_sys::window() { + let speech_synthesis = window.speech_synthesis().unwrap(); + let mut rv: Vec = vec![]; + for v in speech_synthesis.get_voices().iter() { + let v: SpeechSynthesisVoice = v.into(); + rv.push(v.into()); + } + Ok(rv) + } else { + Err(Error::NoneError) + } } - fn set_voice(&mut self, voice: &str) -> Result<(), Error> { - unimplemented!() + fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { + if let Some(window) = web_sys::window() { + let speech_synthesis = window.speech_synthesis().unwrap(); + for v in speech_synthesis.get_voices().iter() { + let v: SpeechSynthesisVoice = v.into(); + if v.voice_uri() == voice.id { + self.voice = Some(v); + return Ok(()); + } + } + return Err(Error::OperationFailed); + } else { + Err(Error::NoneError) + } } } @@ -217,3 +263,15 @@ impl Drop for Web { mappings.retain(|v| v.0 != self.id); } } + +impl From for Voice { + fn from(other: SpeechSynthesisVoice) -> Self { + let language = LanguageIdentifier::from_str(&other.lang()).unwrap(); + Voice { + id: other.voice_uri(), + name: other.name(), + gender: Gender::Unspecified, + language, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index b3249d4..1285569 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -237,7 +237,7 @@ pub trait Backend: Clone { fn set_volume(&mut self, volume: f32) -> Result<(), Error>; fn is_speaking(&self) -> Result; fn voices(&self) -> Result, Error>; - fn voice(&self) -> Result; + fn voice(&self) -> Result, Error>; fn set_voice(&mut self, voice: &Voice) -> Result<(), Error>; } @@ -582,7 +582,7 @@ impl Tts { /** * Return the current speaking voice. */ - pub fn voice(&self) -> Result { + pub fn voice(&self) -> Result, Error> { let Features { get_voice, .. } = self.supported_features(); if get_voice { self.0.read().unwrap().voice() @@ -702,7 +702,7 @@ impl Drop for Tts { } } -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub enum Gender { Unspecified, Male, @@ -711,8 +711,26 @@ pub enum Gender { #[derive(Debug)] pub struct Voice { - pub id: String, - pub name: String, - pub gender: Gender, - pub language: LanguageIdentifier, + pub(crate) id: String, + pub(crate) name: String, + pub(crate) gender: Gender, + pub(crate) language: LanguageIdentifier, +} + +impl Voice { + pub fn id(&self) -> String { + self.id.clone() + } + + pub fn name(&self) -> String { + self.name.clone() + } + + pub fn gender(&self) -> Gender { + self.gender + } + + pub fn language(&self) -> LanguageIdentifier { + self.language.clone() + } } From b9aa36cb3be01e97cdcc8a4de4751e341bf8d38b Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 31 Mar 2022 10:43:07 -0500 Subject: [PATCH 70/98] Update APIs to support case where getting a voice is supported but the value isn't set. --- src/backends/speech_dispatcher.rs | 2 +- src/backends/winrt.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 1da28d9..12ea534 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -198,7 +198,7 @@ impl Backend for SpeechDispatcher { Ok(rv) } - fn voice(&self) -> Result { + fn voice(&self) -> Result, Error> { unimplemented!() } diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 22b992e..97bd130 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -301,9 +301,10 @@ impl Backend for WinRt { Ok(!utterances.is_empty()) } - fn voice(&self) -> Result { + fn voice(&self) -> Result, Error> { let voice = self.synth.Voice()?; - voice.try_into() + let voice = voice.try_into()?; + Ok(Some(voice)) } fn voices(&self) -> Result, Error> { From ec6d1f74a11faf3c090fb72c945d6f87a1256ee4 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 31 Mar 2022 10:55:49 -0500 Subject: [PATCH 71/98] Add voice stubs, currently a no-op, on Android. --- src/backends/android.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/backends/android.rs b/src/backends/android.rs index 5856890..eb32586 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -16,7 +16,7 @@ use jni::{ use lazy_static::lazy_static; use log::{error, info}; -use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; +use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice, CALLBACKS}; lazy_static! { static ref BRIDGE: Mutex> = Mutex::new(None); @@ -248,6 +248,8 @@ impl Backend for Android { volume: false, is_speaking: true, utterance_callbacks: true, + voice: false, + get_voice: false, } } @@ -385,4 +387,16 @@ impl Backend for Android { let rv = rv.z()?; Ok(rv) } + + fn voice(&self) -> Result, Error> { + unimplemented!() + } + + fn voices(&self) -> Result, Error> { + unimplemented!() + } + + fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { + unimplemented!() + } } From c6275839284879aaa43e3ee8ff3e0a821c747ff3 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 31 Mar 2022 11:02:20 -0500 Subject: [PATCH 72/98] Eliminate a warning. --- src/backends/android.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/android.rs b/src/backends/android.rs index eb32586..3f6d422 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -396,7 +396,7 @@ impl Backend for Android { unimplemented!() } - fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { + fn set_voice(&mut self, _voice: &Voice) -> Result<(), Error> { unimplemented!() } } From a0945d7ebb6789eb5cb793e5eabda568422df615 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 31 Mar 2022 11:04:47 -0500 Subject: [PATCH 73/98] Update example for new API. --- examples/hello_world.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index dc70110..fdfc02d 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -81,11 +81,13 @@ fn main() -> Result<(), Error> { let Features { get_voice, .. } = tts.supported_features(); if get_voice { let original_voice = tts.voice()?; - for v in &voices { - tts.set_voice(v)?; - tts.speak(format!("This is {}.", v.name), false)?; + if let Some(original_voice) = original_voice { + for v in &voices { + tts.set_voice(v)?; + tts.speak(format!("This is {}.", v.name()), false)?; + } + tts.set_voice(&original_voice)?; } - tts.set_voice(&original_voice)?; } } tts.speak("Goodbye.", false)?; From 55c0fbbd2bf1f42321b21b495d65858278e7c2ae Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 31 Mar 2022 11:06:23 -0500 Subject: [PATCH 74/98] Remove unnecessary patch. --- speech-dispatcher.patch | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 speech-dispatcher.patch diff --git a/speech-dispatcher.patch b/speech-dispatcher.patch deleted file mode 100644 index 65ebeab..0000000 --- a/speech-dispatcher.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git src/lib.rs src/lib.rs -index 26ba271..180513e 100644 ---- src/lib.rs -+++ src/lib.rs -@@ -127,7 +127,7 @@ unsafe extern "C" fn cb(msg_id: u64, client_id: u64, state: u32) { - } - } - --unsafe extern "C" fn cb_im(msg_id: u64, client_id: u64, state: u32, index_mark: *mut i8) { -+unsafe extern "C" fn cb_im(msg_id: u64, client_id: u64, state: u32, index_mark: *mut u8) { - let index_mark = CStr::from_ptr(index_mark); - let index_mark = index_mark.to_string_lossy().to_string(); - let state = match state { -@@ -325,7 +325,7 @@ impl Connection { - i32_to_bool(v) - } - -- pub fn wchar(&self, priority: Priority, wchar: i32) -> bool { -+ pub fn wchar(&self, priority: Priority, wchar: u32) -> bool { - let v = unsafe { spd_wchar(self.0, priority as u32, wchar) }; - i32_to_bool(v) - } From e3542abd7cb471cd78107533d95092c4b26c2fb0 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 31 Mar 2022 11:52:30 -0500 Subject: [PATCH 75/98] Stub out methods for now. --- src/backends/appkit.rs | 8 ++++---- src/backends/av_foundation.rs | 22 +++++++++------------- src/backends/av_foundation/voices.rs | 6 ++---- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/backends/appkit.rs b/src/backends/appkit.rs index 389418d..3df4bfb 100644 --- a/src/backends/appkit.rs +++ b/src/backends/appkit.rs @@ -7,7 +7,7 @@ use objc::declare::ClassDecl; use objc::runtime::*; use objc::*; -use crate::{Backend, BackendId, Error, Features, UtteranceId}; +use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice}; #[derive(Clone, Debug)] pub(crate) struct AppKit(*mut Object, *mut Object); @@ -201,15 +201,15 @@ impl Backend for AppKit { Ok(is_speaking != NO as i8) } - fn voice(&self) -> Result { + fn voice(&self) -> Result, Error> { unimplemented!() } - fn list_voices(&self) -> Vec { + fn voices(&self) -> Result, Error> { unimplemented!() } - fn set_voice(&mut self, voice: &str) -> Result<(), Error> { + fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { unimplemented!() } } diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 9e79a47..24c8653 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -9,8 +9,7 @@ use log::{info, trace}; use objc::runtime::{Object, Sel}; use objc::{class, declare::ClassDecl, msg_send, sel, sel_impl}; -use crate::voices::Backend as VoiceBackend; -use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; +use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice, CALLBACKS}; mod voices; use voices::*; @@ -167,7 +166,8 @@ impl Backend for AvFoundation { pitch: true, volume: true, is_speaking: true, - voices: true, + voice: true, + get_voice: true, utterance_callbacks: true, } } @@ -280,20 +280,16 @@ impl Backend for AvFoundation { Ok(is_speaking != NO as i8) } - fn voice(&self) -> Result { - Ok(self.voice.id()) + fn voice(&self) -> Result, Error> { + unimplemented!() } - fn list_voices(&self) -> Vec { - AVSpeechSynthesisVoice::list() - .iter() - .map(|v| v.id()) - .collect() + fn voices(&self) -> Result, Error> { + unimplemented!() } - fn set_voice(&mut self, voice: &str) -> Result<(), Error> { - self.voice = AVSpeechSynthesisVoice::new(); - Ok(()) + fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { + unimplemented!() } } diff --git a/src/backends/av_foundation/voices.rs b/src/backends/av_foundation/voices.rs index 836adc7..3df9b69 100644 --- a/src/backends/av_foundation/voices.rs +++ b/src/backends/av_foundation/voices.rs @@ -6,8 +6,6 @@ use objc::runtime::*; use objc::*; use crate::backends::AvFoundation; -use crate::voices; -use crate::voices::Gender; #[derive(Copy, Clone, Debug)] pub(crate) struct AVSpeechSynthesisVoice(*const Object); @@ -22,7 +20,7 @@ impl AVSpeechSynthesisVoice { } } -impl voices::Backend for AVSpeechSynthesisVoice { +/*impl voices::Backend for AVSpeechSynthesisVoice { type Backend = AvFoundation; fn from_id(id: String) -> Self { @@ -66,4 +64,4 @@ impl voices::Backend for AVSpeechSynthesisVoice { let lang: CFString = unsafe { msg_send![self.0, language] }; lang.to_string().parse().unwrap() } -} +}*/ From 264af78c5834d035e6c612539d0a18f319b0617e Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 31 Mar 2022 13:09:37 -0500 Subject: [PATCH 76/98] Get example previewing voices even if one can't be gotten. --- examples/hello_world.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index fdfc02d..9cd7d59 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -79,15 +79,13 @@ fn main() -> Result<(), Error> { println!("{:?}", v); } let Features { get_voice, .. } = tts.supported_features(); - if get_voice { - let original_voice = tts.voice()?; - if let Some(original_voice) = original_voice { - for v in &voices { - tts.set_voice(v)?; - tts.speak(format!("This is {}.", v.name()), false)?; - } - tts.set_voice(&original_voice)?; - } + let original_voice = if get_voice { tts.voice()? } else { None }; + for v in &voices { + tts.set_voice(v)?; + tts.speak(format!("This is {}.", v.name()), false)?; + } + if let Some(original_voice) = original_voice { + tts.set_voice(&original_voice)?; } } tts.speak("Goodbye.", false)?; From 219cfbbe00067e4e8e083c24f5d30608a793f164 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 31 Mar 2022 13:10:01 -0500 Subject: [PATCH 77/98] src Add voices support to AvFoundation backend. --- src/backends/av_foundation.rs | 51 +++++++++++++++++++++++++++-------- src/lib.rs | 2 +- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 24c8653..d1b5dca 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -1,18 +1,18 @@ #[cfg(any(target_os = "macos", target_os = "ios"))] #[link(name = "AVFoundation", kind = "framework")] -use std::sync::Mutex; +use std::{str::FromStr, sync::Mutex}; use cocoa_foundation::base::{id, nil, NO}; use cocoa_foundation::foundation::NSString; +use core_foundation::array::CFArray; +use core_foundation::string::CFString; use lazy_static::lazy_static; use log::{info, trace}; use objc::runtime::{Object, Sel}; use objc::{class, declare::ClassDecl, msg_send, sel, sel_impl}; +use unic_langid::LanguageIdentifier; -use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice, CALLBACKS}; - -mod voices; -use voices::*; +use crate::{Backend, BackendId, Error, Features, Gender, UtteranceId, Voice, CALLBACKS}; #[derive(Clone, Debug)] pub(crate) struct AvFoundation { @@ -22,7 +22,7 @@ pub(crate) struct AvFoundation { rate: f32, volume: f32, pitch: f32, - voice: AVSpeechSynthesisVoice, + voice: Option, } lazy_static! { @@ -146,7 +146,7 @@ impl AvFoundation { rate: 0.5, volume: 1., pitch: 1., - voice: AVSpeechSynthesisVoice::new(), + voice: None, } }; *backend_id += 1; @@ -167,7 +167,7 @@ impl Backend for AvFoundation { volume: true, is_speaking: true, voice: true, - get_voice: true, + get_voice: false, utterance_callbacks: true, } } @@ -192,7 +192,12 @@ impl Backend for AvFoundation { let _: () = msg_send![utterance, setVolume: self.volume]; trace!("Setting pitch to {}", self.pitch); let _: () = msg_send![utterance, setPitchMultiplier: self.pitch]; - let _: () = msg_send![utterance, setVoice: self.voice]; + if let Some(voice) = &self.voice { + let mut vid = NSString::alloc(nil); + vid = vid.init_str(&voice.id()); + let v: id = msg_send![class!(AVSpeechSynthesisVoice), voiceWithIdentifier: vid]; + let _: () = msg_send![utterance, setVoice: v]; + } trace!("Enqueuing"); let _: () = msg_send![self.synth, speakUtterance: utterance]; trace!("Done queuing"); @@ -285,11 +290,35 @@ impl Backend for AvFoundation { } fn voices(&self) -> Result, Error> { - unimplemented!() + let voices: CFArray = unsafe { msg_send![class!(AVSpeechSynthesisVoice), speechVoices] }; + let rv = voices + .iter() + .map(|v| { + let id: CFString = unsafe { msg_send![*v as *const Object, identifier] }; + let name: CFString = unsafe { msg_send![*v as *const Object, name] }; + let gender: i64 = unsafe { msg_send![*v as *const Object, gender] }; + let gender = match gender { + 0 => Gender::Male, + 1 => Gender::Female, + _ => Gender::Unspecified, + }; + let language: CFString = unsafe { msg_send![*v as *const Object, language] }; + let language = language.to_string(); + let language = LanguageIdentifier::from_str(&language).unwrap(); + Voice { + id: id.to_string(), + name: name.to_string(), + gender, + language, + } + }) + .collect(); + Ok(rv) } fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { - unimplemented!() + self.voice = Some(voice.clone()); + Ok(()) } } diff --git a/src/lib.rs b/src/lib.rs index 1285569..db970f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -709,7 +709,7 @@ pub enum Gender { Female, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Voice { pub(crate) id: String, pub(crate) name: String, From 2b4251f6fa301821502674ed86edcbca6ff8e75a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 31 Mar 2022 13:16:35 -0500 Subject: [PATCH 78/98] Don't support voices in AppKit for now. --- src/backends/appkit.rs | 2 +- src/backends/av_foundation/voices.rs | 67 ---------------------------- 2 files changed, 1 insertion(+), 68 deletions(-) delete mode 100644 src/backends/av_foundation/voices.rs diff --git a/src/backends/appkit.rs b/src/backends/appkit.rs index 3df4bfb..e9beb67 100644 --- a/src/backends/appkit.rs +++ b/src/backends/appkit.rs @@ -209,7 +209,7 @@ impl Backend for AppKit { unimplemented!() } - fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { + fn set_voice(&mut self, _voice: &Voice) -> Result<(), Error> { unimplemented!() } } diff --git a/src/backends/av_foundation/voices.rs b/src/backends/av_foundation/voices.rs deleted file mode 100644 index 3df9b69..0000000 --- a/src/backends/av_foundation/voices.rs +++ /dev/null @@ -1,67 +0,0 @@ -use cocoa_foundation::base::{id, nil}; -use cocoa_foundation::foundation::NSString; -use core_foundation::array::CFArray; -use core_foundation::string::CFString; -use objc::runtime::*; -use objc::*; - -use crate::backends::AvFoundation; - -#[derive(Copy, Clone, Debug)] -pub(crate) struct AVSpeechSynthesisVoice(*const Object); - -impl AVSpeechSynthesisVoice { - pub fn new() -> Self { - let voice: *const Object; - unsafe { - voice = msg_send![class!(AVSpeechSynthesisVoice), new]; - }; - AVSpeechSynthesisVoice { 0: voice } - } -} - -/*impl voices::Backend for AVSpeechSynthesisVoice { - type Backend = AvFoundation; - - fn from_id(id: String) -> Self { - unimplemented!() - } - - fn from_language(lang: voices::LanguageIdentifier) -> Self { - unimplemented!() - } - - fn list() -> Vec { - let voices: CFArray = unsafe { msg_send![class!(AVSpeechSynthesisVoice), speechVoices] }; - voices - .iter() - .map(|v| AVSpeechSynthesisVoice { - 0: *v as *const Object, - }) - .collect() - } - - fn name(self) -> String { - let name: CFString = unsafe { msg_send![self.0, name] }; - name.to_string() - } - - fn gender(self) -> Gender { - let gender: i64 = unsafe { msg_send![self.0, gender] }; - match gender { - 1 => Gender::Male, - 2 => Gender::Female, - _ => Gender::Other, - } - } - - fn id(self) -> String { - let identifier: CFString = unsafe { msg_send![self.0, identifier] }; - identifier.to_string() - } - - fn language(self) -> voices::LanguageIdentifier { - let lang: CFString = unsafe { msg_send![self.0, language] }; - lang.to_string().parse().unwrap() - } -}*/ From 9bd767629a123fc6a4303eddc4082f3b425f2e9f Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 31 Mar 2022 13:18:57 -0500 Subject: [PATCH 79/98] Remove unspecified gender in favor of `Option`. --- src/backends/av_foundation.rs | 6 +++--- src/lib.rs | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index d1b5dca..4f87395 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -298,9 +298,9 @@ impl Backend for AvFoundation { let name: CFString = unsafe { msg_send![*v as *const Object, name] }; let gender: i64 = unsafe { msg_send![*v as *const Object, gender] }; let gender = match gender { - 0 => Gender::Male, - 1 => Gender::Female, - _ => Gender::Unspecified, + 0 => Some(Gender::Male), + 1 => Some(Gender::Female), + _ => None, }; let language: CFString = unsafe { msg_send![*v as *const Object, language] }; let language = language.to_string(); diff --git a/src/lib.rs b/src/lib.rs index db970f1..4b56bd1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -704,7 +704,6 @@ impl Drop for Tts { #[derive(Clone, Copy, Debug)] pub enum Gender { - Unspecified, Male, Female, } @@ -713,7 +712,7 @@ pub enum Gender { pub struct Voice { pub(crate) id: String, pub(crate) name: String, - pub(crate) gender: Gender, + pub(crate) gender: Option, pub(crate) language: LanguageIdentifier, } @@ -726,7 +725,7 @@ impl Voice { self.name.clone() } - pub fn gender(&self) -> Gender { + pub fn gender(&self) -> Option { self.gender } From 822f770ab8e7acc547e9df955e0d1967cb42b322 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 31 Mar 2022 13:25:08 -0500 Subject: [PATCH 80/98] Finish making gender optional. --- Cargo.toml | 1 - src/backends/speech_dispatcher.rs | 4 ++-- src/backends/web.rs | 6 ++---- src/backends/winrt.rs | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eccc258..c7447cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ crate-type = ["lib", "cdylib", "staticlib"] [features] speech_dispatcher_0_10 = ["speech-dispatcher/0_10"] -default = ["speech_dispatcher_0_10"] [dependencies] dyn-clonable = "0.9" diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 12ea534..b3034a1 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -6,7 +6,7 @@ use log::{info, trace}; use speech_dispatcher::*; use unic_langid::LanguageIdentifier; -use crate::{Backend, BackendId, Error, Features, Gender, UtteranceId, Voice, CALLBACKS}; +use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice, CALLBACKS}; #[derive(Clone, Debug)] pub(crate) struct SpeechDispatcher(Connection); @@ -191,7 +191,7 @@ impl Backend for SpeechDispatcher { .map(|v| Voice { id: v.name.clone(), name: v.name.clone(), - gender: Gender::Unspecified, + gender: None, language: LanguageIdentifier::from_str(&v.language).unwrap(), }) .collect::>(); diff --git a/src/backends/web.rs b/src/backends/web.rs index a39dff2..578a213 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -11,9 +11,7 @@ use web_sys::{ SpeechSynthesisUtterance, SpeechSynthesisVoice, }; -use crate::Gender; -use crate::Voice; -use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; +use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice, CALLBACKS}; #[derive(Clone, Debug)] pub struct Web { @@ -270,7 +268,7 @@ impl From for Voice { Voice { id: other.voice_uri(), name: other.name(), - gender: Gender::Unspecified, + gender: None, language, } } diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 97bd130..ceb5f00 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -354,7 +354,7 @@ impl TryInto for VoiceInformation { Ok(Voice { id: self.Id()?.try_into()?, name: self.DisplayName()?.try_into()?, - gender, + gender: Some(gender), language, }) } From da19d5f16cb13d522b3d2ec385d645b8c657d420 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 31 Mar 2022 13:37:42 -0500 Subject: [PATCH 81/98] Restore. --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index c7447cb..eccc258 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["lib", "cdylib", "staticlib"] [features] speech_dispatcher_0_10 = ["speech-dispatcher/0_10"] +default = ["speech_dispatcher_0_10"] [dependencies] dyn-clonable = "0.9" From 4d01717e75d4d7d26b110ed94c4fe758162f1234 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 31 Mar 2022 13:38:39 -0500 Subject: [PATCH 82/98] Fix return type in Tolk backend. --- src/backends/tolk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/tolk.rs b/src/backends/tolk.rs index 55da024..056d75e 100644 --- a/src/backends/tolk.rs +++ b/src/backends/tolk.rs @@ -109,7 +109,7 @@ impl Backend for Tolk { unimplemented!() } - fn voice(&self) -> Result { + fn voice(&self) -> Result, Error> { unimplemented!() } From 569bb160b8702929c8c23a8ff3d985e1ca07e084 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 31 Mar 2022 14:47:20 -0500 Subject: [PATCH 83/98] Try to intercept cases where voice might be nil. --- src/backends/av_foundation.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 4f87395..cf657e1 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -42,6 +42,12 @@ impl AvFoundation { utterance: id, ) { trace!("speech_synthesizer_did_start_speech_utterance"); + let vid: id = unsafe { msg_send![utterance, voice] }; + if vid == nil { + println!("nil voice"); + } else { + println!("Got voice ID"); + } unsafe { let backend_id: u64 = *this.get_ivar("backend_id"); let backend_id = BackendId::AvFoundation(backend_id); From 428362372362cb058aeec7063c3a922159ee5bf8 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Sat, 7 May 2022 11:04:22 -0500 Subject: [PATCH 84/98] Bump dependency. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index eccc258..4be9928 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.34", features = ["alloc", "Foundation", "Foundation_Collections", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.36", features = ["alloc", "Foundation", "Foundation_Collections", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = { version = "0.13", default-features = false } From 4079f4b3c4598b8c60d6139260ac057cabba33fe Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 9 May 2022 08:44:33 -0500 Subject: [PATCH 85/98] Fix mismatched gender codes. --- src/backends/av_foundation.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index cf657e1..2b1184a 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -304,8 +304,8 @@ impl Backend for AvFoundation { let name: CFString = unsafe { msg_send![*v as *const Object, name] }; let gender: i64 = unsafe { msg_send![*v as *const Object, gender] }; let gender = match gender { - 0 => Some(Gender::Male), - 1 => Some(Gender::Female), + 1 => Some(Gender::Male), + 2 => Some(Gender::Female), _ => None, }; let language: CFString = unsafe { msg_send![*v as *const Object, language] }; From 40e28876b2cad55ea84b00ad6ce87df94ec0437f Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 9 May 2022 08:46:46 -0500 Subject: [PATCH 86/98] Remove unnecessary printlns and link directives. --- src/backends/appkit.rs | 1 - src/backends/av_foundation.rs | 7 ------- 2 files changed, 8 deletions(-) diff --git a/src/backends/appkit.rs b/src/backends/appkit.rs index e9beb67..2918c09 100644 --- a/src/backends/appkit.rs +++ b/src/backends/appkit.rs @@ -1,5 +1,4 @@ #[cfg(target_os = "macos")] -#[link(name = "AppKit", kind = "framework")] use cocoa_foundation::base::{id, nil}; use cocoa_foundation::foundation::NSString; use log::{info, trace}; diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 2b1184a..6f0f1e0 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -1,5 +1,4 @@ #[cfg(any(target_os = "macos", target_os = "ios"))] -#[link(name = "AVFoundation", kind = "framework")] use std::{str::FromStr, sync::Mutex}; use cocoa_foundation::base::{id, nil, NO}; @@ -42,12 +41,6 @@ impl AvFoundation { utterance: id, ) { trace!("speech_synthesizer_did_start_speech_utterance"); - let vid: id = unsafe { msg_send![utterance, voice] }; - if vid == nil { - println!("nil voice"); - } else { - println!("Got voice ID"); - } unsafe { let backend_id: u64 = *this.get_ivar("backend_id"); let backend_id = BackendId::AvFoundation(backend_id); From 40f682080d5f35d98fb90cb772d1d74956bc2987 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 9 May 2022 08:48:07 -0500 Subject: [PATCH 87/98] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4be9928..4dccc10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.21.1" +version = "0.22.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 9b4ae761a07d139ed3137e62ec85a33d4d6a86ba Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 19 May 2022 12:03:23 -0500 Subject: [PATCH 88/98] Bump version and dependency. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4dccc10..8bbe63c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.22.0" +version = "0.22.1" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -28,7 +28,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.36", features = ["alloc", "Foundation", "Foundation_Collections", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.37", features = ["alloc", "Foundation", "Foundation_Collections", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = { version = "0.13", default-features = false } From 323f129b7ba475701b7e800282b932672704d058 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 13 Jun 2022 10:22:39 -0500 Subject: [PATCH 89/98] #24: Don't use default features when building on docs.rs. --- Cargo.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8bbe63c..898d2c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.22.1" +version = "0.22.2" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -46,3 +46,6 @@ web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "Spee [target.'cfg(target_os="android")'.dependencies] jni = "0.19" ndk-glue = "0.6" + +[package.metadata.docs.rs] +no-default-features = true \ No newline at end of file From 10ac1021ee8c2290b9d0ab8da0225707da449288 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 13 Jun 2022 10:35:32 -0500 Subject: [PATCH 90/98] Switch to line doc comments. --- src/lib.rs | 128 ++++++++++++++++------------------------------------- 1 file changed, 37 insertions(+), 91 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4b56bd1..3da3bd6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,14 @@ -/*! - * a Text-To-Speech (TTS) library providing high-level interfaces to a variety of backends. - * Currently supported backends are: - * * Windows - * * Screen readers/SAPI via Tolk (requires `tolk` Cargo feature) - * * WinRT - * * Linux via [Speech Dispatcher](https://freebsoft.org/speechd) - * * MacOS/iOS - * * AppKit on MacOS 10.13 and below - * * AVFoundation on MacOS 10.14 and above, and iOS - * * Android - * * WebAssembly - */ +//! * a Text-To-Speech (TTS) library providing high-level interfaces to a variety of backends. +//! * Currently supported backends are: +//! * * Windows +//! * * Screen readers/SAPI via Tolk (requires `tolk` Cargo feature) +//! * * WinRT +//! * * Linux via [Speech Dispatcher](https://freebsoft.org/speechd) +//! * * MacOS/iOS +//! * * AppKit on MacOS 10.13 and below +//! * * AVFoundation on MacOS 10.14 and above, and iOS +//! * * Android +//! * * WebAssembly use std::collections::HashMap; #[cfg(target_os = "macos")] @@ -267,9 +265,7 @@ unsafe impl Send for Tts {} unsafe impl Sync for Tts {} impl Tts { - /** - * Create a new `TTS` instance with the specified backend. - */ + /// Create a new `TTS` instance with the specified backend. pub fn new(backend: Backends) -> Result { let backend = match backend { #[cfg(target_os = "linux")] @@ -360,16 +356,12 @@ impl Tts { tts } - /** - * Returns the features supported by this TTS engine - */ + /// Returns the features supported by this TTS engine pub fn supported_features(&self) -> Features { self.0.read().unwrap().supported_features() } - /** - * Speaks the specified text, optionally interrupting current speech. - */ + /// Speaks the specified text, optionally interrupting current speech. pub fn speak>( &mut self, text: S, @@ -381,9 +373,7 @@ impl Tts { .speak(text.into().as_str(), interrupt) } - /** - * Stops current speech. - */ + /// Stops current speech. pub fn stop(&mut self) -> Result<&Self, Error> { let Features { stop, .. } = self.supported_features(); if stop { @@ -394,30 +384,22 @@ impl Tts { } } - /** - * Returns the minimum rate for this speech synthesizer. - */ + /// Returns the minimum rate for this speech synthesizer. pub fn min_rate(&self) -> f32 { self.0.read().unwrap().min_rate() } - /** - * Returns the maximum rate for this speech synthesizer. - */ + /// Returns the maximum rate for this speech synthesizer. pub fn max_rate(&self) -> f32 { self.0.read().unwrap().max_rate() } - /** - * Returns the normal rate for this speech synthesizer. - */ + /// Returns the normal rate for this speech synthesizer. pub fn normal_rate(&self) -> f32 { self.0.read().unwrap().normal_rate() } - /** - * Gets the current speech rate. - */ + /// Gets the current speech rate. pub fn get_rate(&self) -> Result { let Features { rate, .. } = self.supported_features(); if rate { @@ -427,9 +409,7 @@ impl Tts { } } - /** - * Sets the desired speech rate. - */ + /// Sets the desired speech rate. pub fn set_rate(&mut self, rate: f32) -> Result<&Self, Error> { let Features { rate: rate_feature, .. @@ -447,30 +427,22 @@ impl Tts { } } - /** - * Returns the minimum pitch for this speech synthesizer. - */ + /// Returns the minimum pitch for this speech synthesizer. pub fn min_pitch(&self) -> f32 { self.0.read().unwrap().min_pitch() } - /** - * Returns the maximum pitch for this speech synthesizer. - */ + /// Returns the maximum pitch for this speech synthesizer. pub fn max_pitch(&self) -> f32 { self.0.read().unwrap().max_pitch() } - /** - * Returns the normal pitch for this speech synthesizer. - */ + /// Returns the normal pitch for this speech synthesizer. pub fn normal_pitch(&self) -> f32 { self.0.read().unwrap().normal_pitch() } - /** - * Gets the current speech pitch. - */ + /// Gets the current speech pitch. pub fn get_pitch(&self) -> Result { let Features { pitch, .. } = self.supported_features(); if pitch { @@ -480,9 +452,7 @@ impl Tts { } } - /** - * Sets the desired speech pitch. - */ + /// Sets the desired speech pitch. pub fn set_pitch(&mut self, pitch: f32) -> Result<&Self, Error> { let Features { pitch: pitch_feature, @@ -501,30 +471,22 @@ impl Tts { } } - /** - * Returns the minimum volume for this speech synthesizer. - */ + /// Returns the minimum volume for this speech synthesizer. pub fn min_volume(&self) -> f32 { self.0.read().unwrap().min_volume() } - /** - * Returns the maximum volume for this speech synthesizer. - */ + /// Returns the maximum volume for this speech synthesizer. pub fn max_volume(&self) -> f32 { self.0.read().unwrap().max_volume() } - /** - * Returns the normal volume for this speech synthesizer. - */ + /// Returns the normal volume for this speech synthesizer. pub fn normal_volume(&self) -> f32 { self.0.read().unwrap().normal_volume() } - /** - * Gets the current speech volume. - */ + /// Gets the current speech volume. pub fn get_volume(&self) -> Result { let Features { volume, .. } = self.supported_features(); if volume { @@ -534,9 +496,7 @@ impl Tts { } } - /** - * Sets the desired speech volume. - */ + /// Sets the desired speech volume. pub fn set_volume(&mut self, volume: f32) -> Result<&Self, Error> { let Features { volume: volume_feature, @@ -555,9 +515,7 @@ impl Tts { } } - /** - * Returns whether this speech synthesizer is speaking. - */ + /// Returns whether this speech synthesizer is speaking. pub fn is_speaking(&self) -> Result { let Features { is_speaking, .. } = self.supported_features(); if is_speaking { @@ -567,9 +525,7 @@ impl Tts { } } - /** - * Returns list of available voices. - */ + /// Returns list of available voices. pub fn voices(&self) -> Result, Error> { let Features { voice, .. } = self.supported_features(); if voice { @@ -579,9 +535,7 @@ impl Tts { } } - /** - * Return the current speaking voice. - */ + /// Return the current speaking voice. pub fn voice(&self) -> Result, Error> { let Features { get_voice, .. } = self.supported_features(); if get_voice { @@ -591,9 +545,7 @@ impl Tts { } } - /** - * Set speaking voice. - */ + /// Set speaking voice. pub fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { let Features { voice: voice_feature, @@ -606,9 +558,7 @@ impl Tts { } } - /** - * Called when this speech synthesizer begins speaking an utterance. - */ + /// Called when this speech synthesizer begins speaking an utterance. pub fn on_utterance_begin( &self, callback: Option>, @@ -628,9 +578,7 @@ impl Tts { } } - /** - * Called when this speech synthesizer finishes speaking an utterance. - */ + /// Called when this speech synthesizer finishes speaking an utterance. pub fn on_utterance_end( &self, callback: Option>, @@ -650,9 +598,7 @@ impl Tts { } } - /** - * Called when this speech synthesizer is stopped and still has utterances in its queue. - */ + /// Called when this speech synthesizer is stopped and still has utterances in its queue. pub fn on_utterance_stop( &self, callback: Option>, From 507d0b5418208ff2f027c271729bc858c6dc42e8 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 14 Jun 2022 13:09:50 -0500 Subject: [PATCH 91/98] Replace some `unwrap` calls with `ok_or(Error::OperationFailed)`. --- src/backends/appkit.rs | 18 ++++++++++++------ src/backends/av_foundation.rs | 7 ++++--- src/lib.rs | 8 ++++---- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/backends/appkit.rs b/src/backends/appkit.rs index 2918c09..cf1375a 100644 --- a/src/backends/appkit.rs +++ b/src/backends/appkit.rs @@ -12,12 +12,12 @@ use crate::{Backend, BackendId, Error, Features, UtteranceId, Voice}; pub(crate) struct AppKit(*mut Object, *mut Object); impl AppKit { - pub(crate) fn new() -> Self { + pub(crate) fn new() -> Result { info!("Initializing AppKit backend"); unsafe { let obj: *mut Object = msg_send![class!(NSSpeechSynthesizer), new]; - let mut decl = - ClassDecl::new("MyNSSpeechSynthesizerDelegate", class!(NSObject)).unwrap(); + let mut decl = ClassDecl::new("MyNSSpeechSynthesizerDelegate", class!(NSObject)) + .ok_or(Error::OperationFailed)?; decl.add_ivar::("synth"); decl.add_ivar::("strings"); @@ -81,11 +81,17 @@ impl AppKit { let delegate_class = decl.register(); let delegate_obj: *mut Object = msg_send![delegate_class, new]; - delegate_obj.as_mut().unwrap().set_ivar("synth", obj); + delegate_obj + .as_mut() + .ok_or(Error::OperationFailed)? + .set_ivar("synth", obj); let strings: id = msg_send![class!(NSMutableArray), new]; - delegate_obj.as_mut().unwrap().set_ivar("strings", strings); + delegate_obj + .as_mut() + .ok_or(Error::OperationFailed)? + .set_ivar("strings", strings); let _: Object = msg_send![obj, setDelegate: delegate_obj]; - AppKit(obj, delegate_obj) + Ok(AppKit(obj, delegate_obj)) } } } diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 6f0f1e0..a64d9b4 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -29,9 +29,10 @@ lazy_static! { } impl AvFoundation { - pub(crate) fn new() -> Self { + pub(crate) fn new() -> Result { info!("Initializing AVFoundation backend"); - let mut decl = ClassDecl::new("MyNSSpeechSynthesizerDelegate", class!(NSObject)).unwrap(); + let mut decl = ClassDecl::new("MyNSSpeechSynthesizerDelegate", class!(NSObject)) + .ok_or(Error::OperationFailed)?; decl.add_ivar::("backend_id"); extern "C" fn speech_synthesizer_did_start_speech_utterance( @@ -149,7 +150,7 @@ impl AvFoundation { } }; *backend_id += 1; - rv + Ok(rv) } } diff --git a/src/lib.rs b/src/lib.rs index 3da3bd6..76c7b94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -293,12 +293,12 @@ impl Tts { Ok(Tts(Arc::new(RwLock::new(Box::new(tts))))) } #[cfg(target_os = "macos")] - Backends::AppKit => Ok(Tts(Arc::new(RwLock::new( - Box::new(backends::AppKit::new()), - )))), + Backends::AppKit => Ok(Tts(Arc::new(RwLock::new(Box::new( + backends::AppKit::new()? + ))))), #[cfg(any(target_os = "macos", target_os = "ios"))] Backends::AvFoundation => Ok(Tts(Arc::new(RwLock::new(Box::new( - backends::AvFoundation::new(), + backends::AvFoundation::new()?, ))))), #[cfg(target_os = "android")] Backends::Android => { From 238d7e2cb3a08d244d104e6ad304ec2e7220a072 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 14 Jun 2022 13:13:00 -0500 Subject: [PATCH 92/98] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 898d2c7..f9712ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.22.2" +version = "0.22.3" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 5feb8e318646d726cf59b0c9fdff8fec29cfc110 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 14 Jun 2022 13:57:53 -0500 Subject: [PATCH 93/98] Constrain version so example builds. --- examples/web/Cargo.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/web/Cargo.toml b/examples/web/Cargo.toml index 745c74e..1296fce 100644 --- a/examples/web/Cargo.toml +++ b/examples/web/Cargo.toml @@ -9,5 +9,6 @@ edition = "2018" [dependencies] console_log = "0.2" log = "0.4" -seed = "0.8" -tts = { path = "../.." } \ No newline at end of file +seed = "0.9" +tts = { path = "../.." } +wasm-bindgen = "= 0.2.80" \ No newline at end of file From b3d2b788f77c2b11b75d02e2bb5076b030649ca7 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 30 Jun 2022 18:15:42 -0500 Subject: [PATCH 94/98] Bump version and dependency. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f9712ee..361d164 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.22.3" +version = "0.22.4" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -28,7 +28,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.37", features = ["alloc", "Foundation", "Foundation_Collections", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.38", features = ["alloc", "Foundation", "Foundation_Collections", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = { version = "0.13", default-features = false } From 15f28c9af4f93f6013874e351ee53de968b39fb8 Mon Sep 17 00:00:00 2001 From: Bear-03 <64696287+Bear-03@users.noreply.github.com> Date: Thu, 21 Jul 2022 01:25:14 +0200 Subject: [PATCH 95/98] Derive common traits for Gender and Voice --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 76c7b94..8a8df57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -648,13 +648,13 @@ impl Drop for Tts { } } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Gender { Male, Female, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Voice { pub(crate) id: String, pub(crate) name: String, From 748f07138dfd05b8a91086ef80f6f2aff00904ff Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 21 Jul 2022 18:34:22 -0500 Subject: [PATCH 96/98] Bump version and dependency. --- Cargo.toml | 4 ++-- src/backends/winrt.rs | 25 ++++++++++++++----------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 361d164..41e605a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.22.4" +version = "0.22.5" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -28,7 +28,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.38", features = ["alloc", "Foundation", "Foundation_Collections", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.39", features = ["Foundation", "Foundation_Collections", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = { version = "0.13", default-features = false } diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index ceb5f00..d3a349a 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -84,7 +84,7 @@ impl WinRt { backend_to_speech_synthesizer.insert(bid, synth.clone()); drop(backend_to_speech_synthesizer); let bid_clone = bid; - player.MediaEnded(TypedEventHandler::new( + player.MediaEnded(&TypedEventHandler::new( move |sender: &Option, _args| { if let Some(sender) = sender { let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); @@ -108,14 +108,14 @@ impl WinRt { tts.Options()?.SetSpeakingRate(utterance.rate.into())?; tts.Options()?.SetAudioPitch(utterance.pitch.into())?; tts.Options()?.SetAudioVolume(utterance.volume.into())?; - tts.SetVoice(utterance.voice.clone())?; - let stream = tts - .SynthesizeTextToStreamAsync(utterance.text.as_str())? - .get()?; + tts.SetVoice(&utterance.voice)?; + let text = &utterance.text; + let stream = + tts.SynthesizeTextToStreamAsync(&text.into())?.get()?; let content_type = stream.ContentType()?; let source = - MediaSource::CreateFromStream(stream, content_type)?; - sender.SetSource(source)?; + MediaSource::CreateFromStream(&stream, &content_type)?; + sender.SetSource(&source)?; sender.Play()?; if let Some(callback) = callbacks.utterance_begin.as_mut() { callback(utterance.id); @@ -193,10 +193,13 @@ impl Backend for WinRt { self.synth.Options()?.SetSpeakingRate(self.rate.into())?; self.synth.Options()?.SetAudioPitch(self.pitch.into())?; self.synth.Options()?.SetAudioVolume(self.volume.into())?; - let stream = self.synth.SynthesizeTextToStreamAsync(text)?.get()?; + let stream = self + .synth + .SynthesizeTextToStreamAsync(&text.into())? + .get()?; let content_type = stream.ContentType()?; - let source = MediaSource::CreateFromStream(stream, content_type)?; - self.player.SetSource(source)?; + let source = MediaSource::CreateFromStream(&stream, &content_type)?; + self.player.SetSource(&source)?; self.player.Play()?; let mut callbacks = CALLBACKS.lock().unwrap(); let callbacks = callbacks.get_mut(&self.id).unwrap(); @@ -319,7 +322,7 @@ impl Backend for WinRt { for v in SpeechSynthesizer::AllVoices()? { let vid: String = v.Id()?.try_into()?; if vid == voice.id { - self.voice = v.clone(); + self.voice = v; return Ok(()); } } From 7cf80fb64d817b8e34f5f4479f7850cffc1921a5 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 22 Jul 2022 10:08:13 -0500 Subject: [PATCH 97/98] WinRT: Correctly set voice for case where no utterances are in queue. Fixes #29 --- src/backends/winrt.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index d3a349a..70d765f 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -193,6 +193,7 @@ impl Backend for WinRt { self.synth.Options()?.SetSpeakingRate(self.rate.into())?; self.synth.Options()?.SetAudioPitch(self.pitch.into())?; self.synth.Options()?.SetAudioVolume(self.volume.into())?; + self.synth.SetVoice(&self.voice)?; let stream = self .synth .SynthesizeTextToStreamAsync(&text.into())? From f404e180e41275e43d6ea3afd2834cd8f86d126a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 22 Jul 2022 10:13:32 -0500 Subject: [PATCH 98/98] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 41e605a..37edeb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.22.5" +version = "0.23.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface"