From 4fadad9d36335f2c0d9eb31f8fead86b623755ce Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Sun, 30 Dec 2018 17:13:48 +0000 Subject: [PATCH] Add Web backend and refactor API. --- Cargo.toml | 5 ++ examples/hello_world.rs | 47 ++++++++------- src/backends/mod.rs | 6 ++ src/backends/speech_dispatcher.rs | 31 ++++++---- src/backends/web.rs | 85 ++++++++++++++++++++++++++ src/lib.rs | 99 ++++++++++++++++++++----------- 6 files changed, 202 insertions(+), 71 deletions(-) create mode 100644 src/backends/web.rs diff --git a/Cargo.toml b/Cargo.toml index 9966b07..2635b52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT" edition = "2018" [dependencies] +failure = "0.1" log = "0.4" [dev-dependencies] @@ -15,3 +16,7 @@ env_logger = "0.6" [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.2" + +[target.wasm32-unknown-unknown.dependencies] +wasm-bindgen = "0.2" +web-sys = { version = "0.3", features = ["SpeechSynthesis", "SpeechSynthesisUtterance", "Window", ] } diff --git a/examples/hello_world.rs b/examples/hello_world.rs index fb77552..27794b9 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -2,28 +2,29 @@ use std::u8; use tts::TTS; -fn main() { +fn main() -> Result<(), std::io::Error> { env_logger::init(); - let tts: TTS = Default::default(); - tts.speak("Hello, world.", false); - let original_rate = tts.get_rate(); - tts.speak(format!("Current rate: {}", original_rate), false); - tts.set_rate(u8::MAX); - tts.speak("This is very fast.", false); - tts.set_rate(0); - tts.speak("This is very slow.", false); - tts.set_rate(original_rate); - let original_pitch = tts.get_pitch(); - tts.set_pitch(u8::MAX); - tts.speak("This is high-pitch.", false); - tts.set_pitch(0); - tts.speak("This is low pitch.", false); - tts.set_pitch(original_pitch); - let original_volume = tts.get_volume(); - tts.set_volume(u8::MAX); - tts.speak("This is loud!", false); - tts.set_volume(0); - tts.speak("This is quiet.", false); - tts.set_volume(original_volume); - tts.speak("Goodbye.", false); + let mut tts = TTS::default()?; + tts.speak("Hello, world.", false)?; + let original_rate = tts.get_rate()?; + tts.speak(format!("Current rate: {}", original_rate), false)?; + tts.set_rate(u8::MAX)?; + tts.speak("This is very fast.", false)?; + tts.set_rate(0)?; + tts.speak("This is very slow.", false)?; + tts.set_rate(original_rate)?; + let original_pitch = tts.get_pitch()?; + tts.set_pitch(u8::MAX)?; + tts.speak("This is high-pitch.", false)?; + tts.set_pitch(0)?; + tts.speak("This is low pitch.", false)?; + tts.set_pitch(original_pitch)?; + let original_volume = tts.get_volume()?; + tts.set_volume(u8::MAX)?; + tts.speak("This is loud!", false)?; + tts.set_volume(0)?; + tts.speak("This is quiet.", false)?; + tts.set_volume(original_volume)?; + tts.speak("Goodbye.", false)?; + Ok(()) } diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 6500e27..9fd026a 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -1,5 +1,11 @@ #[cfg(target_os = "linux")] mod speech_dispatcher; +#[cfg(target_arch = "wasm32")] +mod web; + #[cfg(target_os = "linux")] pub use self::speech_dispatcher::*; + +#[cfg(target_arch = "wasm32")] +pub use self::web::*; diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 18087ef..6b400e6 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -5,7 +5,7 @@ use std::u8; use log::{info, trace}; use speech_dispatcher::*; -use crate::Backend; +use crate::{Backend, Error}; pub struct SpeechDispatcher(Connection); @@ -30,40 +30,45 @@ fn i32_to_u8(v: i32) -> u8 { } impl Backend for SpeechDispatcher { - fn speak(&self, text: &str, interrupt: bool) { + fn speak(&self, text: &str, interrupt: bool) -> Result<(), Error> { trace!("speak({}, {})", text, interrupt); if interrupt { - self.0.cancel(); + self.stop()?; } self.0.say(Priority::Important, text); + Ok(()) } - fn stop(&self) { + fn stop(&self) -> Result<(), Error> { trace!("stop()"); self.0.cancel(); + Ok(()) } - fn get_rate(&self) -> u8 { - i32_to_u8(self.0.get_voice_rate()) + fn get_rate(&self) -> Result { + Ok(i32_to_u8(self.0.get_voice_rate())) } - fn set_rate(&self, rate: u8) { + fn set_rate(&mut self, rate: u8) -> Result<(), Error> { self.0.set_voice_rate(u8_to_i32(rate)); + Ok(()) } - fn get_pitch(&self) -> u8 { - i32_to_u8(self.0.get_voice_pitch()) + fn get_pitch(&self) -> Result { + Ok(i32_to_u8(self.0.get_voice_pitch())) } - fn set_pitch(&self, pitch: u8) { + fn set_pitch(&mut self, pitch: u8) -> Result<(), Error> { self.0.set_voice_pitch(u8_to_i32(pitch)); + Ok(()) } - fn get_volume(&self) -> u8 { - i32_to_u8(self.0.get_volume()) + fn get_volume(&self) -> Result { + Ok(i32_to_u8(self.0.get_volume())) } - fn set_volume(&self, volume: u8) { + fn set_volume(&mut self, volume: u8) -> Result<(), Error> { self.0.set_volume(u8_to_i32(volume)); + Ok(()) } } diff --git a/src/backends/web.rs b/src/backends/web.rs new file mode 100644 index 0000000..6167196 --- /dev/null +++ b/src/backends/web.rs @@ -0,0 +1,85 @@ +#[cfg(target_arch = "wasm32")] + +use std::u8; + +use log::{info, trace}; +use web_sys::SpeechSynthesisUtterance; + +use crate::{Backend, Error}; + +pub struct Web { + rate: u8, + pitch: u8, + volume: u8, +} + +impl Web { + pub fn new() -> Result { + info!("Initializing Web backend"); + Ok(Web { + rate: 25, + pitch: 127, + volume: u8::MAX, + }) + } +} + +impl Backend for Web { + fn speak(&self, text: &str, interrupt: bool) -> Result<(), Error> { + trace!("speak({}, {})", text, interrupt); + let utterance = SpeechSynthesisUtterance::new_with_text(text).unwrap(); + let mut rate: f32 = self.rate as f32; + rate = rate / u8::MAX as f32 * 10.; + utterance.set_rate(rate); + let mut pitch: f32 = self.pitch as f32; + pitch = pitch / u8::MAX as f32 * 2.; + utterance.set_pitch(pitch); + let mut volume: f32 = self.volume as f32; + volume = volume / u8::MAX as f32 * 1.; + utterance.set_volume(volume); + if interrupt { + self.stop()?; + } + if let Some(window) = web_sys::window() { + let speech_synthesis = window.speech_synthesis().unwrap(); + speech_synthesis.speak(&utterance); + } + Ok(()) + } + + fn stop(&self) -> Result<(), Error> { + trace!("stop()"); + if let Some(window) = web_sys::window() { + let speech_synthesis = window.speech_synthesis().unwrap(); + speech_synthesis.cancel(); + } + Ok(()) + } + + fn get_rate(&self) -> Result { + Ok(self.rate) + } + + fn set_rate(&mut self, rate: u8) -> Result<(), Error> { + self.rate = rate; + Ok(()) + } + + fn get_pitch(&self) -> Result { + Ok(self.pitch) + } + + fn set_pitch(&mut self, pitch: u8) -> Result<(), Error> { + self.pitch = pitch; + Ok(()) + } + + fn get_volume(&self) -> Result { + Ok(self.volume) + } + + fn set_volume(&mut self, volume: u8) -> Result<(), Error> { + self.volume = volume; + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 86c8c8e..a9d897f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,23 +4,46 @@ */ use std::boxed::Box; +use std::convert; +use std::fmt; +use std::io; + +use failure::Fail; mod backends; pub enum Backends { #[cfg(target_os = "linux")] SpeechDispatcher, + #[cfg(target_arch = "wasm32")] + Web, +} + +#[derive(Debug, Fail)] +pub struct Error(String); + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "{}", self.0)?; + Ok(()) + } +} + +impl convert::From for io::Error { + fn from(e: Error) -> io::Error { + io::Error::new(io::ErrorKind::Other, e.0) + } } trait Backend { - fn speak(&self, text: &str, interrupt: bool); - fn stop(&self); - fn get_rate(&self) -> u8; - fn set_rate(&self, rate: u8); - fn get_pitch(&self) -> u8; - fn set_pitch(&self, pitch: u8); - fn get_volume(&self) -> u8; - fn set_volume(&self, volume: u8); + fn speak(&self, text: &str, interrupt: bool) -> Result<(), Error>; + fn stop(&self) -> Result<(), Error>; + fn get_rate(&self) -> Result; + fn set_rate(&mut self, rate: u8) -> Result<(), Error>; + fn get_pitch(&self) -> Result; + fn set_pitch(&mut self, pitch: u8) -> Result<(), Error>; + fn get_volume(&self) -> Result; + fn set_volume(&mut self, volume: u8) -> Result<(), Error>; } pub struct TTS(Box); @@ -30,78 +53,84 @@ impl TTS { /** * Create a new `TTS` instance with the specified backend. */ - pub fn new(backend: Backends) -> TTS { + pub fn new(backend: Backends) -> Result { match backend { #[cfg(target_os = "linux")] - Backends::SpeechDispatcher => TTS(Box::new(backends::SpeechDispatcher::new())), + Backends::SpeechDispatcher => Ok(TTS(Box::new(backends::SpeechDispatcher::new()))), + #[cfg(target_arch = "wasm32")] + Backends::Web => { + let tts = backends::Web::new()?; + Ok(TTS(Box::new(tts))) + }, } } + pub fn default() -> Result { + #[cfg(target_os = "linux")] + let tts = TTS::new(Backends::SpeechDispatcher); + #[cfg(target_arch = "wasm32")] + let tts = TTS::new(Backends::Web); + tts + } + /** * Speaks the specified text, optionally interrupting current speech. */ - pub fn speak>(&self, text: S, interrupt: bool) -> &Self { - self.0.speak(text.into().as_str(), interrupt); - self + pub fn speak>(&self, text: S, interrupt: bool) -> Result<&Self, Error> { + self.0.speak(text.into().as_str(), interrupt)?; + Ok(self) } /** * Stops current speech. */ - pub fn stop(&self) -> &Self { - self.0.stop(); - self + pub fn stop(&self) -> Result<&Self, Error> { + self.0.stop()?; + Ok(self) } /** * Gets the current speech rate. */ - pub fn get_rate(&self) -> u8 { + pub fn get_rate(&self) -> Result { self.0.get_rate() } /** * Sets the desired speech rate. */ - pub fn set_rate(&self, rate: u8) -> &Self { - self.0.set_rate(rate); - self + pub fn set_rate(&mut self, rate: u8) -> Result<&Self, Error> { + self.0.set_rate(rate)?; + Ok(self) } /** * Gets the current speech pitch. */ - pub fn get_pitch(&self) -> u8 { + pub fn get_pitch(&self) -> Result { self.0.get_pitch() } /** * Sets the desired speech pitch. */ - pub fn set_pitch(&self, pitch: u8) -> &Self { - self.0.set_pitch(pitch); - self + pub fn set_pitch(&mut self, pitch: u8) -> Result<&Self, Error> { + self.0.set_pitch(pitch)?; + Ok(self) } /** * Gets the current speech volume. */ - pub fn get_volume(&self) -> u8 { + pub fn get_volume(&self) -> Result { self.0.get_volume() } /** * Sets the desired speech volume. */ - pub fn set_volume(&self, volume: u8) -> &Self { - self.0.set_volume(volume); - self - } -} - -impl Default for TTS { - fn default() -> TTS { - #[cfg(target_os = "linux")] - TTS::new(Backends::SpeechDispatcher) + pub fn set_volume(&mut self, volume: u8) -> Result<&Self, Error> { + self.0.set_volume(volume)?; + Ok(self) } }