From d6508edd12b996e1e73f1d0f9c7ee0efee77a8fa Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 21 Sep 2020 15:13:22 -0500 Subject: [PATCH 01/17] Remove workaround for incorrect Tolk string handling, pin minimum working version, and bump version. --- Cargo.toml | 4 ++-- src/backends/tolk.rs | 20 +------------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8ecb3f0..082c2a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.6.3" +version = "0.6.4" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -20,7 +20,7 @@ thiserror = "1" env_logger = "0.7" [target.'cfg(windows)'.dependencies] -tolk = "0.2" +tolk = ">= 0.2.1" winrt = "0.7" tts_winrt_bindings = { version = "0.1", path="winrt_bindings" } diff --git a/src/backends/tolk.rs b/src/backends/tolk.rs index c71cdfa..370da65 100644 --- a/src/backends/tolk.rs +++ b/src/backends/tolk.rs @@ -28,25 +28,7 @@ impl Backend for Tolk { fn speak(&mut self, text: &str, interrupt: bool) -> Result<(), Error> { trace!("speak({}, {})", text, interrupt); - const BUFFER_LENGTH: usize = 300; - if text.len() <= BUFFER_LENGTH { - self.0.speak(text, interrupt); - } else { - if interrupt { - self.stop()?; - } - let tokens = text.split_whitespace(); - let mut buffer = String::new(); - for token in tokens { - if buffer.len() + token.len() > BUFFER_LENGTH { - self.0.speak(buffer, false); - buffer = String::new(); - } else { - buffer.push_str(token); - buffer.push(' '); - } - } - } + self.0.speak(text, interrupt); Ok(()) } From 4816ec575c3ff9d49c923bb6a90ab24f4f172999 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 22 Sep 2020 12:40:03 -0500 Subject: [PATCH 02/17] Make speak calls return an utterance ID, where possible. --- src/backends/speech_dispatcher.rs | 12 ++++++++---- src/backends/tolk.rs | 6 +++--- src/backends/web.rs | 17 ++++++++++++++--- src/backends/winrt.rs | 19 ++++++++++++++++--- src/lib.rs | 21 +++++++++++++++++---- 5 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 009a8fb..5717c61 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -6,7 +6,7 @@ use lazy_static::*; use log::{info, trace}; use speech_dispatcher::*; -use crate::{Backend, Error, Features}; +use crate::{Backend, Error, Features, UtteranceId}; pub struct SpeechDispatcher(Connection); @@ -59,7 +59,7 @@ impl Backend for SpeechDispatcher { } } - fn speak(&mut self, text: &str, interrupt: bool) -> Result<(), Error> { + fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error> { trace!("speak({}, {})", text, interrupt); if interrupt { self.stop()?; @@ -68,11 +68,15 @@ impl Backend for SpeechDispatcher { if single_char { self.0.set_punctuation(Punctuation::All); } - self.0.say(Priority::Important, text); + let id = self.0.say(Priority::Important, text); if single_char { self.0.set_punctuation(Punctuation::None); } - Ok(()) + if let Some(id) = id { + Ok(Some(UtteranceId::SpeechDispatcher(id))) + } else { + Err(Error::NoneError) + } } fn stop(&mut self) -> Result<(), Error> { diff --git a/src/backends/tolk.rs b/src/backends/tolk.rs index 370da65..28c3f42 100644 --- a/src/backends/tolk.rs +++ b/src/backends/tolk.rs @@ -2,7 +2,7 @@ use log::{info, trace}; use tolk::Tolk as TolkPtr; -use crate::{Backend, Error, Features}; +use crate::{Backend, Error, Features, UtteranceId}; pub struct Tolk(TolkPtr); @@ -26,10 +26,10 @@ impl Backend for Tolk { } } - fn speak(&mut self, text: &str, interrupt: bool) -> Result<(), Error> { + fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error> { trace!("speak({}, {})", text, interrupt); self.0.speak(text, interrupt); - Ok(()) + Ok(None) } fn stop(&mut self) -> Result<(), Error> { diff --git a/src/backends/web.rs b/src/backends/web.rs index 664793c..89db8a9 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -1,8 +1,11 @@ #[cfg(target_arch = "wasm32")] +use std::sync::Mutex; + +use lazy_static::lazy_static; use log::{info, trace}; use web_sys::SpeechSynthesisUtterance; -use crate::{Backend, Error, Features}; +use crate::{Backend, Error, Features, UtteranceId}; pub struct Web { rate: f32, @@ -10,6 +13,10 @@ pub struct Web { volume: f32, } +lazy_static! { + static ref NEXT_UTTERANCE_ID: Mutex = Mutex::new(0); +} + impl Web { pub fn new() -> Result { info!("Initializing Web backend"); @@ -32,7 +39,7 @@ impl Backend for Web { } } - fn speak(&mut self, text: &str, interrupt: bool) -> Result<(), Error> { + fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error> { trace!("speak({}, {})", text, interrupt); let utterance = SpeechSynthesisUtterance::new_with_text(text).unwrap(); utterance.set_rate(self.rate); @@ -44,8 +51,12 @@ impl Backend for Web { if let Some(window) = web_sys::window() { let speech_synthesis = window.speech_synthesis().unwrap(); speech_synthesis.speak(&utterance); + let mut utterance_id = NEXT_UTTERANCE_ID.lock().unwrap(); + *utterance_id += 1; + Ok(Some(UtteranceId::Web(*utterance_id))) + } else { + Err(Error::NoneError) } - Ok(()) } fn stop(&mut self) -> Result<(), Error> { diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 7464f58..32fb219 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -1,4 +1,7 @@ #[cfg(windows)] +use std::sync::Mutex; + +use lazy_static::lazy_static; use log::{info, trace}; use tts_winrt_bindings::windows::media::core::MediaSource; @@ -7,7 +10,7 @@ use tts_winrt_bindings::windows::media::playback::{ }; use tts_winrt_bindings::windows::media::speech_synthesis::SpeechSynthesizer; -use crate::{Backend, Error, Features}; +use crate::{Backend, Error, Features, UtteranceId}; impl From for Error { fn from(e: winrt::Error) -> Self { @@ -21,6 +24,10 @@ pub struct WinRT { playback_list: MediaPlaybackList, } +lazy_static! { + static ref NEXT_UTTERANCE_ID: Mutex = Mutex::new(0); +} + impl WinRT { pub fn new() -> std::result::Result { info!("Initializing WinRT backend"); @@ -55,7 +62,11 @@ impl Backend for WinRT { } } - fn speak(&mut self, text: &str, interrupt: bool) -> std::result::Result<(), Error> { + fn speak( + &mut self, + text: &str, + interrupt: bool, + ) -> std::result::Result, Error> { trace!("speak({}, {})", text, interrupt); if interrupt { self.stop()?; @@ -76,7 +87,9 @@ impl Backend for WinRT { if !self.is_speaking()? { self.player.play()?; } - Ok(()) + let mut utterance_id = NEXT_UTTERANCE_ID.lock().unwrap(); + *utterance_id += 1; + Ok(Some(UtteranceId::WinRT(*utterance_id))) } fn stop(&mut self) -> std::result::Result<(), Error> { diff --git a/src/lib.rs b/src/lib.rs index 56b9f32..2d494cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,16 @@ pub enum Backends { AvFoundation, } +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum UtteranceId { + #[cfg(target_os = "linux")] + SpeechDispatcher(i32), + #[cfg(target_arch = "wasm32")] + Web(u64), + #[cfg(windows)] + WinRT(u64), +} + pub struct Features { pub stop: bool, pub rate: bool, @@ -80,7 +90,7 @@ pub enum Error { pub trait Backend { fn supported_features(&self) -> Features; - fn speak(&mut self, text: &str, interrupt: bool) -> Result<(), Error>; + fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error>; fn stop(&mut self) -> Result<(), Error>; fn min_rate(&self) -> f32; fn max_rate(&self) -> f32; @@ -184,9 +194,12 @@ impl TTS { /** * Speaks the specified text, optionally interrupting current speech. */ - pub fn speak>(&mut self, text: S, interrupt: bool) -> Result<&Self, Error> { - self.0.speak(text.into().as_str(), interrupt)?; - Ok(self) + pub fn speak>( + &mut self, + text: S, + interrupt: bool, + ) -> Result, Error> { + self.0.speak(text.into().as_str(), interrupt) } /** From 6b023c3071a4853b8fa49341215877f6b4c68b82 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 22 Sep 2020 14:08:19 -0500 Subject: [PATCH 03/17] Add AV Foundation support for returning utterance IDs. --- src/backends/appkit.rs | 6 +++--- src/backends/av_foundation.rs | 11 +++++++---- src/lib.rs | 2 ++ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/backends/appkit.rs b/src/backends/appkit.rs index 8584188..4b9a089 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, Error, Features}; +use crate::{Backend, Error, Features, UtteranceId}; pub struct AppKit(*mut Object, *mut Object); @@ -101,7 +101,7 @@ impl Backend for AppKit { } } - fn speak(&mut self, text: &str, interrupt: bool) -> Result<(), Error> { + fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error> { trace!("speak({}, {})", text, interrupt); if interrupt { self.stop()?; @@ -110,7 +110,7 @@ impl Backend for AppKit { let str = NSString::alloc(nil).init_str(text); let _: () = msg_send![self.1, enqueueAndSpeak: str]; } - Ok(()) + Ok(None) } fn stop(&mut self) -> Result<(), Error> { diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index a04eaea..fdd9cd7 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -1,12 +1,14 @@ #[cfg(any(target_os = "macos", target_os = "ios"))] #[link(name = "AVFoundation", kind = "framework")] +use std::sync::Mutex; + use cocoa_foundation::base::{id, nil}; use cocoa_foundation::foundation::NSString; use log::{info, trace}; use objc::runtime::*; use objc::*; -use crate::{Backend, Error, Features}; +use crate::{Backend, Error, Features, UtteranceId}; pub struct AvFoundation { synth: *mut Object, @@ -41,21 +43,22 @@ impl Backend for AvFoundation { } } - fn speak(&mut self, text: &str, interrupt: bool) -> Result<(), Error> { + fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error> { trace!("speak({}, {})", text, interrupt); if interrupt { self.stop()?; } + let utterance: id; unsafe { let str = NSString::alloc(nil).init_str(text); - let utterance: id = msg_send![class!(AVSpeechUtterance), alloc]; + utterance = msg_send![class!(AVSpeechUtterance), alloc]; let _: () = msg_send![utterance, initWithString: str]; let _: () = msg_send![utterance, setRate: self.rate]; let _: () = msg_send![utterance, setVolume: self.volume]; let _: () = msg_send![utterance, setPitchMultiplier: self.pitch]; let _: () = msg_send![self.synth, speakUtterance: utterance]; } - Ok(()) + Ok(Some(UtteranceId::AvFoundation(utterance))) } fn stop(&mut self) -> Result<(), Error> { diff --git a/src/lib.rs b/src/lib.rs index 2d494cf..c8166a2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,8 @@ pub enum UtteranceId { Web(u64), #[cfg(windows)] WinRT(u64), + #[cfg(any(target_os = "macos", target_os = "ios"))] + AvFoundation(id), } pub struct Features { From 017aa8863bd28dfdf10cebd2035123e716bf2db4 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 22 Sep 2020 14:29:45 -0500 Subject: [PATCH 04/17] Remove unused import and fix i~~OS builds. --- src/backends/av_foundation.rs | 2 -- src/lib.rs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index fdd9cd7..be1eb6d 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -1,7 +1,5 @@ #[cfg(any(target_os = "macos", target_os = "ios"))] #[link(name = "AVFoundation", kind = "framework")] -use std::sync::Mutex; - use cocoa_foundation::base::{id, nil}; use cocoa_foundation::foundation::NSString; use log::{info, trace}; diff --git a/src/lib.rs b/src/lib.rs index c8166a2..7e8be2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,7 @@ use std::boxed::Box; #[cfg(target_os = "macos")] use std::ffi::CStr; -#[cfg(target_os = "macos")] +#[cfg(any(target_os = "macos", target_os = "ios"))] use cocoa_foundation::base::id; #[cfg(target_os = "macos")] use libc::c_char; From f5f11b7cdfe21cc41b50cd8edca156c7d1455d6c Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 22 Sep 2020 14:51:59 -0500 Subject: [PATCH 05/17] Switch to using MediaPlaybackItem as WinRT utterance ID. --- src/backends/winrt.rs | 13 ++----------- src/lib.rs | 7 +++++-- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 32fb219..f558c91 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -1,7 +1,4 @@ #[cfg(windows)] -use std::sync::Mutex; - -use lazy_static::lazy_static; use log::{info, trace}; use tts_winrt_bindings::windows::media::core::MediaSource; @@ -24,10 +21,6 @@ pub struct WinRT { playback_list: MediaPlaybackList, } -lazy_static! { - static ref NEXT_UTTERANCE_ID: Mutex = Mutex::new(0); -} - impl WinRT { pub fn new() -> std::result::Result { info!("Initializing WinRT backend"); @@ -83,13 +76,11 @@ impl Backend for WinRT { self.reinit_player()?; } } - self.playback_list.items()?.append(item)?; + self.playback_list.items()?.append(&item)?; if !self.is_speaking()? { self.player.play()?; } - let mut utterance_id = NEXT_UTTERANCE_ID.lock().unwrap(); - *utterance_id += 1; - Ok(Some(UtteranceId::WinRT(*utterance_id))) + Ok(Some(UtteranceId::WinRT(item))) } fn stop(&mut self) -> std::result::Result<(), Error> { diff --git a/src/lib.rs b/src/lib.rs index 2d494cf..fc0558a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,9 @@ use libc::c_char; use objc::{class, msg_send, sel, sel_impl}; use thiserror::Error; +#[cfg(windows)] +use tts_winrt_bindings::windows::media::playback::MediaPlaybackItem; + mod backends; pub enum Backends { @@ -40,14 +43,14 @@ pub enum Backends { AvFoundation, } -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum UtteranceId { #[cfg(target_os = "linux")] SpeechDispatcher(i32), #[cfg(target_arch = "wasm32")] Web(u64), #[cfg(windows)] - WinRT(u64), + WinRT(MediaPlaybackItem), } pub struct Features { From 61522610cd851850f0f152224e4bf23255005162 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 23 Sep 2020 10:12:51 -0500 Subject: [PATCH 06/17] Implement utterance begin/end callback framework, and set up for Speech-Dispatcher. --- examples/hello_world.rs | 12 +++++ src/backends/mod.rs | 2 +- src/backends/speech_dispatcher.rs | 32 ++++++++--- src/lib.rs | 90 ++++++++++++++++++++++++++++++- 4 files changed, 127 insertions(+), 9 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index e95c53c..9b1954b 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -12,6 +12,18 @@ use tts::*; fn main() -> Result<(), Error> { env_logger::init(); let mut tts = TTS::default()?; + let Features { + utterance_callbacks, + .. + } = tts.supported_features(); + if utterance_callbacks { + tts.on_utterance_begin(Some(|utterance| { + println!("Started speaking {:?}", utterance) + }))?; + tts.on_utterance_end(Some(|utterance| { + println!("Finished speaking {:?}", utterance) + }))?; + } tts.speak("Hello, world.", false)?; let Features { rate, .. } = tts.supported_features(); if rate { diff --git a/src/backends/mod.rs b/src/backends/mod.rs index c999faf..a9f094f 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -17,7 +17,7 @@ mod appkit; mod av_foundation; #[cfg(target_os = "linux")] -pub use self::speech_dispatcher::*; +pub(crate) use self::speech_dispatcher::*; #[cfg(windows)] pub use self::tolk::*; diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 5717c61..2b9d6b5 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -1,14 +1,15 @@ #[cfg(target_os = "linux")] use std::collections::HashMap; +use std::convert::TryInto; use std::sync::Mutex; use lazy_static::*; use log::{info, trace}; use speech_dispatcher::*; -use crate::{Backend, Error, Features, UtteranceId}; +use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; -pub struct SpeechDispatcher(Connection); +pub(crate) struct SpeechDispatcher(Connection); lazy_static! { static ref SPEAKING: Mutex> = { @@ -18,19 +19,33 @@ lazy_static! { } impl SpeechDispatcher { - pub fn new() -> Self { + pub(crate) fn new() -> Self { info!("Initializing SpeechDispatcher backend"); 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); - sd.0.on_begin(Some(|_msg_id, client_id| { + sd.0.on_begin(Some(|msg_id, client_id| { let mut speaking = SPEAKING.lock().unwrap(); speaking.insert(client_id, true); + let callbacks = CALLBACKS.lock().unwrap(); + let backend_id = BackendId::SpeechDispatcher(client_id); + let cb = callbacks.get(&backend_id).unwrap(); + let utterance_id = UtteranceId::SpeechDispatcher(msg_id); + if let Some(f) = cb.utterance_begin { + f(utterance_id); + } })); - sd.0.on_end(Some(|_msg_id, client_id| { + sd.0.on_end(Some(|msg_id, client_id| { let mut speaking = SPEAKING.lock().unwrap(); speaking.insert(client_id, false); + let callbacks = CALLBACKS.lock().unwrap(); + let backend_id = BackendId::SpeechDispatcher(client_id); + let cb = callbacks.get(&backend_id).unwrap(); + let utterance_id = UtteranceId::SpeechDispatcher(msg_id); + if let Some(f) = cb.utterance_end { + f(utterance_id); + } })); sd.0.on_cancel(Some(|_msg_id, client_id| { let mut speaking = SPEAKING.lock().unwrap(); @@ -49,6 +64,10 @@ impl SpeechDispatcher { } impl Backend for SpeechDispatcher { + fn id(&self) -> Option { + Some(BackendId::SpeechDispatcher(self.0.client_id())) + } + fn supported_features(&self) -> Features { Features { stop: true, @@ -56,6 +75,7 @@ impl Backend for SpeechDispatcher { pitch: true, volume: true, is_speaking: true, + utterance_callbacks: true, } } @@ -73,7 +93,7 @@ impl Backend for SpeechDispatcher { self.0.set_punctuation(Punctuation::None); } if let Some(id) = id { - Ok(Some(UtteranceId::SpeechDispatcher(id))) + Ok(Some(UtteranceId::SpeechDispatcher(id.try_into().unwrap()))) } else { Err(Error::NoneError) } diff --git a/src/lib.rs b/src/lib.rs index fc0558a..c98a625 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,11 +12,14 @@ */ use std::boxed::Box; +use std::collections::HashMap; #[cfg(target_os = "macos")] use std::ffi::CStr; +use std::sync::Mutex; #[cfg(target_os = "macos")] use cocoa_foundation::base::id; +use lazy_static::lazy_static; #[cfg(target_os = "macos")] use libc::c_char; #[cfg(target_os = "macos")] @@ -43,10 +46,20 @@ pub enum Backends { AvFoundation, } +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum BackendId { + #[cfg(target_os = "linux")] + SpeechDispatcher(u64), + #[cfg(target_arch = "wasm32")] + Web(u64), + #[cfg(windows)] + WinRT(MediaPlaybackItem), +} + #[derive(Clone, Debug, PartialEq)] pub enum UtteranceId { #[cfg(target_os = "linux")] - SpeechDispatcher(i32), + SpeechDispatcher(u64), #[cfg(target_arch = "wasm32")] Web(u64), #[cfg(windows)] @@ -59,6 +72,7 @@ pub struct Features { pub pitch: bool, pub volume: bool, pub is_speaking: bool, + pub utterance_callbacks: bool, } impl Default for Features { @@ -69,6 +83,7 @@ impl Default for Features { pitch: false, volume: false, is_speaking: false, + utterance_callbacks: false, } } } @@ -92,6 +107,7 @@ pub enum Error { } pub trait Backend { + fn id(&self) -> Option; fn supported_features(&self) -> Features; fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error>; fn stop(&mut self) -> Result<(), Error>; @@ -113,6 +129,19 @@ pub trait Backend { fn is_speaking(&self) -> Result; } +#[derive(Default)] +struct Callbacks { + utterance_begin: Option, + utterance_end: Option, +} + +lazy_static! { + static ref CALLBACKS: Mutex> = { + let m: HashMap = HashMap::new(); + Mutex::new(m) + }; +} + pub struct TTS(Box); unsafe impl std::marker::Send for TTS {} @@ -124,7 +153,7 @@ impl TTS { * Create a new `TTS` instance with the specified backend. */ pub fn new(backend: Backends) -> Result { - match backend { + let backend = match backend { #[cfg(target_os = "linux")] Backends::SpeechDispatcher => Ok(TTS(Box::new(backends::SpeechDispatcher::new()))), #[cfg(target_arch = "wasm32")] @@ -150,6 +179,16 @@ impl TTS { Backends::AppKit => Ok(TTS(Box::new(backends::AppKit::new()))), #[cfg(any(target_os = "macos", target_os = "ios"))] Backends::AvFoundation => Ok(TTS(Box::new(backends::AvFoundation::new()))), + }; + let mut callbacks = CALLBACKS.lock().unwrap(); + if backend.is_ok() { + let backend = backend.unwrap(); + if let Some(id) = backend.0.id() { + callbacks.insert(id, Callbacks::default()); + } + Ok(backend) + } else { + backend } } @@ -387,4 +426,51 @@ impl TTS { Err(Error::UnsupportedFeature) } } + + /** + * Called when this speech synthesizer begins speaking an utterance. + */ + pub fn on_utterance_begin(&self, callback: Option) -> Result<(), Error> { + let Features { + utterance_callbacks, + .. + } = self.supported_features(); + if utterance_callbacks { + let mut callbacks = CALLBACKS.lock().unwrap(); + let id = self.0.id().unwrap(); + let mut callbacks = callbacks.get_mut(&id).unwrap(); + callbacks.utterance_begin = callback; + Ok(()) + } else { + Err(Error::UnsupportedFeature) + } + } + + /** + * Called when this speech synthesizer finishes speaking an utterance. + */ + pub fn on_utterance_end(&self, callback: Option) -> Result<(), Error> { + let Features { + utterance_callbacks, + .. + } = self.supported_features(); + if utterance_callbacks { + let mut callbacks = CALLBACKS.lock().unwrap(); + let id = self.0.id().unwrap(); + let mut callbacks = callbacks.get_mut(&id).unwrap(); + callbacks.utterance_end = callback; + Ok(()) + } else { + Err(Error::UnsupportedFeature) + } + } +} + +impl Drop for TTS { + fn drop(&mut self) { + if let Some(id) = self.0.id() { + let mut callbacks = CALLBACKS.lock().unwrap(); + callbacks.remove(&id); + } + } } From 6788277a4dadcaddc3b9f72502a5f90fafae82cc Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 23 Sep 2020 10:31:21 -0500 Subject: [PATCH 07/17] Implement framework for utterance callbacks in Windows backends, though they aren't currently called. --- src/backends/tolk.rs | 6 +++++- src/backends/winrt.rs | 23 ++++++++++++++++++++--- src/lib.rs | 4 ++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/backends/tolk.rs b/src/backends/tolk.rs index 28c3f42..0be20d1 100644 --- a/src/backends/tolk.rs +++ b/src/backends/tolk.rs @@ -2,7 +2,7 @@ use log::{info, trace}; use tolk::Tolk as TolkPtr; -use crate::{Backend, Error, Features, UtteranceId}; +use crate::{Backend, BackendId, Error, Features, UtteranceId}; pub struct Tolk(TolkPtr); @@ -19,6 +19,10 @@ impl Tolk { } impl Backend for Tolk { + fn id(&self) -> Option { + None + } + fn supported_features(&self) -> Features { Features { stop: true, diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index f558c91..c72040c 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -1,4 +1,7 @@ #[cfg(windows)] +use std::sync::Mutex; + +use lazy_static::lazy_static; use log::{info, trace}; use tts_winrt_bindings::windows::media::core::MediaSource; @@ -7,7 +10,7 @@ use tts_winrt_bindings::windows::media::playback::{ }; use tts_winrt_bindings::windows::media::speech_synthesis::SpeechSynthesizer; -use crate::{Backend, Error, Features, UtteranceId}; +use crate::{Backend, BackendId, Error, Features, UtteranceId}; impl From for Error { fn from(e: winrt::Error) -> Self { @@ -16,11 +19,16 @@ impl From for Error { } pub struct WinRT { + id: BackendId, synth: SpeechSynthesizer, player: MediaPlayer, playback_list: MediaPlaybackList, } +lazy_static! { + static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); +} + impl WinRT { pub fn new() -> std::result::Result { info!("Initializing WinRT backend"); @@ -28,11 +36,15 @@ impl WinRT { let player = MediaPlayer::new()?; player.set_auto_play(true)?; player.set_source(&playback_list)?; - Ok(Self { + let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); + let rv = Ok(Self { + id: BackendId::WinRT(*backend_id), synth: SpeechSynthesizer::new()?, player: player, playback_list: playback_list, - }) + }); + *backend_id += 1; + rv } fn reinit_player(&mut self) -> std::result::Result<(), Error> { @@ -45,6 +57,10 @@ impl WinRT { } impl Backend for WinRT { + fn id(&self) -> Option { + Some(self.id) + } + fn supported_features(&self) -> Features { Features { stop: true, @@ -52,6 +68,7 @@ impl Backend for WinRT { pitch: true, volume: true, is_speaking: true, + ..Default::default() } } diff --git a/src/lib.rs b/src/lib.rs index c98a625..e4c6d8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,14 +46,14 @@ pub enum Backends { AvFoundation, } -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum BackendId { #[cfg(target_os = "linux")] SpeechDispatcher(u64), #[cfg(target_arch = "wasm32")] Web(u64), #[cfg(windows)] - WinRT(MediaPlaybackItem), + WinRT(u64), } #[derive(Clone, Debug, PartialEq)] From c5524113ff286682e0926bd3962190b826d603ce Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 23 Sep 2020 10:33:30 -0500 Subject: [PATCH 08/17] Document the fact that we only need an NSRunLoop in the example because there isn't one already. --- examples/hello_world.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 9b1954b..4d7accd 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -61,6 +61,8 @@ fn main() -> Result<(), Error> { } 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. + // 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() }; From bd57075d53fcce210a6411ebb8d4d0b0e937a56c Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 23 Sep 2020 11:28:56 -0500 Subject: [PATCH 09/17] Implement unused framework for AVFoundation callbacks. --- src/backends/appkit.rs | 10 +++++++--- src/backends/av_foundation.rs | 27 ++++++++++++++++++++++----- src/backends/mod.rs | 4 ++-- src/lib.rs | 4 +++- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/backends/appkit.rs b/src/backends/appkit.rs index 4b9a089..647cc3f 100644 --- a/src/backends/appkit.rs +++ b/src/backends/appkit.rs @@ -7,12 +7,12 @@ use objc::declare::ClassDecl; use objc::runtime::*; use objc::*; -use crate::{Backend, Error, Features, UtteranceId}; +use crate::{Backend, BackendId, Error, Features, UtteranceId}; -pub struct AppKit(*mut Object, *mut Object); +pub(crate) struct AppKit(*mut Object, *mut Object); impl AppKit { - pub fn new() -> Self { + pub(crate) fn new() -> Self { info!("Initializing AppKit backend"); unsafe { let obj: *mut Object = msg_send![class!(NSSpeechSynthesizer), new]; @@ -91,6 +91,10 @@ impl AppKit { } impl Backend for AppKit { + fn id(&self) -> Option { + None + } + fn supported_features(&self) -> Features { Features { stop: true, diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index be1eb6d..bdd5620 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -1,36 +1,52 @@ #[cfg(any(target_os = "macos", target_os = "ios"))] #[link(name = "AVFoundation", kind = "framework")] +use std::sync::Mutex; + use cocoa_foundation::base::{id, nil}; use cocoa_foundation::foundation::NSString; +use lazy_static::lazy_static; use log::{info, trace}; use objc::runtime::*; use objc::*; -use crate::{Backend, Error, Features, UtteranceId}; +use crate::{Backend, BackendId, Error, Features, UtteranceId}; -pub struct AvFoundation { +pub(crate) struct AvFoundation { + id: BackendId, synth: *mut Object, rate: f32, volume: f32, pitch: f32, } +lazy_static! { + static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); +} + impl AvFoundation { - pub fn new() -> Self { + pub(crate) fn new() -> Self { info!("Initializing AVFoundation backend"); - unsafe { + let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); + let rv = unsafe { let synth: *mut Object = msg_send![class!(AVSpeechSynthesizer), new]; AvFoundation { + id: BackendId::AvFoundation(*backend_id), synth: synth, rate: 0.5, volume: 1., pitch: 1., } - } + }; + *backend_id += 1; + rv } } impl Backend for AvFoundation { + fn id(&self) -> Option { + Some(self.id) + } + fn supported_features(&self) -> Features { Features { stop: true, @@ -38,6 +54,7 @@ impl Backend for AvFoundation { pitch: true, volume: true, is_speaking: true, + utterance_callbacks: true, } } diff --git a/src/backends/mod.rs b/src/backends/mod.rs index a9f094f..3e7106b 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -26,7 +26,7 @@ pub use self::tolk::*; pub use self::web::*; #[cfg(target_os = "macos")] -pub use self::appkit::*; +pub(crate) use self::appkit::*; #[cfg(any(target_os = "macos", target_os = "ios"))] -pub use self::av_foundation::*; +pub(crate) use self::av_foundation::*; diff --git a/src/lib.rs b/src/lib.rs index b9d4aba..ced1f17 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,7 +55,7 @@ pub enum BackendId { #[cfg(windows)] WinRT(u64), #[cfg(any(target_os = "macos", target_os = "ios"))] - AvFoundation(id), + AvFoundation(u64), } #[derive(Clone, Debug, PartialEq)] @@ -66,6 +66,8 @@ pub enum UtteranceId { Web(u64), #[cfg(windows)] WinRT(MediaPlaybackItem), + #[cfg(any(target_os = "macos", target_os = "ios"))] + AvFoundation(id), } pub struct Features { From 251fb8d8c14671c944097850fc64370d41a9bbcf Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 23 Sep 2020 12:21:05 -0500 Subject: [PATCH 10/17] Implement callbacks on AVFoundation. --- src/backends/av_foundation.rs | 68 +++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index bdd5620..e097b1e 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -6,13 +6,14 @@ use cocoa_foundation::base::{id, nil}; use cocoa_foundation::foundation::NSString; use lazy_static::lazy_static; use log::{info, trace}; -use objc::runtime::*; -use objc::*; +use objc::runtime::{Object, Sel}; +use objc::{class, declare::ClassDecl, msg_send, sel, sel_impl}; -use crate::{Backend, BackendId, Error, Features, UtteranceId}; +use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; pub(crate) struct AvFoundation { id: BackendId, + delegate: *mut Object, synth: *mut Object, rate: f32, volume: f32, @@ -26,11 +27,71 @@ lazy_static! { impl AvFoundation { pub(crate) fn new() -> Self { info!("Initializing AVFoundation backend"); + let mut decl = ClassDecl::new("MyNSSpeechSynthesizerDelegate", class!(NSObject)).unwrap(); + decl.add_ivar::("backend_id"); + + extern "C" fn speech_synthesizer_did_start_speech_utterance( + this: &Object, + _: Sel, + synth: *const Object, + utterance: id, + ) { + unsafe { + let backend_id: u64 = *this.get_ivar("backend_id"); + let backend_id = BackendId::AvFoundation(backend_id); + let callbacks = CALLBACKS.lock().unwrap(); + let callbacks = callbacks.get(&backend_id).unwrap(); + if let Some(callback) = callbacks.utterance_begin { + let utterance_id = UtteranceId::AvFoundation(utterance); + callback(utterance_id); + } + } + } + + extern "C" fn speech_synthesizer_did_finish_speech_utterance( + this: &Object, + _: Sel, + synth: *const Object, + utterance: id, + ) { + unsafe { + let backend_id: u64 = *this.get_ivar("backend_id"); + let backend_id = BackendId::AvFoundation(backend_id); + let callbacks = CALLBACKS.lock().unwrap(); + let callbacks = callbacks.get(&backend_id).unwrap(); + if let Some(callback) = callbacks.utterance_end { + let utterance_id = UtteranceId::AvFoundation(utterance); + callback(utterance_id); + } + } + } + + unsafe { + decl.add_method( + sel!(speechSynthesizer:didStartSpeechUtterance:), + speech_synthesizer_did_start_speech_utterance + as extern "C" fn(&Object, Sel, *const Object, id) -> (), + ); + decl.add_method( + sel!(speechSynthesizer:didFinishSpeechUtterance:), + speech_synthesizer_did_finish_speech_utterance + as extern "C" fn(&Object, Sel, *const Object, id) -> (), + ); + } + + let delegate_class = decl.register(); + let delegate_obj: *mut Object = unsafe { msg_send![delegate_class, new] }; let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); let rv = unsafe { let synth: *mut Object = msg_send![class!(AVSpeechSynthesizer), new]; + delegate_obj + .as_mut() + .unwrap() + .set_ivar("backend_id", *backend_id); + let _: () = msg_send![synth, setDelegate: delegate_obj]; AvFoundation { id: BackendId::AvFoundation(*backend_id), + delegate: delegate_obj, synth: synth, rate: 0.5, volume: 1., @@ -157,6 +218,7 @@ impl Backend for AvFoundation { impl Drop for AvFoundation { fn drop(&mut self) { unsafe { + let _: Object = msg_send![self.delegate, release]; let _: Object = msg_send![self.synth, release]; } } From 532d5d9b580a15e7abad162bc291cc093f9d1df1 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 23 Sep 2020 12:23:46 -0500 Subject: [PATCH 11/17] Tighten up access. --- src/backends/mod.rs | 2 +- src/backends/tolk.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 3e7106b..6274692 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -20,7 +20,7 @@ mod av_foundation; pub(crate) use self::speech_dispatcher::*; #[cfg(windows)] -pub use self::tolk::*; +pub(crate) use self::tolk::*; #[cfg(target_arch = "wasm32")] pub use self::web::*; diff --git a/src/backends/tolk.rs b/src/backends/tolk.rs index 0be20d1..66c88e7 100644 --- a/src/backends/tolk.rs +++ b/src/backends/tolk.rs @@ -4,10 +4,10 @@ use tolk::Tolk as TolkPtr; use crate::{Backend, BackendId, Error, Features, UtteranceId}; -pub struct Tolk(TolkPtr); +pub(crate) struct Tolk(TolkPtr); impl Tolk { - pub fn new() -> Option { + pub(crate) fn new() -> Option { info!("Initializing Tolk backend"); let tolk = TolkPtr::new(); if tolk.detect_screen_reader().is_some() { From a22242af504da0b29fbdb7d3b72401809943149c Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 24 Sep 2020 14:26:30 -0500 Subject: [PATCH 12/17] Implement callbacks for web backend. --- Cargo.toml | 4 ++-- src/backends/web.rs | 46 +++++++++++++++++++++++++++++++++++++-------- src/lib.rs | 4 +++- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 082c2a4..96fdb72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ exclude = ["*.cfg", "*.yml"] edition = "2018" [lib] -crate-type = ["lib", "staticlib"] +crate-type = ["lib", "cdylib", "staticlib"] [dependencies] lazy_static = "1" @@ -34,4 +34,4 @@ objc = "0.2" [target.wasm32-unknown-unknown.dependencies] wasm-bindgen = "0.2" -web-sys = { version = "0.3", features = ["SpeechSynthesis", "SpeechSynthesisUtterance", "Window", ] } +web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "SpeechSynthesisEvent", "SpeechSynthesisUtterance", "Window", ] } diff --git a/src/backends/web.rs b/src/backends/web.rs index 89db8a9..619a350 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -3,32 +3,43 @@ use std::sync::Mutex; use lazy_static::lazy_static; use log::{info, trace}; -use web_sys::SpeechSynthesisUtterance; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use web_sys::{SpeechSynthesisEvent, SpeechSynthesisUtterance}; -use crate::{Backend, Error, Features, UtteranceId}; +use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; pub struct Web { + id: BackendId, rate: f32, pitch: f32, volume: f32, } lazy_static! { - static ref NEXT_UTTERANCE_ID: Mutex = Mutex::new(0); + static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); } impl Web { pub fn new() -> Result { info!("Initializing Web backend"); - Ok(Web { + let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); + let rv = Web { + id: BackendId::Web(*backend_id), rate: 1., pitch: 1., volume: 1., - }) + }; + *backend_id += 1; + Ok(rv) } } impl Backend for Web { + fn id(&self) -> Option { + Some(self.id) + } + fn supported_features(&self) -> Features { Features { stop: true, @@ -36,6 +47,7 @@ impl Backend for Web { pitch: true, volume: true, is_speaking: true, + utterance_callbacks: true, } } @@ -45,15 +57,33 @@ impl Backend for Web { utterance.set_rate(self.rate); utterance.set_pitch(self.pitch); utterance.set_volume(self.volume); + let id = self.id().unwrap(); + let utterance_id = UtteranceId::Web(utterance.clone()); + let callback = Closure::wrap(Box::new(move |evt: SpeechSynthesisEvent| { + let callbacks = CALLBACKS.lock().unwrap(); + let callback = callbacks.get(&id).unwrap(); + if let Some(f) = callback.utterance_begin { + let utterance_id = UtteranceId::Web(evt.utterance()); + f(utterance_id); + } + }) as Box); + utterance.set_onstart(Some(callback.as_ref().unchecked_ref())); + let callback = Closure::wrap(Box::new(move |evt: SpeechSynthesisEvent| { + let callbacks = CALLBACKS.lock().unwrap(); + let callback = callbacks.get(&id).unwrap(); + if let Some(f) = callback.utterance_end { + let utterance_id = UtteranceId::Web(evt.utterance()); + f(utterance_id); + } + }) as Box); + utterance.set_onend(Some(callback.as_ref().unchecked_ref())); if interrupt { self.stop()?; } if let Some(window) = web_sys::window() { let speech_synthesis = window.speech_synthesis().unwrap(); speech_synthesis.speak(&utterance); - let mut utterance_id = NEXT_UTTERANCE_ID.lock().unwrap(); - *utterance_id += 1; - Ok(Some(UtteranceId::Web(*utterance_id))) + Ok(Some(utterance_id)) } else { Err(Error::NoneError) } diff --git a/src/lib.rs b/src/lib.rs index ced1f17..622df31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,8 @@ use libc::c_char; #[cfg(target_os = "macos")] use objc::{class, msg_send, sel, sel_impl}; use thiserror::Error; +#[cfg(target_arch = "wasm32")] +use web_sys::SpeechSynthesisUtterance; #[cfg(windows)] use tts_winrt_bindings::windows::media::playback::MediaPlaybackItem; @@ -63,7 +65,7 @@ pub enum UtteranceId { #[cfg(target_os = "linux")] SpeechDispatcher(u64), #[cfg(target_arch = "wasm32")] - Web(u64), + Web(SpeechSynthesisUtterance), #[cfg(windows)] WinRT(MediaPlaybackItem), #[cfg(any(target_os = "macos", target_os = "ios"))] From 96e5d21e24877880e2d7aad9052e80be53e3a427 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 24 Sep 2020 17:56:46 -0500 Subject: [PATCH 13/17] Implement callbacks for WinRT. --- src/backends/winrt.rs | 103 ++++++++++++++++++++++++++++++++++++++---- src/lib.rs | 2 +- 2 files changed, 95 insertions(+), 10 deletions(-) diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index c72040c..815607a 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -1,16 +1,19 @@ #[cfg(windows)] +use std::collections::HashMap; use std::sync::Mutex; use lazy_static::lazy_static; use log::{info, trace}; +use winrt::ComInterface; -use tts_winrt_bindings::windows::media::core::MediaSource; use tts_winrt_bindings::windows::media::playback::{ - MediaPlaybackItem, MediaPlaybackList, MediaPlaybackState, MediaPlayer, + CurrentMediaPlaybackItemChangedEventArgs, MediaPlaybackItem, MediaPlaybackList, + MediaPlaybackState, MediaPlayer, }; use tts_winrt_bindings::windows::media::speech_synthesis::SpeechSynthesizer; +use tts_winrt_bindings::windows::{foundation::TypedEventHandler, media::core::MediaSource}; -use crate::{Backend, BackendId, Error, Features, UtteranceId}; +use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; impl From for Error { fn from(e: winrt::Error) -> Self { @@ -27,6 +30,18 @@ pub struct WinRT { lazy_static! { static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); + static ref BACKEND_TO_MEDIA_PLAYER: Mutex> = { + let v: HashMap = HashMap::new(); + Mutex::new(v) + }; + static ref BACKEND_TO_PLAYBACK_LIST: Mutex> = { + let v: HashMap = HashMap::new(); + Mutex::new(v) + }; + static ref LAST_SPOKEN_UTTERANCE: Mutex> = { + let v: HashMap = HashMap::new(); + Mutex::new(v) + }; } impl WinRT { @@ -37,14 +52,16 @@ impl WinRT { player.set_auto_play(true)?; player.set_source(&playback_list)?; let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); - let rv = Ok(Self { - id: BackendId::WinRT(*backend_id), + let bid = BackendId::WinRT(*backend_id); + let mut rv = Self { + id: bid, synth: SpeechSynthesizer::new()?, player: player, playback_list: playback_list, - }); + }; *backend_id += 1; - rv + Self::init_callbacks(&mut rv)?; + Ok(rv) } fn reinit_player(&mut self) -> std::result::Result<(), Error> { @@ -52,6 +69,61 @@ impl WinRT { self.player = MediaPlayer::new()?; self.player.set_auto_play(true)?; self.player.set_source(&self.playback_list)?; + self.init_callbacks()?; + Ok(()) + } + + fn init_callbacks(&mut self) -> Result<(), winrt::Error> { + let id = self.id().unwrap(); + let mut backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); + backend_to_media_player.insert(id, self.player.clone()); + self.player + .media_ended(TypedEventHandler::new(|sender, _args| { + let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); + let id = backend_to_media_player.iter().find(|v| v.1 == sender); + if let Some(id) = id { + let id = id.0; + let callbacks = CALLBACKS.lock().unwrap(); + let callbacks = callbacks.get(&id).unwrap(); + if let Some(callback) = callbacks.utterance_end { + let last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); + if let Some(utterance_id) = last_spoken_utterance.get(&id) { + callback(utterance_id.clone()); + } + } + } + Ok(()) + }))?; + let mut backend_to_playback_list = BACKEND_TO_PLAYBACK_LIST.lock().unwrap(); + backend_to_playback_list.insert(id, self.playback_list.clone()); + self.playback_list + .current_item_changed(TypedEventHandler::new( + |sender: &MediaPlaybackList, args: &CurrentMediaPlaybackItemChangedEventArgs| { + let backend_to_playback_list = BACKEND_TO_PLAYBACK_LIST.lock().unwrap(); + let id = backend_to_playback_list.iter().find(|v| v.1 == sender); + if let Some(id) = id { + let id = id.0; + let callbacks = CALLBACKS.lock().unwrap(); + let callbacks = callbacks.get(&id).unwrap(); + let old_item = args.old_item()?; + if !old_item.is_null() { + if let Some(callback) = callbacks.utterance_end { + callback(UtteranceId::WinRT(old_item)); + } + } + let new_item = args.new_item()?; + if !new_item.is_null() { + let utterance_id = UtteranceId::WinRT(new_item); + let mut last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); + last_spoken_utterance.insert(*id, utterance_id.clone()); + if let Some(callback) = callbacks.utterance_begin { + callback(utterance_id); + } + } + } + Ok(()) + }, + ))?; Ok(()) } } @@ -68,7 +140,7 @@ impl Backend for WinRT { pitch: true, volume: true, is_speaking: true, - ..Default::default() + utterance_callbacks: true, } } @@ -97,7 +169,8 @@ impl Backend for WinRT { if !self.is_speaking()? { self.player.play()?; } - Ok(Some(UtteranceId::WinRT(item))) + let utterance_id = UtteranceId::WinRT(item); + Ok(Some(utterance_id)) } fn stop(&mut self) -> std::result::Result<(), Error> { @@ -178,3 +251,15 @@ impl Backend for WinRT { Ok(playing) } } + +impl Drop for WinRT { + fn drop(&mut self) { + let id = self.id().unwrap(); + let mut backend_to_playback_list = BACKEND_TO_PLAYBACK_LIST.lock().unwrap(); + backend_to_playback_list.remove(&id); + let mut backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); + backend_to_media_player.remove(&id); + let mut last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); + last_spoken_utterance.remove(&id); + } +} diff --git a/src/lib.rs b/src/lib.rs index 622df31..5560ed1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,10 +186,10 @@ impl TTS { #[cfg(any(target_os = "macos", target_os = "ios"))] Backends::AvFoundation => Ok(TTS(Box::new(backends::AvFoundation::new()))), }; - let mut callbacks = CALLBACKS.lock().unwrap(); if backend.is_ok() { let backend = backend.unwrap(); if let Some(id) = backend.0.id() { + let mut callbacks = CALLBACKS.lock().unwrap(); callbacks.insert(id, Callbacks::default()); } Ok(backend) From 2c70f77a15d7d1d10a8feffa8f7c955311a02dc9 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 24 Sep 2020 18:21:14 -0500 Subject: [PATCH 14/17] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 96fdb72..cf649b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.6.4" +version = "0.7.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 1f22843086febfde925edfeb7c21fc497f580519 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 25 Sep 2020 11:08:19 -0500 Subject: [PATCH 15/17] Refactor Linux, Windows, and Wasm platforms to use FnMut for callbacks, and bump version. --- Cargo.toml | 4 ++-- examples/hello_world.rs | 8 ++++---- src/backends/speech_dispatcher.rs | 12 ++++++------ src/backends/web.rs | 12 ++++++------ src/backends/winrt.rs | 16 ++++++++-------- src/lib.rs | 18 ++++++++++++++---- 6 files changed, 40 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cf649b6..9bce264 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.7.0" +version = "0.8.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -25,7 +25,7 @@ winrt = "0.7" tts_winrt_bindings = { version = "0.1", path="winrt_bindings" } [target.'cfg(target_os = "linux")'.dependencies] -speech-dispatcher = "0.6" +speech-dispatcher = "0.7" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] cocoa-foundation = "0.1" diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 4d7accd..d21fe9d 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -17,12 +17,12 @@ fn main() -> Result<(), Error> { .. } = tts.supported_features(); if utterance_callbacks { - tts.on_utterance_begin(Some(|utterance| { + tts.on_utterance_begin(Some(Box::new(|utterance| { println!("Started speaking {:?}", utterance) - }))?; - tts.on_utterance_end(Some(|utterance| { + })))?; + tts.on_utterance_end(Some(Box::new(|utterance| { println!("Finished speaking {:?}", utterance) - }))?; + })))?; } tts.speak("Hello, world.", false)?; let Features { rate, .. } = tts.supported_features(); diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 2b9d6b5..8ad6020 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -28,22 +28,22 @@ impl SpeechDispatcher { sd.0.on_begin(Some(|msg_id, client_id| { let mut speaking = SPEAKING.lock().unwrap(); speaking.insert(client_id, true); - let callbacks = CALLBACKS.lock().unwrap(); + let mut callbacks = CALLBACKS.lock().unwrap(); let backend_id = BackendId::SpeechDispatcher(client_id); - let cb = callbacks.get(&backend_id).unwrap(); + let cb = callbacks.get_mut(&backend_id).unwrap(); let utterance_id = UtteranceId::SpeechDispatcher(msg_id); - if let Some(f) = cb.utterance_begin { + if let Some(f) = cb.utterance_begin.as_mut() { f(utterance_id); } })); sd.0.on_end(Some(|msg_id, client_id| { let mut speaking = SPEAKING.lock().unwrap(); speaking.insert(client_id, false); - let callbacks = CALLBACKS.lock().unwrap(); + let mut callbacks = CALLBACKS.lock().unwrap(); let backend_id = BackendId::SpeechDispatcher(client_id); - let cb = callbacks.get(&backend_id).unwrap(); + let cb = callbacks.get_mut(&backend_id).unwrap(); let utterance_id = UtteranceId::SpeechDispatcher(msg_id); - if let Some(f) = cb.utterance_end { + if let Some(f) = cb.utterance_end.as_mut() { f(utterance_id); } })); diff --git a/src/backends/web.rs b/src/backends/web.rs index 619a350..32be8f7 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -60,18 +60,18 @@ impl Backend for Web { let id = self.id().unwrap(); let utterance_id = UtteranceId::Web(utterance.clone()); let callback = Closure::wrap(Box::new(move |evt: SpeechSynthesisEvent| { - let callbacks = CALLBACKS.lock().unwrap(); - let callback = callbacks.get(&id).unwrap(); - if let Some(f) = callback.utterance_begin { + let mut callbacks = CALLBACKS.lock().unwrap(); + let callback = callbacks.get_mut(&id).unwrap(); + if let Some(f) = callback.utterance_begin.as_mut() { let utterance_id = UtteranceId::Web(evt.utterance()); f(utterance_id); } }) as Box); utterance.set_onstart(Some(callback.as_ref().unchecked_ref())); let callback = Closure::wrap(Box::new(move |evt: SpeechSynthesisEvent| { - let callbacks = CALLBACKS.lock().unwrap(); - let callback = callbacks.get(&id).unwrap(); - if let Some(f) = callback.utterance_end { + let mut callbacks = CALLBACKS.lock().unwrap(); + let callback = callbacks.get_mut(&id).unwrap(); + if let Some(f) = callback.utterance_end.as_mut() { let utterance_id = UtteranceId::Web(evt.utterance()); f(utterance_id); } diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 815607a..1625040 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -83,9 +83,9 @@ impl WinRT { let id = backend_to_media_player.iter().find(|v| v.1 == sender); if let Some(id) = id { let id = id.0; - let callbacks = CALLBACKS.lock().unwrap(); - let callbacks = callbacks.get(&id).unwrap(); - if let Some(callback) = callbacks.utterance_end { + let mut callbacks = CALLBACKS.lock().unwrap(); + let callbacks = callbacks.get_mut(&id).unwrap(); + if let Some(callback) = callbacks.utterance_end.as_mut() { let last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); if let Some(utterance_id) = last_spoken_utterance.get(&id) { callback(utterance_id.clone()); @@ -103,20 +103,20 @@ impl WinRT { let id = backend_to_playback_list.iter().find(|v| v.1 == sender); if let Some(id) = id { let id = id.0; - let callbacks = CALLBACKS.lock().unwrap(); - let callbacks = callbacks.get(&id).unwrap(); + let mut callbacks = CALLBACKS.lock().unwrap(); + let callbacks = callbacks.get_mut(&id).unwrap(); let old_item = args.old_item()?; if !old_item.is_null() { - if let Some(callback) = callbacks.utterance_end { + if let Some(callback) = callbacks.utterance_end.as_mut() { callback(UtteranceId::WinRT(old_item)); } } let new_item = args.new_item()?; if !new_item.is_null() { - let utterance_id = UtteranceId::WinRT(new_item); let mut last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); + let utterance_id = UtteranceId::WinRT(new_item); last_spoken_utterance.insert(*id, utterance_id.clone()); - if let Some(callback) = callbacks.utterance_begin { + if let Some(callback) = callbacks.utterance_begin.as_mut() { callback(utterance_id); } } diff --git a/src/lib.rs b/src/lib.rs index 5560ed1..853a69c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -137,10 +137,14 @@ pub trait Backend { #[derive(Default)] struct Callbacks { - utterance_begin: Option, - utterance_end: Option, + utterance_begin: Option>, + utterance_end: Option>, } +unsafe impl Send for Callbacks {} + +unsafe impl Sync for Callbacks {} + lazy_static! { static ref CALLBACKS: Mutex> = { let m: HashMap = HashMap::new(); @@ -436,7 +440,10 @@ impl TTS { /** * Called when this speech synthesizer begins speaking an utterance. */ - pub fn on_utterance_begin(&self, callback: Option) -> Result<(), Error> { + pub fn on_utterance_begin( + &self, + callback: Option>, + ) -> Result<(), Error> { let Features { utterance_callbacks, .. @@ -455,7 +462,10 @@ impl TTS { /** * Called when this speech synthesizer finishes speaking an utterance. */ - pub fn on_utterance_end(&self, callback: Option) -> Result<(), Error> { + pub fn on_utterance_end( + &self, + callback: Option>, + ) -> Result<(), Error> { let Features { utterance_callbacks, .. From 589c613bbee9520e5a5e8b361378cb46e6bd352b Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 25 Sep 2020 11:12:44 -0500 Subject: [PATCH 16/17] Implement FnMut callbacks on AV Foundation, and fix warnings. --- src/backends/av_foundation.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index e097b1e..84c8824 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -33,15 +33,15 @@ impl AvFoundation { extern "C" fn speech_synthesizer_did_start_speech_utterance( this: &Object, _: Sel, - synth: *const Object, + _synth: *const Object, utterance: id, ) { unsafe { let backend_id: u64 = *this.get_ivar("backend_id"); let backend_id = BackendId::AvFoundation(backend_id); - let callbacks = CALLBACKS.lock().unwrap(); - let callbacks = callbacks.get(&backend_id).unwrap(); - if let Some(callback) = callbacks.utterance_begin { + let mut callbacks = CALLBACKS.lock().unwrap(); + let callbacks = callbacks.get_mut(&backend_id).unwrap(); + if let Some(callback) = callbacks.utterance_begin.as_mut() { let utterance_id = UtteranceId::AvFoundation(utterance); callback(utterance_id); } @@ -51,15 +51,15 @@ impl AvFoundation { extern "C" fn speech_synthesizer_did_finish_speech_utterance( this: &Object, _: Sel, - synth: *const Object, + _synth: *const Object, utterance: id, ) { unsafe { let backend_id: u64 = *this.get_ivar("backend_id"); let backend_id = BackendId::AvFoundation(backend_id); - let callbacks = CALLBACKS.lock().unwrap(); - let callbacks = callbacks.get(&backend_id).unwrap(); - if let Some(callback) = callbacks.utterance_end { + let mut callbacks = CALLBACKS.lock().unwrap(); + let callbacks = callbacks.get_mut(&backend_id).unwrap(); + if let Some(callback) = callbacks.utterance_end.as_mut() { let utterance_id = UtteranceId::AvFoundation(utterance); callback(utterance_id); } From ace5d2fd1fe3ec1f6006ac05f974b4df16cabbb9 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 25 Sep 2020 11:33:49 -0500 Subject: [PATCH 17/17] Make compatible with newly-released speech-dispatcher-rs. --- src/backends/speech_dispatcher.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 8ad6020..b9001a7 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -25,7 +25,7 @@ impl SpeechDispatcher { let sd = SpeechDispatcher(connection); let mut speaking = SPEAKING.lock().unwrap(); speaking.insert(sd.0.client_id(), false); - sd.0.on_begin(Some(|msg_id, client_id| { + sd.0.on_begin(Some(Box::new(|msg_id, client_id| { let mut speaking = SPEAKING.lock().unwrap(); speaking.insert(client_id, true); let mut callbacks = CALLBACKS.lock().unwrap(); @@ -35,8 +35,8 @@ impl SpeechDispatcher { if let Some(f) = cb.utterance_begin.as_mut() { f(utterance_id); } - })); - sd.0.on_end(Some(|msg_id, client_id| { + }))); + sd.0.on_end(Some(Box::new(|msg_id, client_id| { let mut speaking = SPEAKING.lock().unwrap(); speaking.insert(client_id, false); let mut callbacks = CALLBACKS.lock().unwrap(); @@ -46,19 +46,19 @@ impl SpeechDispatcher { if let Some(f) = cb.utterance_end.as_mut() { f(utterance_id); } - })); - sd.0.on_cancel(Some(|_msg_id, client_id| { + }))); + sd.0.on_cancel(Some(Box::new(|_msg_id, client_id| { let mut speaking = SPEAKING.lock().unwrap(); speaking.insert(client_id, false); - })); - sd.0.on_pause(Some(|_msg_id, client_id| { + }))); + sd.0.on_pause(Some(Box::new(|_msg_id, client_id| { let mut speaking = SPEAKING.lock().unwrap(); speaking.insert(client_id, false); - })); - sd.0.on_resume(Some(|_msg_id, client_id| { + }))); + sd.0.on_resume(Some(Box::new(|_msg_id, client_id| { let mut speaking = SPEAKING.lock().unwrap(); speaking.insert(client_id, true); - })); + }))); sd } }