Implement utterance begin/end callback framework, and set up for Speech-Dispatcher.

This commit is contained in:
Nolan Darilek 2020-09-23 10:12:51 -05:00
parent f5f11b7cdf
commit 61522610cd
4 changed files with 127 additions and 9 deletions

View File

@ -12,6 +12,18 @@ use tts::*;
fn main() -> Result<(), Error> { fn main() -> Result<(), Error> {
env_logger::init(); env_logger::init();
let mut tts = TTS::default()?; 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)?; tts.speak("Hello, world.", false)?;
let Features { rate, .. } = tts.supported_features(); let Features { rate, .. } = tts.supported_features();
if rate { if rate {

View File

@ -17,7 +17,7 @@ mod appkit;
mod av_foundation; mod av_foundation;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub use self::speech_dispatcher::*; pub(crate) use self::speech_dispatcher::*;
#[cfg(windows)] #[cfg(windows)]
pub use self::tolk::*; pub use self::tolk::*;

View File

@ -1,14 +1,15 @@
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryInto;
use std::sync::Mutex; use std::sync::Mutex;
use lazy_static::*; use lazy_static::*;
use log::{info, trace}; use log::{info, trace};
use speech_dispatcher::*; 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! { lazy_static! {
static ref SPEAKING: Mutex<HashMap<u64, bool>> = { static ref SPEAKING: Mutex<HashMap<u64, bool>> = {
@ -18,19 +19,33 @@ lazy_static! {
} }
impl SpeechDispatcher { impl SpeechDispatcher {
pub fn new() -> Self { pub(crate) fn new() -> Self {
info!("Initializing SpeechDispatcher backend"); 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 sd = SpeechDispatcher(connection);
let mut speaking = SPEAKING.lock().unwrap(); let mut speaking = SPEAKING.lock().unwrap();
speaking.insert(sd.0.client_id(), false); 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(); let mut speaking = SPEAKING.lock().unwrap();
speaking.insert(client_id, true); 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(); let mut speaking = SPEAKING.lock().unwrap();
speaking.insert(client_id, false); 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| { sd.0.on_cancel(Some(|_msg_id, client_id| {
let mut speaking = SPEAKING.lock().unwrap(); let mut speaking = SPEAKING.lock().unwrap();
@ -49,6 +64,10 @@ impl SpeechDispatcher {
} }
impl Backend for SpeechDispatcher { impl Backend for SpeechDispatcher {
fn id(&self) -> Option<BackendId> {
Some(BackendId::SpeechDispatcher(self.0.client_id()))
}
fn supported_features(&self) -> Features { fn supported_features(&self) -> Features {
Features { Features {
stop: true, stop: true,
@ -56,6 +75,7 @@ impl Backend for SpeechDispatcher {
pitch: true, pitch: true,
volume: true, volume: true,
is_speaking: true, is_speaking: true,
utterance_callbacks: true,
} }
} }
@ -73,7 +93,7 @@ impl Backend for SpeechDispatcher {
self.0.set_punctuation(Punctuation::None); self.0.set_punctuation(Punctuation::None);
} }
if let Some(id) = id { if let Some(id) = id {
Ok(Some(UtteranceId::SpeechDispatcher(id))) Ok(Some(UtteranceId::SpeechDispatcher(id.try_into().unwrap())))
} else { } else {
Err(Error::NoneError) Err(Error::NoneError)
} }

View File

@ -12,11 +12,14 @@
*/ */
use std::boxed::Box; use std::boxed::Box;
use std::collections::HashMap;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use std::ffi::CStr; use std::ffi::CStr;
use std::sync::Mutex;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use cocoa_foundation::base::id; use cocoa_foundation::base::id;
use lazy_static::lazy_static;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use libc::c_char; use libc::c_char;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@ -43,10 +46,20 @@ pub enum Backends {
AvFoundation, 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)] #[derive(Clone, Debug, PartialEq)]
pub enum UtteranceId { pub enum UtteranceId {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
SpeechDispatcher(i32), SpeechDispatcher(u64),
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
Web(u64), Web(u64),
#[cfg(windows)] #[cfg(windows)]
@ -59,6 +72,7 @@ pub struct Features {
pub pitch: bool, pub pitch: bool,
pub volume: bool, pub volume: bool,
pub is_speaking: bool, pub is_speaking: bool,
pub utterance_callbacks: bool,
} }
impl Default for Features { impl Default for Features {
@ -69,6 +83,7 @@ impl Default for Features {
pitch: false, pitch: false,
volume: false, volume: false,
is_speaking: false, is_speaking: false,
utterance_callbacks: false,
} }
} }
} }
@ -92,6 +107,7 @@ pub enum Error {
} }
pub trait Backend { pub trait Backend {
fn id(&self) -> Option<BackendId>;
fn supported_features(&self) -> Features; fn supported_features(&self) -> Features;
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error>; fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error>;
fn stop(&mut self) -> Result<(), Error>; fn stop(&mut self) -> Result<(), Error>;
@ -113,6 +129,19 @@ pub trait Backend {
fn is_speaking(&self) -> Result<bool, Error>; fn is_speaking(&self) -> Result<bool, Error>;
} }
#[derive(Default)]
struct Callbacks {
utterance_begin: Option<fn(UtteranceId)>,
utterance_end: Option<fn(UtteranceId)>,
}
lazy_static! {
static ref CALLBACKS: Mutex<HashMap<BackendId, Callbacks>> = {
let m: HashMap<BackendId, Callbacks> = HashMap::new();
Mutex::new(m)
};
}
pub struct TTS(Box<dyn Backend>); pub struct TTS(Box<dyn Backend>);
unsafe impl std::marker::Send for TTS {} unsafe impl std::marker::Send for TTS {}
@ -124,7 +153,7 @@ 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<TTS, Error> { pub fn new(backend: Backends) -> Result<TTS, Error> {
match backend { let backend = match backend {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
Backends::SpeechDispatcher => Ok(TTS(Box::new(backends::SpeechDispatcher::new()))), Backends::SpeechDispatcher => Ok(TTS(Box::new(backends::SpeechDispatcher::new()))),
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
@ -150,6 +179,16 @@ impl TTS {
Backends::AppKit => Ok(TTS(Box::new(backends::AppKit::new()))), Backends::AppKit => Ok(TTS(Box::new(backends::AppKit::new()))),
#[cfg(any(target_os = "macos", target_os = "ios"))] #[cfg(any(target_os = "macos", target_os = "ios"))]
Backends::AvFoundation => Ok(TTS(Box::new(backends::AvFoundation::new()))), 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) Err(Error::UnsupportedFeature)
} }
} }
/**
* Called when this speech synthesizer begins speaking an utterance.
*/
pub fn on_utterance_begin(&self, callback: Option<fn(UtteranceId)>) -> 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<fn(UtteranceId)>) -> 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);
}
}
} }