diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 87f292d..06b847b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,27 +61,13 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} args: --all-features --target wasm32-unknown-unknown - - publish_winrt_bindings: - name: Publish winrt_bindings - runs-on: windows-latest - needs: [check] - env: - CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} - steps: - - uses: actions/checkout@v2 - - uses: Swatinem/rust-cache@v1 - - uses: actions-rs/toolchain@v1 + - uses: actions-rs/install@v0.1 with: - target: wasm32-unknown-unknown - profile: minimal - toolchain: stable - components: rustfmt, clippy - override: true - - run: | - cargo login $CARGO_TOKEN - cd winrt_bindings - cargo publish || true + crate: cargo-make + - uses: actions-rs/cargo@v1 + with: + command: make + args: build-web-example publish: name: Publish diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 50d9534..ff68b02 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -82,3 +82,24 @@ jobs: with: command: apk args: build + + check_web_example: + name: Check Web Example + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 + - uses: actions-rs/toolchain@v1 + with: + target: wasm32-unknown-unknown + profile: minimal + toolchain: stable + components: rustfmt, clippy + override: true + - uses: actions-rs/install@v0.1 + with: + crate: cargo-make + - uses: actions-rs/cargo@v1 + with: + command: make + args: build-web-example diff --git a/.gitignore b/.gitignore index fa8d85a..ea81e28 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ Cargo.lock target +*.dll \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index d58d3b5..97216d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.14.0" +version = "0.17.3" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -29,9 +29,11 @@ env_logger = "0.8" cbindgen = {version = "0.18.0", optional = true} [target.'cfg(windows)'.dependencies] -tolk = { version = "0.3", optional = true } -windows = "0.2" -tts_winrt_bindings = { version = "0.3", path="winrt_bindings" } +tolk = { version = "0.5", optional = true } +windows = "0.9" + +[target.'cfg(windows)'.build-dependencies] +windows = "0.9" [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.7" @@ -39,12 +41,12 @@ speech-dispatcher = "0.7" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] cocoa-foundation = "0.1" libc = "0.2" -objc = "0.2" +objc = { version = "0.2", features = ["exception"] } [target.wasm32-unknown-unknown.dependencies] wasm-bindgen = "0.2" web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "SpeechSynthesisErrorCode", "SpeechSynthesisErrorEvent", "SpeechSynthesisEvent", "SpeechSynthesisUtterance", "Window", ] } [target.'cfg(target_os="android")'.dependencies] -jni = "0.18" -ndk-glue = "0.2" +jni = "0.19" +ndk-glue = "0.3" diff --git a/Makefile.toml b/Makefile.toml index 241094f..92df00d 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -18,3 +18,21 @@ script = [ [tasks.log-android] command = "adb" args = ["logcat", "RustStdoutStderr:D", "*:S"] + +[tasks.install-trunk] +install_crate = { crate_name = "trunk", binary = "trunk", test_arg = "--help" } + +[tasks.install-wasm-bindgen-cli] +install_crate = { crate_name = "wasm-bindgen-cli", binary = "wasm-bindgen", test_arg = "--help" } + +[tasks.build-web-example] +dependencies = ["install-trunk", "install-wasm-bindgen-cli"] +cwd = "examples/web" +command = "trunk" +args = ["build"] + +[tasks.run-web-example] +dependencies = ["install-trunk", "install-wasm-bindgen-cli"] +cwd = "examples/web" +command = "trunk" +args = ["serve"] diff --git a/build.rs b/build.rs index c92ffc7..d0f4906 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,14 @@ fn main() { + #[cfg(windows)] + if std::env::var("TARGET").unwrap().contains("windows") { + windows::build!( + Windows::Foundation::{EventRegistrationToken, IAsyncOperation, TypedEventHandler}, + Windows::Media::Core::MediaSource, + Windows::Media::Playback::{MediaPlaybackSession, MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory}, + Windows::Media::SpeechSynthesis::{SpeechSynthesisStream, SpeechSynthesizer, SpeechSynthesizerOptions}, + Windows::Storage::Streams::IRandomAccessStream, + ); + } if std::env::var("TARGET").unwrap().contains("-apple") { println!("cargo:rustc-link-lib=framework=AVFoundation"); if !std::env::var("CARGO_CFG_TARGET_OS") diff --git a/examples/99bottles.rs b/examples/99bottles.rs index 9371b82..ff3f130 100644 --- a/examples/99bottles.rs +++ b/examples/99bottles.rs @@ -12,7 +12,7 @@ use tts::*; fn main() -> Result<(), Error> { env_logger::init(); - let mut tts = TTS::default()?; + let mut tts = Tts::default()?; let mut bottles = 99; while bottles > 0 { tts.speak(format!("{} bottles of beer on the wall,", bottles), false)?; diff --git a/examples/android/src/lib.rs b/examples/android/src/lib.rs index 6bf79d6..a8e4677 100644 --- a/examples/android/src/lib.rs +++ b/examples/android/src/lib.rs @@ -4,7 +4,7 @@ use tts::*; // Without it, the `TTS` instance gets dropped before callbacks can run. #[allow(unreachable_code)] fn run() -> Result<(), Error> { - let mut tts = TTS::default()?; + let mut tts = Tts::default()?; let Features { utterance_callbacks, .. diff --git a/examples/hello_world.rs b/examples/hello_world.rs index f3fa7a8..1db3ed3 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -11,7 +11,12 @@ use tts::*; fn main() -> Result<(), Error> { env_logger::init(); - let mut tts = TTS::default()?; + let mut tts = Tts::default()?; + if Tts::screen_reader_available() { + println!("A screen reader is available on this platform."); + } else { + println!("No screen reader is available on this platform."); + } let Features { utterance_callbacks, .. diff --git a/examples/latency.rs b/examples/latency.rs index 819becc..d6d998e 100644 --- a/examples/latency.rs +++ b/examples/latency.rs @@ -4,7 +4,7 @@ use tts::*; fn main() -> Result<(), Error> { env_logger::init(); - let mut tts = TTS::default()?; + let mut tts = Tts::default()?; println!("Press Enter and wait for speech."); loop { let mut _input = String::new(); diff --git a/examples/ramble.rs b/examples/ramble.rs index 85bde46..e327374 100644 --- a/examples/ramble.rs +++ b/examples/ramble.rs @@ -4,7 +4,7 @@ use tts::*; fn main() -> Result<(), Error> { env_logger::init(); - let mut tts = TTS::default()?; + let mut tts = Tts::default()?; let mut phrase = 1; loop { tts.speak(format!("Phrase {}", phrase), false)?; diff --git a/examples/web/.cargo/config b/examples/web/.cargo/config new file mode 100644 index 0000000..435ed75 --- /dev/null +++ b/examples/web/.cargo/config @@ -0,0 +1,2 @@ +[build] +target = "wasm32-unknown-unknown" \ No newline at end of file diff --git a/examples/web/.gitignore b/examples/web/.gitignore new file mode 100644 index 0000000..53c37a1 --- /dev/null +++ b/examples/web/.gitignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/examples/web/Cargo.toml b/examples/web/Cargo.toml new file mode 100644 index 0000000..e93d316 --- /dev/null +++ b/examples/web/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "web" +version = "0.1.0" +authors = ["Nolan Darilek "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +seed = "0.8" +tts = { path = "../.." } \ No newline at end of file diff --git a/examples/web/index.html b/examples/web/index.html new file mode 100644 index 0000000..15e486f --- /dev/null +++ b/examples/web/index.html @@ -0,0 +1,12 @@ + + + + + Example + + + +
+ + + \ No newline at end of file diff --git a/examples/web/src/main.rs b/examples/web/src/main.rs new file mode 100644 index 0000000..fb03c38 --- /dev/null +++ b/examples/web/src/main.rs @@ -0,0 +1,111 @@ +#![allow(clippy::wildcard_imports)] +use seed::{prelude::*, *}; + +use tts::Tts; + +#[derive(Clone)] +struct Model { + text: String, + tts: Tts, +} + +#[derive(Clone)] +enum Msg { + TextChanged(String), + RateChanged(String), + PitchChanged(String), + VolumeChanged(String), + Speak, +} + +fn init(_: Url, _: &mut impl Orders) -> Model { + let tts = Tts::default().unwrap(); + Model { + text: Default::default(), + tts, + } +} + +fn update(msg: Msg, model: &mut Model, _: &mut impl Orders) { + use Msg::*; + match msg { + TextChanged(text) => model.text = text, + RateChanged(rate) => { + let rate = rate.parse::().unwrap(); + model.tts.set_rate(rate).unwrap(); + } + PitchChanged(pitch) => { + let pitch = pitch.parse::().unwrap(); + model.tts.set_pitch(pitch).unwrap(); + } + VolumeChanged(volume) => { + let volume = volume.parse::().unwrap(); + model.tts.set_volume(volume).unwrap(); + } + Speak => { + model.tts.speak(&model.text, false).unwrap(); + } + } +} + +fn view(model: &Model) -> Node { + form![ + div![label![ + "Text to speak", + input![ + attrs! { + At::Value => model.text, + At::AutoFocus => AtValue::None, + }, + input_ev(Ev::Input, Msg::TextChanged) + ], + ],], + div![label![ + "Rate", + input![ + attrs! { + At::Type => "number", + At::Value => model.tts.get_rate().unwrap(), + At::Min => model.tts.min_rate(), + At::Max => model.tts.max_rate() + }, + input_ev(Ev::Input, Msg::RateChanged) + ], + ],], + div![label![ + "Pitch", + input![ + attrs! { + At::Type => "number", + At::Value => model.tts.get_pitch().unwrap(), + At::Min => model.tts.min_pitch(), + At::Max => model.tts.max_pitch() + }, + input_ev(Ev::Input, Msg::PitchChanged) + ], + ],], + div![label![ + "Volume", + input![ + attrs! { + At::Type => "number", + At::Value => model.tts.get_volume().unwrap(), + At::Min => model.tts.min_volume(), + At::Max => model.tts.max_volume() + }, + input_ev(Ev::Input, Msg::VolumeChanged) + ], + ],], + button![ + "Speak", + ev(Ev::Click, |e| { + e.prevent_default(); + Msg::Speak + }), + ], + ] +} + +fn main() { + App::start("app", init, update, view); +} diff --git a/src/backends/appkit.rs b/src/backends/appkit.rs index 97f6cb4..2a07450 100644 --- a/src/backends/appkit.rs +++ b/src/backends/appkit.rs @@ -198,7 +198,7 @@ impl Backend for AppKit { fn is_speaking(&self) -> Result { let is_speaking: i8 = unsafe { msg_send![self.0, isSpeaking] }; - Ok(is_speaking == YES) + Ok(is_speaking != NO as i8) } } diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index dbca640..68f95c9 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -2,7 +2,7 @@ #[link(name = "AVFoundation", kind = "framework")] use std::sync::Mutex; -use cocoa_foundation::base::{id, nil}; +use cocoa_foundation::base::{id, nil, NO}; use cocoa_foundation::foundation::NSString; use lazy_static::lazy_static; use log::{info, trace}; @@ -37,16 +37,22 @@ impl AvFoundation { _synth: *const Object, utterance: id, ) { + trace!("speech_synthesizer_did_start_speech_utterance"); unsafe { let backend_id: u64 = *this.get_ivar("backend_id"); let backend_id = BackendId::AvFoundation(backend_id); + trace!("Locking callbacks"); let mut callbacks = CALLBACKS.lock().unwrap(); + trace!("Locked"); let callbacks = callbacks.get_mut(&backend_id).unwrap(); if let Some(callback) = callbacks.utterance_begin.as_mut() { + trace!("Calling utterance_begin"); let utterance_id = UtteranceId::AvFoundation(utterance); callback(utterance_id); + trace!("Called"); } } + trace!("Done speech_synthesizer_did_start_speech_utterance"); } extern "C" fn speech_synthesizer_did_finish_speech_utterance( @@ -55,16 +61,22 @@ impl AvFoundation { _synth: *const Object, utterance: id, ) { + trace!("speech_synthesizer_did_finish_speech_utterance"); unsafe { let backend_id: u64 = *this.get_ivar("backend_id"); let backend_id = BackendId::AvFoundation(backend_id); + trace!("Locking callbacks"); let mut callbacks = CALLBACKS.lock().unwrap(); + trace!("Locked"); let callbacks = callbacks.get_mut(&backend_id).unwrap(); if let Some(callback) = callbacks.utterance_end.as_mut() { + trace!("Calling utterance_end"); let utterance_id = UtteranceId::AvFoundation(utterance); callback(utterance_id); + trace!("Called"); } } + trace!("Done speech_synthesizer_did_finish_speech_utterance"); } extern "C" fn speech_synthesizer_did_cancel_speech_utterance( @@ -73,16 +85,22 @@ impl AvFoundation { _synth: *const Object, utterance: id, ) { + trace!("speech_synthesizer_did_cancel_speech_utterance"); unsafe { let backend_id: u64 = *this.get_ivar("backend_id"); let backend_id = BackendId::AvFoundation(backend_id); + trace!("Locking callbacks"); let mut callbacks = CALLBACKS.lock().unwrap(); + trace!("Locked"); let callbacks = callbacks.get_mut(&backend_id).unwrap(); if let Some(callback) = callbacks.utterance_stop.as_mut() { + trace!("Calling utterance_stop"); let utterance_id = UtteranceId::AvFoundation(utterance); callback(utterance_id); + trace!("Called"); } } + trace!("Done speech_synthesizer_did_cancel_speech_utterance"); } unsafe { @@ -107,16 +125,20 @@ impl AvFoundation { let delegate_obj: *mut Object = unsafe { msg_send![delegate_class, new] }; let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); let rv = unsafe { + trace!("Creating synth"); let synth: *mut Object = msg_send![class!(AVSpeechSynthesizer), new]; + trace!("Allocated {:?}", synth); delegate_obj .as_mut() .unwrap() .set_ivar("backend_id", *backend_id); + trace!("Set backend ID in delegate"); let _: () = msg_send![synth, setDelegate: delegate_obj]; + trace!("Assigned delegate: {:?}", delegate_obj); AvFoundation { id: BackendId::AvFoundation(*backend_id), delegate: delegate_obj, - synth: synth, + synth, rate: 0.5, volume: 1., pitch: 1., @@ -145,18 +167,27 @@ impl Backend for AvFoundation { fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error> { trace!("speak({}, {})", text, interrupt); - if interrupt { + if interrupt && self.is_speaking()? { self.stop()?; } - let utterance: id; + let mut utterance: id; unsafe { - let str = NSString::alloc(nil).init_str(text); + trace!("Allocating utterance string"); + let mut str = NSString::alloc(nil); + str = str.init_str(text); + trace!("Allocating utterance"); utterance = msg_send![class!(AVSpeechUtterance), alloc]; - let _: () = msg_send![utterance, initWithString: str]; + trace!("Initializing utterance"); + utterance = msg_send![utterance, initWithString: str]; + trace!("Setting rate to {}", self.rate); let _: () = msg_send![utterance, setRate: self.rate]; + trace!("Setting volume to {}", self.volume); let _: () = msg_send![utterance, setVolume: self.volume]; + trace!("Setting pitch to {}", self.pitch); let _: () = msg_send![utterance, setPitchMultiplier: self.pitch]; + trace!("Enqueuing"); let _: () = msg_send![self.synth, speakUtterance: utterance]; + trace!("Done queuing"); } Ok(Some(UtteranceId::AvFoundation(utterance))) } @@ -208,6 +239,7 @@ impl Backend for AvFoundation { } fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> { + trace!("set_pitch({})", pitch); self.pitch = pitch; Ok(()) } @@ -229,13 +261,15 @@ impl Backend for AvFoundation { } fn set_volume(&mut self, volume: f32) -> Result<(), Error> { + trace!("set_volume({})", volume); self.volume = volume; Ok(()) } fn is_speaking(&self) -> Result { + trace!("is_speaking()"); let is_speaking: i8 = unsafe { msg_send![self.synth, isSpeaking] }; - Ok(is_speaking == 1) + Ok(is_speaking != NO as i8) } } diff --git a/src/backends/tolk.rs b/src/backends/tolk.rs index ae16e4f..51431a8 100644 --- a/src/backends/tolk.rs +++ b/src/backends/tolk.rs @@ -1,11 +1,13 @@ #[cfg(all(windows, feature = "tolk"))] +use std::sync::Arc; + use log::{info, trace}; use tolk::Tolk as TolkPtr; use crate::{Backend, BackendId, Error, Features, UtteranceId}; #[derive(Clone, Debug)] -pub(crate) struct Tolk(TolkPtr); +pub(crate) struct Tolk(Arc); impl Tolk { pub(crate) fn new() -> Option { diff --git a/winrt_bindings/src/lib.rs b/src/backends/winrt/bindings.rs similarity index 96% rename from winrt_bindings/src/lib.rs rename to src/backends/winrt/bindings.rs index 42af6ba..7915760 100644 --- a/winrt_bindings/src/lib.rs +++ b/src/backends/winrt/bindings.rs @@ -1 +1 @@ -::windows::include_bindings!(); +::windows::include_bindings!(); diff --git a/src/backends/winrt.rs b/src/backends/winrt/mod.rs similarity index 79% rename from src/backends/winrt.rs rename to src/backends/winrt/mod.rs index d9b7704..af9ca34 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt/mod.rs @@ -5,22 +5,27 @@ use std::sync::Mutex; use lazy_static::lazy_static; use log::{info, trace}; -use tts_winrt_bindings::windows::media::playback::{ - MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory, +mod bindings; + +use bindings::Windows::{ + Foundation::TypedEventHandler, + Media::{ + Core::MediaSource, + Playback::{MediaPlayer, MediaPlayerAudioCategory}, + SpeechSynthesis::SpeechSynthesizer, + }, }; -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, CALLBACKS}; impl From for Error { fn from(e: windows::Error) -> Self { - Error::WinRT(e) + Error::WinRt(e) } } #[derive(Clone, Debug)] -pub struct WinRT { +pub struct WinRt { id: BackendId, synth: SpeechSynthesizer, player: MediaPlayer, @@ -54,15 +59,15 @@ lazy_static! { }; } -impl WinRT { +impl WinRt { pub fn new() -> std::result::Result { info!("Initializing WinRT backend"); let synth = SpeechSynthesizer::new()?; let player = MediaPlayer::new()?; - player.set_real_time_playback(true)?; - player.set_audio_category(MediaPlayerAudioCategory::Speech)?; + player.SetRealTimePlayback(true)?; + player.SetAudioCategory(MediaPlayerAudioCategory::Speech)?; let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); - let bid = BackendId::WinRT(*backend_id); + let bid = BackendId::WinRt(*backend_id); *backend_id += 1; drop(backend_id); { @@ -76,7 +81,7 @@ impl WinRT { backend_to_speech_synthesizer.insert(bid, synth.clone()); drop(backend_to_speech_synthesizer); let bid_clone = bid; - player.media_ended(TypedEventHandler::new( + player.MediaEnded(TypedEventHandler::new( move |sender: &Option, _args| { if let Some(sender) = sender { let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); @@ -97,19 +102,17 @@ impl WinRT { .iter() .find(|v| *v.0 == bid_clone); if let Some((_, tts)) = id { - tts.options()?.set_speaking_rate(utterance.rate.into())?; - tts.options()?.set_audio_pitch(utterance.pitch.into())?; - tts.options()?.set_audio_volume(utterance.volume.into())?; + tts.Options()?.SetSpeakingRate(utterance.rate.into())?; + tts.Options()?.SetAudioPitch(utterance.pitch.into())?; + tts.Options()?.SetAudioVolume(utterance.volume.into())?; let stream = tts - .synthesize_text_to_stream_async( - utterance.text.as_str(), - )? + .SynthesizeTextToStreamAsync(utterance.text.as_str())? .get()?; - let content_type = stream.content_type()?; + let content_type = stream.ContentType()?; let source = - MediaSource::create_from_stream(stream, content_type)?; - sender.set_source(source)?; - sender.play()?; + MediaSource::CreateFromStream(stream, content_type)?; + sender.SetSource(source)?; + sender.Play()?; if let Some(callback) = callbacks.utterance_begin.as_mut() { callback(utterance.id); } @@ -133,7 +136,7 @@ impl WinRT { } } -impl Backend for WinRT { +impl Backend for WinRt { fn id(&self) -> Option { Some(self.id) } @@ -159,7 +162,7 @@ impl Backend for WinRT { } let utterance_id = { let mut uid = NEXT_UTTERANCE_ID.lock().unwrap(); - let utterance_id = UtteranceId::WinRT(*uid); + let utterance_id = UtteranceId::WinRt(*uid); *uid += 1; utterance_id }; @@ -178,17 +181,15 @@ impl Backend for WinRT { utterances.push_back(utterance); } } - if no_utterances - && self.player.playback_session()?.playback_state()? != MediaPlaybackState::Playing - { - self.synth.options()?.set_speaking_rate(self.rate.into())?; - self.synth.options()?.set_audio_pitch(self.pitch.into())?; - self.synth.options()?.set_audio_volume(self.volume.into())?; - 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)?; - self.player.set_source(source)?; - self.player.play()?; + if no_utterances { + self.synth.Options()?.SetSpeakingRate(self.rate.into())?; + self.synth.Options()?.SetAudioPitch(self.pitch.into())?; + self.synth.Options()?.SetAudioVolume(self.volume.into())?; + let stream = self.synth.SynthesizeTextToStreamAsync(text)?.get()?; + let content_type = stream.ContentType()?; + let source = MediaSource::CreateFromStream(stream, content_type)?; + self.player.SetSource(source)?; + self.player.Play()?; let mut callbacks = CALLBACKS.lock().unwrap(); let callbacks = callbacks.get_mut(&self.id).unwrap(); if let Some(callback) = callbacks.utterance_begin.as_mut() { @@ -216,7 +217,7 @@ impl Backend for WinRT { if let Some(utterances) = utterances.get_mut(&self.id) { utterances.clear(); } - self.player.pause()?; + self.player.Pause()?; Ok(()) } @@ -233,7 +234,7 @@ impl Backend for WinRT { } fn get_rate(&self) -> std::result::Result { - let rate = self.synth.options()?.speaking_rate()?; + let rate = self.synth.Options()?.SpeakingRate()?; Ok(rate as f32) } @@ -255,7 +256,7 @@ impl Backend for WinRT { } fn get_pitch(&self) -> std::result::Result { - let pitch = self.synth.options()?.audio_pitch()?; + let pitch = self.synth.Options()?.AudioPitch()?; Ok(pitch as f32) } @@ -277,7 +278,7 @@ impl Backend for WinRT { } fn get_volume(&self) -> std::result::Result { - let volume = self.synth.options()?.audio_volume()?; + let volume = self.synth.Options()?.AudioVolume()?; Ok(volume as f32) } @@ -293,7 +294,7 @@ impl Backend for WinRT { } } -impl Drop for WinRT { +impl Drop for WinRt { fn drop(&mut self) { let id = self.id; let mut backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 0ce01c8..ff96121 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,8 @@ use libc::c_char; #[cfg(target_os = "macos")] use objc::{class, msg_send, sel, sel_impl}; use thiserror::Error; +#[cfg(all(windows, feature = "tolk"))] +use tolk::Tolk; mod backends; #[cfg(feature = "ffi")] @@ -42,7 +44,7 @@ pub enum Backends { #[cfg(all(windows, feature = "tolk"))] Tolk, #[cfg(windows)] - WinRT, + WinRt, #[cfg(target_os = "macos")] AppKit, #[cfg(any(target_os = "macos", target_os = "ios"))] @@ -58,7 +60,7 @@ pub enum BackendId { #[cfg(target_arch = "wasm32")] Web(u64), #[cfg(windows)] - WinRT(u64), + WinRt(u64), #[cfg(any(target_os = "macos", target_os = "ios"))] AvFoundation(u64), #[cfg(target_os = "android")] @@ -72,7 +74,7 @@ pub enum UtteranceId { #[cfg(target_arch = "wasm32")] Web(u64), #[cfg(windows)] - WinRT(u64), + WinRt(u64), #[cfg(any(target_os = "macos", target_os = "ios"))] AvFoundation(id), #[cfg(target_os = "android")] @@ -109,7 +111,7 @@ impl Default for Features { #[derive(Debug, Error)] pub enum Error { #[error("IO error: {0}")] - IO(#[from] std::io::Error), + Io(#[from] std::io::Error), #[error("Value not received")] NoneError, #[error("Operation failed")] @@ -119,7 +121,7 @@ pub enum Error { JavaScriptError(wasm_bindgen::JsValue), #[cfg(windows)] #[error("WinRT error")] - WinRT(windows::Error), + WinRt(windows::Error), #[error("Unsupported feature")] UnsupportedFeature, #[error("Out of range")] @@ -172,47 +174,47 @@ lazy_static! { } #[derive(Clone)] -pub struct TTS(Box); +pub struct Tts(Box); -unsafe impl Send for TTS {} +unsafe impl Send for Tts {} -unsafe impl Sync for TTS {} +unsafe impl Sync for Tts {} -impl TTS { +impl Tts { /** * Create a new `TTS` instance with the specified backend. */ - pub fn new(backend: Backends) -> Result { + pub fn new(backend: Backends) -> Result { let backend = match backend { #[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")] Backends::Web => { let tts = backends::Web::new()?; - Ok(TTS(Box::new(tts))) + Ok(Tts(Box::new(tts))) } #[cfg(all(windows, feature = "tolk"))] Backends::Tolk => { let tts = backends::Tolk::new(); if let Some(tts) = tts { - Ok(TTS(Box::new(tts))) + Ok(Tts(Box::new(tts))) } else { Err(Error::NoneError) } } #[cfg(windows)] - Backends::WinRT => { - let tts = backends::WinRT::new()?; - Ok(TTS(Box::new(tts))) + Backends::WinRt => { + let tts = backends::WinRt::new()?; + Ok(Tts(Box::new(tts))) } #[cfg(target_os = "macos")] - 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"))] - Backends::AvFoundation => Ok(TTS(Box::new(backends::AvFoundation::new()))), + Backends::AvFoundation => Ok(Tts(Box::new(backends::AvFoundation::new()))), #[cfg(target_os = "android")] Backends::Android => { let tts = backends::Android::new()?; - Ok(TTS(Box::new(tts))) + Ok(Tts(Box::new(tts))) } }; if let Ok(backend) = backend { @@ -226,19 +228,19 @@ impl TTS { } } - pub fn default() -> Result { + pub fn default() -> Result { #[cfg(target_os = "linux")] - let tts = TTS::new(Backends::SpeechDispatcher); + let tts = Tts::new(Backends::SpeechDispatcher); #[cfg(all(windows, feature = "tolk"))] - let tts = if let Ok(tts) = TTS::new(Backends::Tolk) { + let tts = if let Ok(tts) = Tts::new(Backends::Tolk) { Ok(tts) } else { - TTS::new(Backends::WinRT) + Tts::new(Backends::WinRt) }; #[cfg(all(windows, not(feature = "tolk")))] - let tts = TTS::new(Backends::WinRT); + let tts = Tts::new(Backends::WinRt); #[cfg(target_arch = "wasm32")] - let tts = TTS::new(Backends::Web); + let tts = Tts::new(Backends::Web); #[cfg(target_os = "macos")] let tts = unsafe { // Needed because the Rust NSProcessInfo structs report bogus values, and I don't want to pull in a full bindgen stack. @@ -247,21 +249,21 @@ impl TTS { let str: *const c_char = msg_send![version, UTF8String]; let str = CStr::from_ptr(str); let str = str.to_string_lossy(); - let version: Vec<&str> = str.split(" ").collect(); + let version: Vec<&str> = str.split(' ').collect(); let version = version[1]; - let version_parts: Vec<&str> = version.split(".").collect(); + let version_parts: Vec<&str> = version.split('.').collect(); let major_version: i8 = version_parts[0].parse().unwrap(); let minor_version: i8 = version_parts[1].parse().unwrap(); if major_version >= 11 || minor_version >= 14 { - TTS::new(Backends::AvFoundation) + Tts::new(Backends::AvFoundation) } else { - TTS::new(Backends::AppKit) + Tts::new(Backends::AppKit) } }; #[cfg(target_os = "ios")] - let tts = TTS::new(Backends::AvFoundation); + let tts = Tts::new(Backends::AvFoundation); #[cfg(target_os = "android")] - let tts = TTS::new(Backends::Android); + let tts = Tts::new(Backends::Android); tts } @@ -531,9 +533,27 @@ impl TTS { Err(Error::UnsupportedFeature) } } + + /* + * Returns `true` if a screen reader is available to provide speech. + */ + #[allow(unreachable_code)] + pub fn screen_reader_available() -> bool { + #[cfg(target_os = "windows")] + { + #[cfg(feature = "tolk")] + { + let tolk = Tolk::new(); + return tolk.detect_screen_reader().is_some(); + } + #[cfg(not(feature = "tolk"))] + return false; + } + false + } } -impl Drop for TTS { +impl Drop for Tts { fn drop(&mut self) { if let Some(id) = self.0.id() { let mut callbacks = CALLBACKS.lock().unwrap(); diff --git a/winrt_bindings/Cargo.toml b/winrt_bindings/Cargo.toml deleted file mode 100644 index eaf4bb2..0000000 --- a/winrt_bindings/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "tts_winrt_bindings" -version = "0.3.0" -authors = ["Nolan Darilek "] -description = "Internal crate used by `tts`" -license = "MIT" -edition = "2018" - -[dependencies] -windows = "0.2" - -[build-dependencies] -windows = "0.2" diff --git a/winrt_bindings/build.rs b/winrt_bindings/build.rs deleted file mode 100644 index 654291a..0000000 --- a/winrt_bindings/build.rs +++ /dev/null @@ -1,7 +0,0 @@ -fn main() { - windows::build!( - windows::media::core::MediaSource - windows::media::playback::{MediaPlaybackState, MediaPlayer} - windows::media::speech_synthesis::SpeechSynthesizer - ); -}