diff --git a/Cargo.toml b/Cargo.toml index 2c1e96f..5086cd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,12 @@ edition = "2018" [dependencies] env_logger = "0.7" -failure = "0.1" log = "0.4" +thiserror = "1" [target.'cfg(windows)'.dependencies] tolk = "0.2" +winrt = { git = "https://github.com/microsoft/winrt-rs", rev = "ed46a71f506c343b3eb4fa6c15a4d9db1397ebcf" } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.4" diff --git a/examples/hello_world.rs b/examples/hello_world.rs index e61cd3b..0399e1f 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -3,7 +3,7 @@ use std::u8; use tts::*; -fn main() -> Result<(), std::io::Error> { +fn main() -> Result<(), Error> { env_logger::init(); let mut tts = TTS::default()?; tts.speak("Hello, world.", false)?; diff --git a/src/backends/mod.rs b/src/backends/mod.rs index ae26aec..c8b065a 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -4,6 +4,9 @@ mod speech_dispatcher; #[cfg(windows)] mod tolk; +#[cfg(windows)] +pub(crate) mod winrt; + #[cfg(target_arch = "wasm32")] mod web; diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs new file mode 100644 index 0000000..48f8cfe --- /dev/null +++ b/src/backends/winrt.rs @@ -0,0 +1,97 @@ +#[cfg(windows)] +use winrt::*; + +import!( + dependencies + os + modules + "windows.media.core" + "windows.media.playback" + "windows.media.speechsynthesis" +); + +use log::{info, trace}; +use windows::media::core::MediaSource; +use windows::media::playback::{MediaPlaybackItem, MediaPlaybackList, MediaPlayer}; +use windows::media::speech_synthesis::SpeechSynthesizer; + +use crate::{Backend, Error, Features}; + +impl From for Error { + fn from(e: winrt::Error) -> Self { + Error::WinRT(e) + } +} + +pub struct WinRT { + synth: SpeechSynthesizer, + player: MediaPlayer, + playback_list: MediaPlaybackList, +} + +impl WinRT { + pub fn new() -> std::result::Result { + info!("Initializing WinRT backend"); + let player = MediaPlayer::new()?; + player.set_auto_play(true)?; + let playback_list = MediaPlaybackList::new()?; + player.set_source(&playback_list)?; + Ok(Self { + synth: SpeechSynthesizer::new()?, + player: player, + playback_list: playback_list, + }) + } +} + +impl Backend for WinRT { + fn supported_features(&self) -> Features { + Features { + stop: true, + rate: false, + pitch: false, + volume: false, + } + } + + fn speak(&self, text: &str, interrupt: bool) -> std::result::Result<(), Error> { + trace!("speak({}, {})", text, interrupt); + let stream = self.synth.synthesize_text_to_stream_async(text)?.get()?; + let content_type = stream.content_type()?; + let source = MediaSource::create_from_stream(stream, content_type)?; + let item = MediaPlaybackItem::create(source)?; + self.playback_list.items()?.append(item)?; + Ok(()) + } + + fn stop(&self) -> std::result::Result<(), Error> { + trace!("stop()"); + self.player.close()?; + self.playback_list.items()?.clear()?; + Ok(()) + } + + fn get_rate(&self) -> std::result::Result { + unimplemented!(); + } + + fn set_rate(&mut self, _rate: u8) -> std::result::Result<(), Error> { + unimplemented!(); + } + + fn get_pitch(&self) -> std::result::Result { + unimplemented!(); + } + + fn set_pitch(&mut self, _pitch: u8) -> std::result::Result<(), Error> { + unimplemented!(); + } + + fn get_volume(&self) -> std::result::Result { + unimplemented!(); + } + + fn set_volume(&mut self, _volume: u8) -> std::result::Result<(), Error> { + unimplemented!(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 8fcdba3..10648ff 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: - * * [Speech Dispatcher](https://freebsoft.org/speechd) (Linux) + * * [Speech Dispatcher](https://freebsoft.org/speechd) (Linux) + * * Windows screen readers and SAPI via [Tolk](https://github.com/dkager/tolk/) * * WebAssembly */ use std::boxed::Box; -use std::convert; -use std::fmt; -use std::io; -use failure::Fail; +use thiserror::Error; mod backends; @@ -21,22 +19,8 @@ pub enum Backends { Web, #[cfg(windows)] Tolk, -} - -#[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) - } + #[cfg(windows)] + WinRT, } pub struct Features { @@ -46,6 +30,17 @@ pub struct Features { pub volume: bool, } +#[derive(Debug, Error)] +pub enum Error { + #[error("IO error: {0}")] + IO(#[from] std::io::Error), + #[cfg(windows)] + #[error("WinRT error")] + WinRT(winrt::Error), + #[error("Unsupported feature")] + UnsupportedFeature, +} + pub trait Backend { fn supported_features(&self) -> Features; fn speak(&self, text: &str, interrupt: bool) -> Result<(), Error>; @@ -82,6 +77,11 @@ impl TTS { let tts = backends::Tolk::new(); Ok(TTS(Box::new(tts))) } + #[cfg(windows)] + Backends::WinRT => { + let tts = backends::winrt::WinRT::new()?; + Ok(TTS(Box::new(tts))) + } } } @@ -89,7 +89,14 @@ impl TTS { #[cfg(target_os = "linux")] let tts = TTS::new(Backends::SpeechDispatcher); #[cfg(windows)] - let tts = TTS::new(Backends::Tolk); + let tts = { + let tolk = tolk::Tolk::new(); + if tolk.detect_screen_reader().is_some() { + TTS::new(Backends::Tolk) + } else { + TTS::new(Backends::WinRT) + } + }; #[cfg(target_arch = "wasm32")] let tts = TTS::new(Backends::Web); tts @@ -119,7 +126,7 @@ impl TTS { self.0.stop()?; Ok(self) } else { - Err(Error("Feature not supported".to_string())) + Err(Error::UnsupportedFeature) } } @@ -131,7 +138,7 @@ impl TTS { if rate { self.0.get_rate() } else { - Err(Error("Feature not supported".to_string())) + Err(Error::UnsupportedFeature) } } @@ -146,7 +153,7 @@ impl TTS { self.0.set_rate(rate)?; Ok(self) } else { - Err(Error("Unsupported feature".to_string())) + Err(Error::UnsupportedFeature) } } @@ -158,7 +165,7 @@ impl TTS { if pitch { self.0.get_pitch() } else { - Err(Error("Feature not supported".to_string())) + Err(Error::UnsupportedFeature) } } @@ -174,7 +181,7 @@ impl TTS { self.0.set_pitch(pitch)?; Ok(self) } else { - Err(Error("Unsupported feature".to_string())) + Err(Error::UnsupportedFeature) } } @@ -186,7 +193,7 @@ impl TTS { if volume { self.0.get_volume() } else { - Err(Error("Unsupported feature".to_string())) + Err(Error::UnsupportedFeature) } } @@ -202,7 +209,7 @@ impl TTS { self.0.set_volume(volume)?; Ok(self) } else { - Err(Error("Unsupported feature".to_string())) + Err(Error::UnsupportedFeature) } } }