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); + } + } }