From dbac8a3fe092ed6a2fea7770eaaf0039092b23e9 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Sat, 26 Sep 2020 12:43:16 -0500 Subject: [PATCH 001/196] Eliminate some Clippy warnings. --- src/backends/winrt.rs | 4 ++-- src/lib.rs | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 1625040..97bd9f0 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -56,8 +56,8 @@ impl WinRT { let mut rv = Self { id: bid, synth: SpeechSynthesizer::new()?, - player: player, - playback_list: playback_list, + player, + playback_list, }; *backend_id += 1; Self::init_callbacks(&mut rv)?; diff --git a/src/lib.rs b/src/lib.rs index 853a69c..6949ca1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -190,8 +190,7 @@ impl TTS { #[cfg(any(target_os = "macos", target_os = "ios"))] Backends::AvFoundation => Ok(TTS(Box::new(backends::AvFoundation::new()))), }; - if backend.is_ok() { - let backend = backend.unwrap(); + if let Ok(backend) = backend { if let Some(id) = backend.0.id() { let mut callbacks = CALLBACKS.lock().unwrap(); callbacks.insert(id, Callbacks::default()); @@ -206,7 +205,7 @@ impl TTS { #[cfg(target_os = "linux")] let tts = TTS::new(Backends::SpeechDispatcher); #[cfg(windows)] - let tts = if let Some(tts) = TTS::new(Backends::Tolk).ok() { + let tts = if let Ok(tts) = TTS::new(Backends::Tolk) { Ok(tts) } else { TTS::new(Backends::WinRT) From c2bbc5ac04c0e3510a511f4a8d45fc99ed70fa98 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Sat, 26 Sep 2020 12:47:18 -0500 Subject: [PATCH 002/196] Eliminate more Clippy warnings. --- src/backends/speech_dispatcher.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index b9001a7..dfafdf4 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -1,6 +1,5 @@ #[cfg(target_os = "linux")] use std::collections::HashMap; -use std::convert::TryInto; use std::sync::Mutex; use lazy_static::*; @@ -93,7 +92,7 @@ impl Backend for SpeechDispatcher { self.0.set_punctuation(Punctuation::None); } if let Some(id) = id { - Ok(Some(UtteranceId::SpeechDispatcher(id.try_into().unwrap()))) + Ok(Some(UtteranceId::SpeechDispatcher(id))) } else { Err(Error::NoneError) } From 174011bbb4d193a0ce097ae51578e709dfc1ebd3 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 8 Oct 2020 07:16:10 -0500 Subject: [PATCH 003/196] Make `UtteranceId` use `u64` on most platforms, and add additional derives. --- src/backends/web.rs | 25 ++++++++++++++++++----- src/backends/winrt.rs | 46 +++++++++++++++++++++++++++++++++++++------ src/lib.rs | 11 +++-------- 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/src/backends/web.rs b/src/backends/web.rs index 32be8f7..4696e7c 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -18,6 +18,8 @@ pub struct Web { lazy_static! { static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); + static ref UTTERANCE_MAPPINGS: Mutex> = Mutex::new(Vec::new()); + static ref NEXT_UTTERANCE_ID: Mutex = Mutex::new(0); } impl Web { @@ -58,23 +60,29 @@ impl Backend for Web { utterance.set_pitch(self.pitch); utterance.set_volume(self.volume); let id = self.id().unwrap(); - let utterance_id = UtteranceId::Web(utterance.clone()); - let callback = Closure::wrap(Box::new(move |evt: SpeechSynthesisEvent| { + let mut uid = NEXT_UTTERANCE_ID.lock().unwrap(); + let utterance_id = UtteranceId::Web(*uid); + *uid += 1; + drop(uid); + let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); + mappings.push((self.id, utterance_id)); + drop(mappings); + let callback = Closure::wrap(Box::new(move |_evt: SpeechSynthesisEvent| { let mut callbacks = CALLBACKS.lock().unwrap(); let callback = callbacks.get_mut(&id).unwrap(); if let Some(f) = callback.utterance_begin.as_mut() { - let utterance_id = UtteranceId::Web(evt.utterance()); f(utterance_id); } }) as Box); utterance.set_onstart(Some(callback.as_ref().unchecked_ref())); - let callback = Closure::wrap(Box::new(move |evt: SpeechSynthesisEvent| { + let callback = Closure::wrap(Box::new(move |_evt: SpeechSynthesisEvent| { let mut callbacks = CALLBACKS.lock().unwrap(); let callback = callbacks.get_mut(&id).unwrap(); if let Some(f) = callback.utterance_end.as_mut() { - let utterance_id = UtteranceId::Web(evt.utterance()); f(utterance_id); } + let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); + mappings.retain(|v| v.1 != utterance_id); }) as Box); utterance.set_onend(Some(callback.as_ref().unchecked_ref())); if interrupt { @@ -173,3 +181,10 @@ impl Backend for Web { } } } + +impl Drop for Web { + fn drop(&mut self) { + let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); + mappings.retain(|v| v.0 != self.id); + } +} diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 97bd9f0..e81894b 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -30,6 +30,9 @@ pub struct WinRT { lazy_static! { static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); + static ref NEXT_UTTERANCE_ID: Mutex = Mutex::new(0); + static ref UTTERANCE_MAPPINGS: Mutex> = + Mutex::new(Vec::new()); static ref BACKEND_TO_MEDIA_PLAYER: Mutex> = { let v: HashMap = HashMap::new(); Mutex::new(v) @@ -70,6 +73,16 @@ impl WinRT { self.player.set_auto_play(true)?; self.player.set_source(&self.playback_list)?; self.init_callbacks()?; + let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); + let mut callbacks = CALLBACKS.lock().unwrap(); + let callbacks = callbacks.get_mut(&self.id).unwrap(); + if let Some(callback) = callbacks.utterance_end.as_mut() { + let mappings = UTTERANCE_MAPPINGS.lock().unwrap(); + for mapping in &*mappings { + callback(mapping.2); + } + } + mappings.retain(|v| v.0 != self.id); Ok(()) } @@ -107,17 +120,31 @@ impl WinRT { let callbacks = callbacks.get_mut(&id).unwrap(); let old_item = args.old_item()?; if !old_item.is_null() { + let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); if let Some(callback) = callbacks.utterance_end.as_mut() { - callback(UtteranceId::WinRT(old_item)); + for mapping in &*mappings { + if mapping.1 == old_item { + callback(mapping.2); + } + } + mappings.retain(|v| v.1 != old_item); } } let new_item = args.new_item()?; if !new_item.is_null() { let mut last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); - let utterance_id = UtteranceId::WinRT(new_item); - last_spoken_utterance.insert(*id, utterance_id.clone()); + let mappings = UTTERANCE_MAPPINGS.lock().unwrap(); + for mapping in &*mappings { + if mapping.1 == new_item { + last_spoken_utterance.insert(*id, mapping.2); + } + } if let Some(callback) = callbacks.utterance_begin.as_mut() { - callback(utterance_id); + for mapping in &*mappings { + if mapping.1 == new_item { + callback(mapping.2); + } + } } } } @@ -169,7 +196,12 @@ impl Backend for WinRT { if !self.is_speaking()? { self.player.play()?; } - let utterance_id = UtteranceId::WinRT(item); + let mut uid = NEXT_UTTERANCE_ID.lock().unwrap(); + let utterance_id = UtteranceId::WinRT(*uid); + *uid += 1; + drop(uid); + let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); + mappings.push((self.id, item, utterance_id)); Ok(Some(utterance_id)) } @@ -254,12 +286,14 @@ impl Backend for WinRT { impl Drop for WinRT { fn drop(&mut self) { - let id = self.id().unwrap(); + let id = self.id; let mut backend_to_playback_list = BACKEND_TO_PLAYBACK_LIST.lock().unwrap(); backend_to_playback_list.remove(&id); let mut backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); backend_to_media_player.remove(&id); let mut last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); last_spoken_utterance.remove(&id); + let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); + mappings.retain(|v| v.0 != id); } } diff --git a/src/lib.rs b/src/lib.rs index 6949ca1..e3cbbde 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,11 +25,6 @@ use libc::c_char; #[cfg(target_os = "macos")] use objc::{class, msg_send, sel, sel_impl}; use thiserror::Error; -#[cfg(target_arch = "wasm32")] -use web_sys::SpeechSynthesisUtterance; - -#[cfg(windows)] -use tts_winrt_bindings::windows::media::playback::MediaPlaybackItem; mod backends; @@ -60,14 +55,14 @@ pub enum BackendId { AvFoundation(u64), } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum UtteranceId { #[cfg(target_os = "linux")] SpeechDispatcher(u64), #[cfg(target_arch = "wasm32")] - Web(SpeechSynthesisUtterance), + Web(u64), #[cfg(windows)] - WinRT(MediaPlaybackItem), + WinRT(u64), #[cfg(any(target_os = "macos", target_os = "ios"))] AvFoundation(id), } From 8c783205c3b72ec256ac8a4201b90acef7dc475d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 8 Oct 2020 07:56:45 -0500 Subject: [PATCH 004/196] Implement utterance_stop callback on most platforms. --- Cargo.toml | 2 +- src/backends/speech_dispatcher.rs | 9 ++++++++- src/backends/web.rs | 17 ++++++++++++++++- src/backends/winrt.rs | 2 +- src/lib.rs | 22 ++++++++++++++++++++++ 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9bce264..eb15d80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,4 +34,4 @@ objc = "0.2" [target.wasm32-unknown-unknown.dependencies] wasm-bindgen = "0.2" -web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "SpeechSynthesisEvent", "SpeechSynthesisUtterance", "Window", ] } +web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "SpeechSynthesisErrorCode", "SpeechSynthesisErrorEvent", "SpeechSynthesisEvent", "SpeechSynthesisUtterance", "Window", ] } diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index dfafdf4..72332ca 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -46,9 +46,16 @@ impl SpeechDispatcher { f(utterance_id); } }))); - sd.0.on_cancel(Some(Box::new(|_msg_id, client_id| { + sd.0.on_cancel(Some(Box::new(|msg_id, client_id| { let mut speaking = SPEAKING.lock().unwrap(); speaking.insert(client_id, false); + let mut callbacks = CALLBACKS.lock().unwrap(); + let backend_id = BackendId::SpeechDispatcher(client_id); + let cb = callbacks.get_mut(&backend_id).unwrap(); + let utterance_id = UtteranceId::SpeechDispatcher(msg_id); + if let Some(f) = cb.utterance_stop.as_mut() { + f(utterance_id); + } }))); sd.0.on_pause(Some(Box::new(|_msg_id, client_id| { let mut speaking = SPEAKING.lock().unwrap(); diff --git a/src/backends/web.rs b/src/backends/web.rs index 4696e7c..384a222 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -5,7 +5,10 @@ use lazy_static::lazy_static; use log::{info, trace}; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; -use web_sys::{SpeechSynthesisEvent, SpeechSynthesisUtterance}; +use web_sys::{ + SpeechSynthesisErrorCode, SpeechSynthesisErrorEvent, SpeechSynthesisEvent, + SpeechSynthesisUtterance, +}; use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; @@ -85,6 +88,18 @@ impl Backend for Web { mappings.retain(|v| v.1 != utterance_id); }) as Box); utterance.set_onend(Some(callback.as_ref().unchecked_ref())); + let callback = Closure::wrap(Box::new(move |evt: SpeechSynthesisErrorEvent| { + if evt.error() == SpeechSynthesisErrorCode::Cancel { + let mut callbacks = CALLBACKS.lock().unwrap(); + let callback = callbacks.get_mut(&id).unwrap(); + if let Some(f) = callback.utterance_stop.as_mut() { + f(utterance_id); + } + } + let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); + mappings.retain(|v| v.1 != utterance_id); + }) as Box); + utterance.set_onerror(Some(callback.as_ref().unchecked_ref())); if interrupt { self.stop()?; } diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index e81894b..ee4c6b4 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -76,7 +76,7 @@ impl WinRT { let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); let mut callbacks = CALLBACKS.lock().unwrap(); let callbacks = callbacks.get_mut(&self.id).unwrap(); - if let Some(callback) = callbacks.utterance_end.as_mut() { + if let Some(callback) = callbacks.utterance_stop.as_mut() { let mappings = UTTERANCE_MAPPINGS.lock().unwrap(); for mapping in &*mappings { callback(mapping.2); diff --git a/src/lib.rs b/src/lib.rs index e3cbbde..ecf89ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -134,6 +134,7 @@ pub trait Backend { struct Callbacks { utterance_begin: Option>, utterance_end: Option>, + utterance_stop: Option>, } unsafe impl Send for Callbacks {} @@ -474,6 +475,27 @@ impl TTS { Err(Error::UnsupportedFeature) } } + /** + * Called when this speech synthesizer is stopped and still has utterances in its queue. + */ + pub fn on_utterance_stop( + &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_stop = callback; + Ok(()) + } else { + Err(Error::UnsupportedFeature) + } + } } impl Drop for TTS { From 6f12974ce47b823ddcce20669c903166bf021bfe Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 8 Oct 2020 08:07:33 -0500 Subject: [PATCH 005/196] Implement stop callback on MacOS. --- src/backends/av_foundation.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 84c8824..58af2f6 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -66,6 +66,24 @@ impl AvFoundation { } } + extern "C" fn speech_synthesizer_did_cancel_speech_utterance( + this: &Object, + _: Sel, + _synth: *const Object, + utterance: id, + ) { + unsafe { + let backend_id: u64 = *this.get_ivar("backend_id"); + let backend_id = BackendId::AvFoundation(backend_id); + let mut callbacks = CALLBACKS.lock().unwrap(); + let callbacks = callbacks.get_mut(&backend_id).unwrap(); + if let Some(callback) = callbacks.utterance_stop.as_mut() { + let utterance_id = UtteranceId::AvFoundation(utterance); + callback(utterance_id); + } + } + } + unsafe { decl.add_method( sel!(speechSynthesizer:didStartSpeechUtterance:), @@ -77,6 +95,11 @@ impl AvFoundation { speech_synthesizer_did_finish_speech_utterance as extern "C" fn(&Object, Sel, *const Object, id) -> (), ); + decl.add_method( + sel!(speechSynthesizer:didCancelSpeechUtterance:), + speech_synthesizer_did_cancel_speech_utterance + as extern "C" fn(&Object, Sel, *const Object, id) -> (), + ); } let delegate_class = decl.register(); From 724dd1214fb23362d794353b7ac8bd640ac8356b Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 8 Oct 2020 08:08:23 -0500 Subject: [PATCH 006/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index eb15d80..c277576 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.8.0" +version = "0.9.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 0c13c43a7706d3f449bed636018a1cc6f3371e6d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 8 Oct 2020 08:16:01 -0500 Subject: [PATCH 007/196] Fix incorrect error code name. --- src/backends/web.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/web.rs b/src/backends/web.rs index 384a222..1b047a8 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -89,7 +89,7 @@ impl Backend for Web { }) as Box); utterance.set_onend(Some(callback.as_ref().unchecked_ref())); let callback = Closure::wrap(Box::new(move |evt: SpeechSynthesisErrorEvent| { - if evt.error() == SpeechSynthesisErrorCode::Cancel { + if evt.error() == SpeechSynthesisErrorCode::Canceled { let mut callbacks = CALLBACKS.lock().unwrap(); let callback = callbacks.get_mut(&id).unwrap(); if let Some(f) = callback.utterance_stop.as_mut() { From ba90cd66ba94b99ae85fc50ec6ce033915fd72f2 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 8 Oct 2020 09:06:48 -0500 Subject: [PATCH 008/196] Add unused example of setting on_utterance_stop. --- examples/hello_world.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index d21fe9d..4a35999 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -23,6 +23,9 @@ fn main() -> Result<(), Error> { tts.on_utterance_end(Some(Box::new(|utterance| { println!("Finished speaking {:?}", utterance) })))?; + tts.on_utterance_stop(Some(Box::new(|utterance| { + println!("Stopped speaking {:?}", utterance) + })))?; } tts.speak("Hello, world.", false)?; let Features { rate, .. } = tts.supported_features(); From 88ec7db0750d36f73b35761e06b5fcede21d445e Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 8 Oct 2020 09:44:46 -0500 Subject: [PATCH 009/196] Eliminate accidental deadlock. --- src/backends/winrt.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index ee4c6b4..bff7c3f 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -77,7 +77,6 @@ impl WinRT { let mut callbacks = CALLBACKS.lock().unwrap(); let callbacks = callbacks.get_mut(&self.id).unwrap(); if let Some(callback) = callbacks.utterance_stop.as_mut() { - let mappings = UTTERANCE_MAPPINGS.lock().unwrap(); for mapping in &*mappings { callback(mapping.2); } From 6eb03fb1a3868ca63662068e78d5c75d8ee9f488 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 8 Oct 2020 09:47:28 -0500 Subject: [PATCH 010/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c277576..3e25114 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.9.0" +version = "0.9.1" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From fa216a534e90ea0969c4f0de228ce01e66979b36 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 8 Oct 2020 19:07:07 -0500 Subject: [PATCH 011/196] Gate Tolk behind use_tolk feature to support compilation on UWP. --- Cargo.toml | 6 +++++- src/backends/mod.rs | 4 ++-- src/backends/tolk.rs | 2 +- src/lib.rs | 8 +++++--- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3e25114..085a080 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,10 @@ edition = "2018" [lib] crate-type = ["lib", "cdylib", "staticlib"] +[features] + +use_tolk = ["tolk"] + [dependencies] lazy_static = "1" log = "0.4" @@ -20,7 +24,7 @@ thiserror = "1" env_logger = "0.7" [target.'cfg(windows)'.dependencies] -tolk = ">= 0.2.1" +tolk = { version = ">= 0.2.1", optional = true } winrt = "0.7" tts_winrt_bindings = { version = "0.1", path="winrt_bindings" } diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 6274692..408e5bf 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -1,7 +1,7 @@ #[cfg(target_os = "linux")] mod speech_dispatcher; -#[cfg(windows)] +#[cfg(all(windows, feature = "use_tolk"))] mod tolk; #[cfg(windows)] @@ -19,7 +19,7 @@ mod av_foundation; #[cfg(target_os = "linux")] pub(crate) use self::speech_dispatcher::*; -#[cfg(windows)] +#[cfg(all(windows, feature = "use_tolk"))] pub(crate) use self::tolk::*; #[cfg(target_arch = "wasm32")] diff --git a/src/backends/tolk.rs b/src/backends/tolk.rs index 66c88e7..4cf7315 100644 --- a/src/backends/tolk.rs +++ b/src/backends/tolk.rs @@ -1,4 +1,4 @@ -#[cfg(windows)] +#[cfg(all(windows, feature = "use_tolk"))] use log::{info, trace}; use tolk::Tolk as TolkPtr; diff --git a/src/lib.rs b/src/lib.rs index ecf89ad..c6a7a58 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,7 +33,7 @@ pub enum Backends { SpeechDispatcher, #[cfg(target_arch = "wasm32")] Web, - #[cfg(windows)] + #[cfg(all(windows, feature = "use_tolk"))] Tolk, #[cfg(windows)] WinRT, @@ -167,7 +167,7 @@ impl TTS { let tts = backends::Web::new()?; Ok(TTS(Box::new(tts))) } - #[cfg(windows)] + #[cfg(all(windows, feature = "use_tolk"))] Backends::Tolk => { let tts = backends::Tolk::new(); if let Some(tts) = tts { @@ -200,12 +200,14 @@ impl TTS { pub fn default() -> Result { #[cfg(target_os = "linux")] let tts = TTS::new(Backends::SpeechDispatcher); - #[cfg(windows)] + #[cfg(all(windows, feature = "use_tolk"))] let tts = if let Ok(tts) = TTS::new(Backends::Tolk) { Ok(tts) } else { TTS::new(Backends::WinRT) }; + #[cfg(all(windows, not(feature = "use_tolk")))] + let tts = TTS::new(Backends::WinRT); #[cfg(target_arch = "wasm32")] let tts = TTS::new(Backends::Web); #[cfg(target_os = "macos")] From 51837a51bfb4ee9cc5d45315dfbd7c275167aead Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 8 Oct 2020 19:08:18 -0500 Subject: [PATCH 012/196] Document feature. --- README.md | 2 +- src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c76fe23..f06defc 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ This library provides a high-level Text-To-Speech (TTS) interface supporting various backends. Currently supported backends are: * Windows - * Screen readers/SAPI via Tolk + * Screen readers/SAPI via Tolk (requires `use_tolk` Cargo feature) * WinRT * Linux via [Speech Dispatcher](https://freebsoft.org/speechd) * MacOS diff --git a/src/lib.rs b/src/lib.rs index c6a7a58..46ad4c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ * a Text-To-Speech (TTS) library providing high-level interfaces to a variety of backends. * Currently supported backends are: * * Windows - * * Screen readers/SAPI via Tolk + * * Screen readers/SAPI via Tolk (requires `use_tolk` Cargo feature) * * WinRT * * Linux via [Speech Dispatcher](https://freebsoft.org/speechd) * * MacOS From baa442f13621a876460fbc5fcf9e8a29b835960a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 8 Oct 2020 19:10:14 -0500 Subject: [PATCH 013/196] Separate WASM build into separate job. --- .github/workflows/test.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d49d480..09544af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,6 +15,14 @@ jobs: sudo apt-get install -y libspeechd-dev rustup update cargo build --release + + build_web: + name: Build Web + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: | + rustup update rustup target add wasm32-unknown-unknown cargo build --release --target wasm32-unknown-unknown From 1be226df8ab8299e15c59044418401b6f48b1b00 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 8 Oct 2020 19:11:41 -0500 Subject: [PATCH 014/196] Switch to cargo check and build with all features under Windows. --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09544af..35efc33 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: sudo apt-get update sudo apt-get install -y libspeechd-dev rustup update - cargo build --release + cargo check --release build_web: name: Build Web @@ -24,7 +24,7 @@ jobs: - run: | rustup update rustup target add wasm32-unknown-unknown - cargo build --release --target wasm32-unknown-unknown + cargo check --release --target wasm32-unknown-unknown build_windows: name: Build Windows @@ -34,7 +34,7 @@ jobs: - run: | choco install -y llvm rustup update - cargo build --release + cargo check --release --all-features build_macos: name: Build MacOS @@ -43,7 +43,7 @@ jobs: - uses: actions/checkout@v2 - run: | rustup update - cargo build --release + cargo check --release build_ios: name: Build iOS From ca186671b48ca1e0775a60826ae6e1bb7a05dcdb Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 8 Oct 2020 19:15:12 -0500 Subject: [PATCH 015/196] Make similar refactors to release action. --- .github/workflows/release.yml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 911d343..297d893 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,9 +15,19 @@ jobs: sudo apt-get update sudo apt-get install -y libspeechd-dev rustup update - cargo build --release + cargo check --release rustup target add wasm32-unknown-unknown - cargo build --release --target wasm32-unknown-unknown + cargo check --release --target wasm32-unknown-unknown + + build_web: + name: Build Web + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: | + rustup update + rustup target add wasm32-unknown-unknown + cargo check --release --target wasm32-unknown-unknown build_windows: name: Build Windows @@ -27,7 +37,7 @@ jobs: - run: | choco install -y llvm rustup update - cargo build --release + cargo check --all-features --release build_macos: name: Build MacOS @@ -36,7 +46,7 @@ jobs: - uses: actions/checkout@v2 - run: | rustup update - cargo build --release + cargo check --release build_ios: name: Build iOS @@ -68,7 +78,7 @@ jobs: publish: name: Publish runs-on: ubuntu-latest - needs: [build_linux, build_windows, build_macos, build_ios] + needs: [build_linux, build_web, build_windows, build_macos, build_ios] env: CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} steps: From 5d1625e5e253e5ed6024c03acc4dbd3aa3db1ce2 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 8 Oct 2020 20:14:38 -0500 Subject: [PATCH 016/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 085a080..fc4c533 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.9.1" +version = "0.10.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From a281d74e5ccd7ecc8b2b6778f430674223067f87 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 14 Oct 2020 03:51:08 -0500 Subject: [PATCH 017/196] Whitespace fix. --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 46ad4c8..6e9ce86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -477,6 +477,7 @@ impl TTS { Err(Error::UnsupportedFeature) } } + /** * Called when this speech synthesizer is stopped and still has utterances in its queue. */ From e66b8403aa90719f8be0f91a6419d73d9dc9c41a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 14 Oct 2020 03:54:53 -0500 Subject: [PATCH 018/196] Remove unnecessary full module names. --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6e9ce86..a071d93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -150,9 +150,9 @@ lazy_static! { pub struct TTS(Box); -unsafe impl std::marker::Send for TTS {} +unsafe impl Send for TTS {} -unsafe impl std::marker::Sync for TTS {} +unsafe impl Sync for TTS {} impl TTS { /** From 0bbda0a90f7ff54867ab69e05e824c2299aecfb7 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 30 Oct 2020 10:23:24 -0500 Subject: [PATCH 019/196] Remove WinRT code for handling paused player state, which caused issues with queued speech. --- src/backends/winrt.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index bff7c3f..816bebf 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -175,7 +175,7 @@ impl Backend for WinRT { text: &str, interrupt: bool, ) -> std::result::Result, Error> { - trace!("speak({}, {})", text, interrupt); + println!("speak({}, {})", text, interrupt); if interrupt { self.stop()?; } @@ -183,16 +183,9 @@ impl Backend for WinRT { let content_type = stream.content_type()?; let source = MediaSource::create_from_stream(stream, content_type)?; let item = MediaPlaybackItem::create(source)?; - let state = self.player.playback_session()?.playback_state()?; - if state == MediaPlaybackState::Paused { - let index = self.playback_list.current_item_index()?; - let total = self.playback_list.items()?.size()?; - if total != 0 && index == total - 1 { - self.reinit_player()?; - } - } self.playback_list.items()?.append(&item)?; if !self.is_speaking()? { + println!("Playing"); self.player.play()?; } let mut uid = NEXT_UTTERANCE_ID.lock().unwrap(); From 1d48cb93d7599fffe2a17b058fd141e00681853f Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 30 Oct 2020 10:28:02 -0500 Subject: [PATCH 020/196] Bump version and dependencies. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fc4c533..a2498c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.10.0" +version = "0.10.1" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -21,7 +21,7 @@ log = "0.4" thiserror = "1" [dev-dependencies] -env_logger = "0.7" +env_logger = "0.8" [target.'cfg(windows)'.dependencies] tolk = { version = ">= 0.2.1", optional = true } From 5feede0b8f647c0e91acc53a542e60865b020bc9 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 2 Nov 2020 10:11:53 -0600 Subject: [PATCH 021/196] Remove unnecessary debug logs. --- src/backends/winrt.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 816bebf..aceffa4 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -175,7 +175,6 @@ impl Backend for WinRT { text: &str, interrupt: bool, ) -> std::result::Result, Error> { - println!("speak({}, {})", text, interrupt); if interrupt { self.stop()?; } @@ -185,7 +184,6 @@ impl Backend for WinRT { let item = MediaPlaybackItem::create(source)?; self.playback_list.items()?.append(&item)?; if !self.is_speaking()? { - println!("Playing"); self.player.play()?; } let mut uid = NEXT_UTTERANCE_ID.lock().unwrap(); From efdb274eb485c73a9eef8ebf1582a83b8e13590f Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 2 Nov 2020 10:12:25 -0600 Subject: [PATCH 022/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a2498c1..7d0ac26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.10.1" +version = "0.10.2" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 565aa6d654a178c42dc4c0ba77aa7dbafe0b6ab2 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 2 Nov 2020 13:30:39 -0600 Subject: [PATCH 023/196] Fix issue where is_speaking always returns true under WinRT, and bump version. --- Cargo.toml | 2 +- examples/hello_world.rs | 4 ++++ src/backends/winrt.rs | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7d0ac26..9c0a2cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.10.2" +version = "0.10.3" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 4a35999..f3fa7a8 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -27,6 +27,10 @@ fn main() -> Result<(), Error> { println!("Stopped speaking {:?}", utterance) })))?; } + let Features { is_speaking, .. } = tts.supported_features(); + if is_speaking { + println!("Are we speaking? {}", tts.is_speaking()?); + } tts.speak("Hello, world.", false)?; let Features { rate, .. } = tts.supported_features(); if rate { diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index aceffa4..618c810 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -269,7 +269,7 @@ impl Backend for WinRT { fn is_speaking(&self) -> std::result::Result { let state = self.player.playback_session()?.playback_state()?; - let playing = state == MediaPlaybackState::Opening || state == MediaPlaybackState::Playing; + let playing = state == MediaPlaybackState::Playing; Ok(playing) } } From 551bb1292e1e9f0d8859015fae6ee9886eefb686 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 2 Nov 2020 21:27:13 -0600 Subject: [PATCH 024/196] Make `TTS` clonable. Also, add other possibly useful derives. --- Cargo.toml | 3 ++- src/backends/appkit.rs | 1 + src/backends/av_foundation.rs | 1 + src/backends/speech_dispatcher.rs | 1 + src/backends/tolk.rs | 1 + src/backends/web.rs | 1 + src/backends/winrt.rs | 1 + src/lib.rs | 6 +++++- 8 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9c0a2cc..bba2987 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ crate-type = ["lib", "cdylib", "staticlib"] use_tolk = ["tolk"] [dependencies] +dyn-clonable = "0.9" lazy_static = "1" log = "0.4" thiserror = "1" @@ -24,7 +25,7 @@ thiserror = "1" env_logger = "0.8" [target.'cfg(windows)'.dependencies] -tolk = { version = ">= 0.2.1", optional = true } +tolk = { version = "0.3", optional = true } winrt = "0.7" tts_winrt_bindings = { version = "0.1", path="winrt_bindings" } diff --git a/src/backends/appkit.rs b/src/backends/appkit.rs index 647cc3f..432ec23 100644 --- a/src/backends/appkit.rs +++ b/src/backends/appkit.rs @@ -9,6 +9,7 @@ use objc::*; use crate::{Backend, BackendId, Error, Features, UtteranceId}; +#[derive(Clone)] pub(crate) struct AppKit(*mut Object, *mut Object); impl AppKit { diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 58af2f6..14fb584 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -11,6 +11,7 @@ use objc::{class, declare::ClassDecl, msg_send, sel, sel_impl}; use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; +#[derive(Clone)] pub(crate) struct AvFoundation { id: BackendId, delegate: *mut Object, diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 72332ca..b5aa9c9 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -8,6 +8,7 @@ use speech_dispatcher::*; use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; +#[derive(Clone)] pub(crate) struct SpeechDispatcher(Connection); lazy_static! { diff --git a/src/backends/tolk.rs b/src/backends/tolk.rs index 4cf7315..1230a02 100644 --- a/src/backends/tolk.rs +++ b/src/backends/tolk.rs @@ -4,6 +4,7 @@ use tolk::Tolk as TolkPtr; use crate::{Backend, BackendId, Error, Features, UtteranceId}; +#[derive(Clone)] pub(crate) struct Tolk(TolkPtr); impl Tolk { diff --git a/src/backends/web.rs b/src/backends/web.rs index 1b047a8..ee203fe 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -12,6 +12,7 @@ use web_sys::{ use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; +#[derive(Clone)] pub struct Web { id: BackendId, rate: f32, diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 618c810..6d01e97 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -21,6 +21,7 @@ impl From for Error { } } +#[derive(Clone)] pub struct WinRT { id: BackendId, synth: SpeechSynthesizer, diff --git a/src/lib.rs b/src/lib.rs index a071d93..3b5c357 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ use std::sync::Mutex; #[cfg(any(target_os = "macos", target_os = "ios"))] use cocoa_foundation::base::id; +use dyn_clonable::*; use lazy_static::lazy_static; #[cfg(target_os = "macos")] use libc::c_char; @@ -28,6 +29,7 @@ use thiserror::Error; mod backends; +#[derive(Clone, Copy, Debug)] pub enum Backends { #[cfg(target_os = "linux")] SpeechDispatcher, @@ -107,7 +109,8 @@ pub enum Error { OutOfRange, } -pub trait Backend { +#[clonable] +trait Backend: Clone { fn id(&self) -> Option; fn supported_features(&self) -> Features; fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error>; @@ -148,6 +151,7 @@ lazy_static! { }; } +#[derive(Clone)] pub struct TTS(Box); unsafe impl Send for TTS {} From cf0ad2221e10e6729bd828a1c5206a701089749e Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 2 Nov 2020 21:44:47 -0600 Subject: [PATCH 025/196] Derive Debug. --- src/backends/appkit.rs | 2 +- src/backends/av_foundation.rs | 2 +- src/backends/speech_dispatcher.rs | 2 +- src/backends/tolk.rs | 2 +- src/backends/web.rs | 2 +- src/backends/winrt.rs | 2 +- src/lib.rs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/backends/appkit.rs b/src/backends/appkit.rs index 432ec23..97f6cb4 100644 --- a/src/backends/appkit.rs +++ b/src/backends/appkit.rs @@ -9,7 +9,7 @@ use objc::*; use crate::{Backend, BackendId, Error, Features, UtteranceId}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub(crate) struct AppKit(*mut Object, *mut Object); impl AppKit { diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 14fb584..dbca640 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -11,7 +11,7 @@ use objc::{class, declare::ClassDecl, msg_send, sel, sel_impl}; use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub(crate) struct AvFoundation { id: BackendId, delegate: *mut Object, diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index b5aa9c9..39c7fd8 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -8,7 +8,7 @@ use speech_dispatcher::*; use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub(crate) struct SpeechDispatcher(Connection); lazy_static! { diff --git a/src/backends/tolk.rs b/src/backends/tolk.rs index 1230a02..f9281f7 100644 --- a/src/backends/tolk.rs +++ b/src/backends/tolk.rs @@ -4,7 +4,7 @@ use tolk::Tolk as TolkPtr; use crate::{Backend, BackendId, Error, Features, UtteranceId}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub(crate) struct Tolk(TolkPtr); impl Tolk { diff --git a/src/backends/web.rs b/src/backends/web.rs index ee203fe..0db2a10 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -12,7 +12,7 @@ use web_sys::{ use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Web { id: BackendId, rate: f32, diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 6d01e97..4de4060 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -21,7 +21,7 @@ impl From for Error { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct WinRT { id: BackendId, synth: SpeechSynthesizer, diff --git a/src/lib.rs b/src/lib.rs index 3b5c357..d305d3b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,7 +110,7 @@ pub enum Error { } #[clonable] -trait Backend: Clone { +trait Backend: Clone + std::fmt::Debug { fn id(&self) -> Option; fn supported_features(&self) -> Features; fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error>; From 29c0a8463ed815f74e65886b9b53e15cc69f38cd Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 2 Nov 2020 22:40:30 -0600 Subject: [PATCH 026/196] Pass TTS instance as first argument to utterance callbacks. --- examples/hello_world.rs | 6 +++--- src/backends/av_foundation.rs | 6 +++--- src/backends/speech_dispatcher.rs | 6 +++--- src/backends/web.rs | 6 +++--- src/backends/winrt.rs | 8 ++++---- src/lib.rs | 22 ++++++++++++++-------- 6 files changed, 30 insertions(+), 24 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index f3fa7a8..d099f05 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -17,13 +17,13 @@ fn main() -> Result<(), Error> { .. } = tts.supported_features(); if utterance_callbacks { - tts.on_utterance_begin(Some(Box::new(|utterance| { + tts.on_utterance_begin(Some(Box::new(|_tts, utterance| { println!("Started speaking {:?}", utterance) })))?; - tts.on_utterance_end(Some(Box::new(|utterance| { + tts.on_utterance_end(Some(Box::new(|_tts, utterance| { println!("Finished speaking {:?}", utterance) })))?; - tts.on_utterance_stop(Some(Box::new(|utterance| { + tts.on_utterance_stop(Some(Box::new(|_tts, utterance| { println!("Stopped speaking {:?}", utterance) })))?; } diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index dbca640..555be56 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -44,7 +44,7 @@ impl AvFoundation { let callbacks = callbacks.get_mut(&backend_id).unwrap(); if let Some(callback) = callbacks.utterance_begin.as_mut() { let utterance_id = UtteranceId::AvFoundation(utterance); - callback(utterance_id); + callback(callbacks.tts.clone(), utterance_id); } } } @@ -62,7 +62,7 @@ impl AvFoundation { let callbacks = callbacks.get_mut(&backend_id).unwrap(); if let Some(callback) = callbacks.utterance_end.as_mut() { let utterance_id = UtteranceId::AvFoundation(utterance); - callback(utterance_id); + callback(callbacks.tts.clone(), utterance_id); } } } @@ -80,7 +80,7 @@ impl AvFoundation { let callbacks = callbacks.get_mut(&backend_id).unwrap(); if let Some(callback) = callbacks.utterance_stop.as_mut() { let utterance_id = UtteranceId::AvFoundation(utterance); - callback(utterance_id); + callback(callbacks.tts.clone(), utterance_id); } } } diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 39c7fd8..524e791 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -33,7 +33,7 @@ impl SpeechDispatcher { let cb = callbacks.get_mut(&backend_id).unwrap(); let utterance_id = UtteranceId::SpeechDispatcher(msg_id); if let Some(f) = cb.utterance_begin.as_mut() { - f(utterance_id); + f(cb.tts.clone(), utterance_id); } }))); sd.0.on_end(Some(Box::new(|msg_id, client_id| { @@ -44,7 +44,7 @@ impl SpeechDispatcher { let cb = callbacks.get_mut(&backend_id).unwrap(); let utterance_id = UtteranceId::SpeechDispatcher(msg_id); if let Some(f) = cb.utterance_end.as_mut() { - f(utterance_id); + f(cb.tts.clone(), utterance_id); } }))); sd.0.on_cancel(Some(Box::new(|msg_id, client_id| { @@ -55,7 +55,7 @@ impl SpeechDispatcher { let cb = callbacks.get_mut(&backend_id).unwrap(); let utterance_id = UtteranceId::SpeechDispatcher(msg_id); if let Some(f) = cb.utterance_stop.as_mut() { - f(utterance_id); + f(cb.tts.clone(), utterance_id); } }))); sd.0.on_pause(Some(Box::new(|_msg_id, client_id| { diff --git a/src/backends/web.rs b/src/backends/web.rs index 0db2a10..113f564 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -75,7 +75,7 @@ impl Backend for Web { let mut callbacks = CALLBACKS.lock().unwrap(); let callback = callbacks.get_mut(&id).unwrap(); if let Some(f) = callback.utterance_begin.as_mut() { - f(utterance_id); + f(callback.tts.clone(), utterance_id); } }) as Box); utterance.set_onstart(Some(callback.as_ref().unchecked_ref())); @@ -83,7 +83,7 @@ impl Backend for Web { let mut callbacks = CALLBACKS.lock().unwrap(); let callback = callbacks.get_mut(&id).unwrap(); if let Some(f) = callback.utterance_end.as_mut() { - f(utterance_id); + f(callback.tts.clone(), utterance_id); } let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); mappings.retain(|v| v.1 != utterance_id); @@ -94,7 +94,7 @@ impl Backend for Web { let mut callbacks = CALLBACKS.lock().unwrap(); let callback = callbacks.get_mut(&id).unwrap(); if let Some(f) = callback.utterance_stop.as_mut() { - f(utterance_id); + f(callback.tts.clone(), utterance_id); } } let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 4de4060..c702167 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -79,7 +79,7 @@ impl WinRT { let callbacks = callbacks.get_mut(&self.id).unwrap(); if let Some(callback) = callbacks.utterance_stop.as_mut() { for mapping in &*mappings { - callback(mapping.2); + callback(callbacks.tts.clone(), mapping.2); } } mappings.retain(|v| v.0 != self.id); @@ -101,7 +101,7 @@ impl WinRT { if let Some(callback) = callbacks.utterance_end.as_mut() { let last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); if let Some(utterance_id) = last_spoken_utterance.get(&id) { - callback(utterance_id.clone()); + callback(callbacks.tts.clone(), utterance_id.clone()); } } } @@ -124,7 +124,7 @@ impl WinRT { if let Some(callback) = callbacks.utterance_end.as_mut() { for mapping in &*mappings { if mapping.1 == old_item { - callback(mapping.2); + callback(callbacks.tts.clone(), mapping.2); } } mappings.retain(|v| v.1 != old_item); @@ -142,7 +142,7 @@ impl WinRT { if let Some(callback) = callbacks.utterance_begin.as_mut() { for mapping in &*mappings { if mapping.1 == new_item { - callback(mapping.2); + callback(callbacks.tts.clone(), mapping.2); } } } diff --git a/src/lib.rs b/src/lib.rs index d305d3b..27616d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,11 +133,11 @@ trait Backend: Clone + std::fmt::Debug { fn is_speaking(&self) -> Result; } -#[derive(Default)] struct Callbacks { - utterance_begin: Option>, - utterance_end: Option>, - utterance_stop: Option>, + tts: TTS, + utterance_begin: Option>, + utterance_end: Option>, + utterance_stop: Option>, } unsafe impl Send for Callbacks {} @@ -193,7 +193,13 @@ impl TTS { if let Ok(backend) = backend { if let Some(id) = backend.0.id() { let mut callbacks = CALLBACKS.lock().unwrap(); - callbacks.insert(id, Callbacks::default()); + let cb = Callbacks { + tts: backend.clone(), + utterance_begin: None, + utterance_end: None, + utterance_stop: None, + }; + callbacks.insert(id, cb); } Ok(backend) } else { @@ -443,7 +449,7 @@ impl TTS { */ pub fn on_utterance_begin( &self, - callback: Option>, + callback: Option>, ) -> Result<(), Error> { let Features { utterance_callbacks, @@ -465,7 +471,7 @@ impl TTS { */ pub fn on_utterance_end( &self, - callback: Option>, + callback: Option>, ) -> Result<(), Error> { let Features { utterance_callbacks, @@ -487,7 +493,7 @@ impl TTS { */ pub fn on_utterance_stop( &self, - callback: Option>, + callback: Option>, ) -> Result<(), Error> { let Features { utterance_callbacks, From 2343523bb6d610452b9e79b5a0f92a34c7d6bc6e Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 3 Nov 2020 10:05:17 -0600 Subject: [PATCH 027/196] Add example that exposes WinRT issue where speech doubles in some circumstances. --- examples/99bottles.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 examples/99bottles.rs diff --git a/examples/99bottles.rs b/examples/99bottles.rs new file mode 100644 index 0000000..0c688d1 --- /dev/null +++ b/examples/99bottles.rs @@ -0,0 +1,43 @@ +use std::io; +use std::{thread, time}; + +#[cfg(target_os = "macos")] +use cocoa_foundation::base::id; +#[cfg(target_os = "macos")] +use cocoa_foundation::foundation::NSRunLoop; +#[cfg(target_os = "macos")] +use objc::{msg_send, sel, sel_impl}; + +use tts::*; + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut tts = TTS::default()?; + let Features { + utterance_callbacks, + .. + } = tts.supported_features(); + let mut bottles = 99; + while bottles > 0 { + tts.speak(format!("{} bottles of beer on the wall,", bottles), false)?; + tts.speak(format!("{} bottles of beer,", bottles), false)?; + tts.speak("Take one down, pass it around", false)?; + tts.speak("Give us a bit to drink this...", false)?; + let time = time::Duration::from_secs(5); + thread::sleep(time); + bottles -= 1; + tts.speak(format!("{} bottles of beer on the wall,", bottles), false)?; + } + let mut _input = String::new(); + // The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context. + // It shouldn't be needed in an app or game that almost certainly has one already. + #[cfg(target_os = "macos")] + { + let run_loop: id = unsafe { NSRunLoop::currentRunLoop() }; + unsafe { + let _: () = msg_send![run_loop, run]; + } + } + io::stdin().read_line(&mut _input)?; + Ok(()) +} From 5a9c96508f00104caa197c02e07a8c665a8191d6 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 3 Nov 2020 11:00:20 -0600 Subject: [PATCH 028/196] Remove unused variable. --- examples/99bottles.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/99bottles.rs b/examples/99bottles.rs index 0c688d1..960d675 100644 --- a/examples/99bottles.rs +++ b/examples/99bottles.rs @@ -13,17 +13,13 @@ use tts::*; fn main() -> Result<(), Error> { env_logger::init(); let mut tts = TTS::default()?; - let Features { - utterance_callbacks, - .. - } = tts.supported_features(); let mut bottles = 99; while bottles > 0 { tts.speak(format!("{} bottles of beer on the wall,", bottles), false)?; tts.speak(format!("{} bottles of beer,", bottles), false)?; tts.speak("Take one down, pass it around", false)?; tts.speak("Give us a bit to drink this...", false)?; - let time = time::Duration::from_secs(5); + let time = time::Duration::from_secs(10); thread::sleep(time); bottles -= 1; tts.speak(format!("{} bottles of beer on the wall,", bottles), false)?; From d3e05b5a7a642eb3212528ecc8cdedd406673213 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 3 Nov 2020 11:03:55 -0600 Subject: [PATCH 029/196] Revert "Pass TTS instance as first argument to utterance callbacks." This appears to break callbacks, and is of limited utility. This reverts commit 29c0a8463ed815f74e65886b9b53e15cc69f38cd. --- examples/hello_world.rs | 6 +++--- src/backends/av_foundation.rs | 6 +++--- src/backends/speech_dispatcher.rs | 6 +++--- src/backends/web.rs | 6 +++--- src/backends/winrt.rs | 8 ++++---- src/lib.rs | 22 ++++++++-------------- 6 files changed, 24 insertions(+), 30 deletions(-) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index d099f05..f3fa7a8 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -17,13 +17,13 @@ fn main() -> Result<(), Error> { .. } = tts.supported_features(); if utterance_callbacks { - tts.on_utterance_begin(Some(Box::new(|_tts, utterance| { + tts.on_utterance_begin(Some(Box::new(|utterance| { println!("Started speaking {:?}", utterance) })))?; - tts.on_utterance_end(Some(Box::new(|_tts, utterance| { + tts.on_utterance_end(Some(Box::new(|utterance| { println!("Finished speaking {:?}", utterance) })))?; - tts.on_utterance_stop(Some(Box::new(|_tts, utterance| { + tts.on_utterance_stop(Some(Box::new(|utterance| { println!("Stopped speaking {:?}", utterance) })))?; } diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 555be56..dbca640 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -44,7 +44,7 @@ impl AvFoundation { let callbacks = callbacks.get_mut(&backend_id).unwrap(); if let Some(callback) = callbacks.utterance_begin.as_mut() { let utterance_id = UtteranceId::AvFoundation(utterance); - callback(callbacks.tts.clone(), utterance_id); + callback(utterance_id); } } } @@ -62,7 +62,7 @@ impl AvFoundation { let callbacks = callbacks.get_mut(&backend_id).unwrap(); if let Some(callback) = callbacks.utterance_end.as_mut() { let utterance_id = UtteranceId::AvFoundation(utterance); - callback(callbacks.tts.clone(), utterance_id); + callback(utterance_id); } } } @@ -80,7 +80,7 @@ impl AvFoundation { let callbacks = callbacks.get_mut(&backend_id).unwrap(); if let Some(callback) = callbacks.utterance_stop.as_mut() { let utterance_id = UtteranceId::AvFoundation(utterance); - callback(callbacks.tts.clone(), utterance_id); + callback(utterance_id); } } } diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 524e791..39c7fd8 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -33,7 +33,7 @@ impl SpeechDispatcher { let cb = callbacks.get_mut(&backend_id).unwrap(); let utterance_id = UtteranceId::SpeechDispatcher(msg_id); if let Some(f) = cb.utterance_begin.as_mut() { - f(cb.tts.clone(), utterance_id); + f(utterance_id); } }))); sd.0.on_end(Some(Box::new(|msg_id, client_id| { @@ -44,7 +44,7 @@ impl SpeechDispatcher { let cb = callbacks.get_mut(&backend_id).unwrap(); let utterance_id = UtteranceId::SpeechDispatcher(msg_id); if let Some(f) = cb.utterance_end.as_mut() { - f(cb.tts.clone(), utterance_id); + f(utterance_id); } }))); sd.0.on_cancel(Some(Box::new(|msg_id, client_id| { @@ -55,7 +55,7 @@ impl SpeechDispatcher { let cb = callbacks.get_mut(&backend_id).unwrap(); let utterance_id = UtteranceId::SpeechDispatcher(msg_id); if let Some(f) = cb.utterance_stop.as_mut() { - f(cb.tts.clone(), utterance_id); + f(utterance_id); } }))); sd.0.on_pause(Some(Box::new(|_msg_id, client_id| { diff --git a/src/backends/web.rs b/src/backends/web.rs index 113f564..0db2a10 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -75,7 +75,7 @@ impl Backend for Web { let mut callbacks = CALLBACKS.lock().unwrap(); let callback = callbacks.get_mut(&id).unwrap(); if let Some(f) = callback.utterance_begin.as_mut() { - f(callback.tts.clone(), utterance_id); + f(utterance_id); } }) as Box); utterance.set_onstart(Some(callback.as_ref().unchecked_ref())); @@ -83,7 +83,7 @@ impl Backend for Web { let mut callbacks = CALLBACKS.lock().unwrap(); let callback = callbacks.get_mut(&id).unwrap(); if let Some(f) = callback.utterance_end.as_mut() { - f(callback.tts.clone(), utterance_id); + f(utterance_id); } let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); mappings.retain(|v| v.1 != utterance_id); @@ -94,7 +94,7 @@ impl Backend for Web { let mut callbacks = CALLBACKS.lock().unwrap(); let callback = callbacks.get_mut(&id).unwrap(); if let Some(f) = callback.utterance_stop.as_mut() { - f(callback.tts.clone(), utterance_id); + f(utterance_id); } } let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index c702167..4de4060 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -79,7 +79,7 @@ impl WinRT { let callbacks = callbacks.get_mut(&self.id).unwrap(); if let Some(callback) = callbacks.utterance_stop.as_mut() { for mapping in &*mappings { - callback(callbacks.tts.clone(), mapping.2); + callback(mapping.2); } } mappings.retain(|v| v.0 != self.id); @@ -101,7 +101,7 @@ impl WinRT { if let Some(callback) = callbacks.utterance_end.as_mut() { let last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); if let Some(utterance_id) = last_spoken_utterance.get(&id) { - callback(callbacks.tts.clone(), utterance_id.clone()); + callback(utterance_id.clone()); } } } @@ -124,7 +124,7 @@ impl WinRT { if let Some(callback) = callbacks.utterance_end.as_mut() { for mapping in &*mappings { if mapping.1 == old_item { - callback(callbacks.tts.clone(), mapping.2); + callback(mapping.2); } } mappings.retain(|v| v.1 != old_item); @@ -142,7 +142,7 @@ impl WinRT { if let Some(callback) = callbacks.utterance_begin.as_mut() { for mapping in &*mappings { if mapping.1 == new_item { - callback(callbacks.tts.clone(), mapping.2); + callback(mapping.2); } } } diff --git a/src/lib.rs b/src/lib.rs index 27616d9..d305d3b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,11 +133,11 @@ trait Backend: Clone + std::fmt::Debug { fn is_speaking(&self) -> Result; } +#[derive(Default)] struct Callbacks { - tts: TTS, - utterance_begin: Option>, - utterance_end: Option>, - utterance_stop: Option>, + utterance_begin: Option>, + utterance_end: Option>, + utterance_stop: Option>, } unsafe impl Send for Callbacks {} @@ -193,13 +193,7 @@ impl TTS { if let Ok(backend) = backend { if let Some(id) = backend.0.id() { let mut callbacks = CALLBACKS.lock().unwrap(); - let cb = Callbacks { - tts: backend.clone(), - utterance_begin: None, - utterance_end: None, - utterance_stop: None, - }; - callbacks.insert(id, cb); + callbacks.insert(id, Callbacks::default()); } Ok(backend) } else { @@ -449,7 +443,7 @@ impl TTS { */ pub fn on_utterance_begin( &self, - callback: Option>, + callback: Option>, ) -> Result<(), Error> { let Features { utterance_callbacks, @@ -471,7 +465,7 @@ impl TTS { */ pub fn on_utterance_end( &self, - callback: Option>, + callback: Option>, ) -> Result<(), Error> { let Features { utterance_callbacks, @@ -493,7 +487,7 @@ impl TTS { */ pub fn on_utterance_stop( &self, - callback: Option>, + callback: Option>, ) -> Result<(), Error> { let Features { utterance_callbacks, From 9c98026978ca2b4fc433130e0d693f3f83cbe6e0 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 3 Nov 2020 11:11:49 -0600 Subject: [PATCH 030/196] Don't re-initialize a player, just clear the item list. --- src/backends/winrt.rs | 154 +++++++++++++++++++----------------------- 1 file changed, 68 insertions(+), 86 deletions(-) diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 4de4060..50246cc 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -57,101 +57,74 @@ impl WinRT { player.set_source(&playback_list)?; let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); let bid = BackendId::WinRT(*backend_id); - let mut rv = Self { - id: bid, - synth: SpeechSynthesizer::new()?, - player, - playback_list, - }; *backend_id += 1; - Self::init_callbacks(&mut rv)?; - Ok(rv) - } - - fn reinit_player(&mut self) -> std::result::Result<(), Error> { - self.playback_list = MediaPlaybackList::new()?; - self.player = MediaPlayer::new()?; - self.player.set_auto_play(true)?; - self.player.set_source(&self.playback_list)?; - self.init_callbacks()?; - let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); - let mut callbacks = CALLBACKS.lock().unwrap(); - let callbacks = callbacks.get_mut(&self.id).unwrap(); - if let Some(callback) = callbacks.utterance_stop.as_mut() { - for mapping in &*mappings { - callback(mapping.2); - } - } - mappings.retain(|v| v.0 != self.id); - Ok(()) - } - - fn init_callbacks(&mut self) -> Result<(), winrt::Error> { - let id = self.id().unwrap(); let mut backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); - backend_to_media_player.insert(id, self.player.clone()); - self.player - .media_ended(TypedEventHandler::new(|sender, _args| { - let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); - let id = backend_to_media_player.iter().find(|v| v.1 == sender); + backend_to_media_player.insert(bid, player.clone()); + player.media_ended(TypedEventHandler::new(|sender, _args| { + let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); + let id = backend_to_media_player.iter().find(|v| v.1 == sender); + if let Some(id) = id { + let id = id.0; + let mut callbacks = CALLBACKS.lock().unwrap(); + let callbacks = callbacks.get_mut(&id).unwrap(); + if let Some(callback) = callbacks.utterance_end.as_mut() { + let last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); + if let Some(utterance_id) = last_spoken_utterance.get(&id) { + callback(utterance_id.clone()); + } + } + } + Ok(()) + }))?; + let mut backend_to_playback_list = BACKEND_TO_PLAYBACK_LIST.lock().unwrap(); + backend_to_playback_list.insert(bid, playback_list.clone()); + playback_list.current_item_changed(TypedEventHandler::new( + |sender: &MediaPlaybackList, args: &CurrentMediaPlaybackItemChangedEventArgs| { + let backend_to_playback_list = BACKEND_TO_PLAYBACK_LIST.lock().unwrap(); + let id = backend_to_playback_list.iter().find(|v| v.1 == sender); if let Some(id) = id { let id = id.0; let mut callbacks = CALLBACKS.lock().unwrap(); let callbacks = callbacks.get_mut(&id).unwrap(); - if let Some(callback) = callbacks.utterance_end.as_mut() { - let last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); - if let Some(utterance_id) = last_spoken_utterance.get(&id) { - callback(utterance_id.clone()); + let old_item = args.old_item()?; + if !old_item.is_null() { + let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); + if let Some(callback) = callbacks.utterance_end.as_mut() { + for mapping in &*mappings { + if mapping.1 == old_item { + callback(mapping.2); + } + } + mappings.retain(|v| v.1 != old_item); + } + } + let new_item = args.new_item()?; + if !new_item.is_null() { + let mut last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); + let mappings = UTTERANCE_MAPPINGS.lock().unwrap(); + for mapping in &*mappings { + if mapping.1 == new_item { + last_spoken_utterance.insert(*id, mapping.2); + } + } + if let Some(callback) = callbacks.utterance_begin.as_mut() { + for mapping in &*mappings { + if mapping.1 == new_item { + callback(mapping.2); + } + } } } } Ok(()) - }))?; - let mut backend_to_playback_list = BACKEND_TO_PLAYBACK_LIST.lock().unwrap(); - backend_to_playback_list.insert(id, self.playback_list.clone()); - self.playback_list - .current_item_changed(TypedEventHandler::new( - |sender: &MediaPlaybackList, args: &CurrentMediaPlaybackItemChangedEventArgs| { - let backend_to_playback_list = BACKEND_TO_PLAYBACK_LIST.lock().unwrap(); - let id = backend_to_playback_list.iter().find(|v| v.1 == sender); - if let Some(id) = id { - let id = id.0; - let mut callbacks = CALLBACKS.lock().unwrap(); - let callbacks = callbacks.get_mut(&id).unwrap(); - let old_item = args.old_item()?; - if !old_item.is_null() { - let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); - if let Some(callback) = callbacks.utterance_end.as_mut() { - for mapping in &*mappings { - if mapping.1 == old_item { - callback(mapping.2); - } - } - mappings.retain(|v| v.1 != old_item); - } - } - let new_item = args.new_item()?; - if !new_item.is_null() { - let mut last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); - let mappings = UTTERANCE_MAPPINGS.lock().unwrap(); - for mapping in &*mappings { - if mapping.1 == new_item { - last_spoken_utterance.insert(*id, mapping.2); - } - } - if let Some(callback) = callbacks.utterance_begin.as_mut() { - for mapping in &*mappings { - if mapping.1 == new_item { - callback(mapping.2); - } - } - } - } - } - Ok(()) - }, - ))?; - Ok(()) + }, + ))?; + Ok(Self { + id: bid, + synth: SpeechSynthesizer::new()?, + player, + playback_list, + }) } } @@ -198,7 +171,16 @@ impl Backend for WinRT { fn stop(&mut self) -> std::result::Result<(), Error> { trace!("stop()"); - self.reinit_player()?; + self.playback_list.items()?.clear()?; + let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); + let mut callbacks = CALLBACKS.lock().unwrap(); + let callbacks = callbacks.get_mut(&self.id).unwrap(); + if let Some(callback) = callbacks.utterance_stop.as_mut() { + for mapping in &*mappings { + callback(mapping.2); + } + } + mappings.retain(|v| v.0 != self.id); Ok(()) } From 289a35dc83c4e51da4d76ce6b74ea2c8a0020ae4 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 3 Nov 2020 11:20:02 -0600 Subject: [PATCH 031/196] Don't double-speak previous item when not flushing queue. --- src/backends/winrt.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 50246cc..46aef64 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -156,6 +156,12 @@ impl Backend for WinRT { let content_type = stream.content_type()?; let source = MediaSource::create_from_stream(stream, content_type)?; let item = MediaPlaybackItem::create(source)?; + let item_index = self.playback_list.current_item_index()?; + let item_count = self.playback_list.items()?.size()?; + let state = self.player.playback_session()?.playback_state()?; + if state == MediaPlaybackState::Paused && item_index != 0 { + self.playback_list.items()?.clear()?; + } self.playback_list.items()?.append(&item)?; if !self.is_speaking()? { self.player.play()?; From df4adc81a7bd173bafddad52d9a7fc366565a03f Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 3 Nov 2020 11:20:29 -0600 Subject: [PATCH 032/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bba2987..0c1485b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.10.3" +version = "0.11.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 4e157b6fb512269f08399a4d4d5bf424e0bd128a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 3 Nov 2020 11:23:03 -0600 Subject: [PATCH 033/196] Check examples when building. --- .github/workflows/release.yml | 10 +++++----- .github/workflows/test.yml | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 297d893..621ce7a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,9 +15,9 @@ jobs: sudo apt-get update sudo apt-get install -y libspeechd-dev rustup update - cargo check --release + cargo check --examples --release rustup target add wasm32-unknown-unknown - cargo check --release --target wasm32-unknown-unknown + cargo check --examples --release --target wasm32-unknown-unknown build_web: name: Build Web @@ -27,7 +27,7 @@ jobs: - run: | rustup update rustup target add wasm32-unknown-unknown - cargo check --release --target wasm32-unknown-unknown + cargo check --examples --release --target wasm32-unknown-unknown build_windows: name: Build Windows @@ -37,7 +37,7 @@ jobs: - run: | choco install -y llvm rustup update - cargo check --all-features --release + cargo check --examples --all-features --release build_macos: name: Build MacOS @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@v2 - run: | rustup update - cargo check --release + cargo check --examples --release build_ios: name: Build iOS diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 35efc33..0497a4c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: sudo apt-get update sudo apt-get install -y libspeechd-dev rustup update - cargo check --release + cargo check --examples --release build_web: name: Build Web @@ -24,7 +24,7 @@ jobs: - run: | rustup update rustup target add wasm32-unknown-unknown - cargo check --release --target wasm32-unknown-unknown + cargo check --examples --release --target wasm32-unknown-unknown build_windows: name: Build Windows @@ -34,7 +34,7 @@ jobs: - run: | choco install -y llvm rustup update - cargo check --release --all-features + cargo check --examples --release --all-features build_macos: name: Build MacOS @@ -43,7 +43,7 @@ jobs: - uses: actions/checkout@v2 - run: | rustup update - cargo check --release + cargo check --examples --release build_ios: name: Build iOS From 6b74afe503da7badaf265a81660dd35fdfaf6d73 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 3 Nov 2020 11:24:09 -0600 Subject: [PATCH 034/196] Remove unused variable. --- src/backends/winrt.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 46aef64..5509c67 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -157,7 +157,6 @@ impl Backend for WinRT { let source = MediaSource::create_from_stream(stream, content_type)?; let item = MediaPlaybackItem::create(source)?; let item_index = self.playback_list.current_item_index()?; - let item_count = self.playback_list.items()?.size()?; let state = self.player.playback_session()?.playback_state()?; if state == MediaPlaybackState::Paused && item_index != 0 { self.playback_list.items()?.clear()?; From 204cd50935f44121c6012fda42708a34e98b9eca Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 3 Nov 2020 11:51:40 -0600 Subject: [PATCH 035/196] Change example to expose more WinRT breakage. --- examples/99bottles.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/99bottles.rs b/examples/99bottles.rs index 960d675..53ef9e3 100644 --- a/examples/99bottles.rs +++ b/examples/99bottles.rs @@ -15,7 +15,7 @@ fn main() -> Result<(), Error> { let mut tts = TTS::default()?; let mut bottles = 99; while bottles > 0 { - tts.speak(format!("{} bottles of beer on the wall,", bottles), false)?; + tts.speak(format!("{} bottles of beer on the wall,", bottles), true)?; tts.speak(format!("{} bottles of beer,", bottles), false)?; tts.speak("Take one down, pass it around", false)?; tts.speak("Give us a bit to drink this...", false)?; From 031e0ff23f702c7763524262046b3eb08be9f79f Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 3 Nov 2020 12:02:16 -0600 Subject: [PATCH 036/196] Fix more queuing issues under WinRT. --- src/backends/winrt.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 5509c67..13db143 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -149,16 +149,17 @@ impl Backend for WinRT { text: &str, interrupt: bool, ) -> std::result::Result, Error> { - if interrupt { - self.stop()?; - } 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)?; let item_index = self.playback_list.current_item_index()?; + let item_count = self.playback_list.items()?.size()?; let state = self.player.playback_session()?.playback_state()?; - if state == MediaPlaybackState::Paused && item_index != 0 { + if interrupt && state != MediaPlaybackState::Paused { + self.stop()?; + } + if state == MediaPlaybackState::Paused && item_index != 0 && item_index < item_count { self.playback_list.items()?.clear()?; } self.playback_list.items()?.append(&item)?; From e1791c7046088f38c1c3383f9fec5520ccfb0100 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 3 Nov 2020 12:02:37 -0600 Subject: [PATCH 037/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0c1485b..505884f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.11.0" +version = "0.11.1" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 6a706f36ab36b6575a0a2cedfa038590f424bd11 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 11 Nov 2020 10:27:03 -0600 Subject: [PATCH 038/196] Fix double-speaking bug for good, hopefully. --- examples/99bottles.rs | 2 +- examples/ramble.rs | 35 +++++++++++++++++++++++++++++++++++ src/backends/winrt.rs | 10 ++++++++-- 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 examples/ramble.rs diff --git a/examples/99bottles.rs b/examples/99bottles.rs index 53ef9e3..960d675 100644 --- a/examples/99bottles.rs +++ b/examples/99bottles.rs @@ -15,7 +15,7 @@ fn main() -> Result<(), Error> { let mut tts = TTS::default()?; let mut bottles = 99; while bottles > 0 { - tts.speak(format!("{} bottles of beer on the wall,", bottles), true)?; + tts.speak(format!("{} bottles of beer on the wall,", bottles), false)?; tts.speak(format!("{} bottles of beer,", bottles), false)?; tts.speak("Take one down, pass it around", false)?; tts.speak("Give us a bit to drink this...", false)?; diff --git a/examples/ramble.rs b/examples/ramble.rs new file mode 100644 index 0000000..36265e7 --- /dev/null +++ b/examples/ramble.rs @@ -0,0 +1,35 @@ +use std::io; +use std::{thread, time}; + +#[cfg(target_os = "macos")] +use cocoa_foundation::base::id; +#[cfg(target_os = "macos")] +use cocoa_foundation::foundation::NSRunLoop; +#[cfg(target_os = "macos")] +use objc::{msg_send, sel, sel_impl}; + +use tts::*; + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut tts = TTS::default()?; + let mut phrase = 1; + loop { + tts.speak(format!("Phrase {}", phrase), false); + let time = time::Duration::from_secs(5); + thread::sleep(time); + phrase += 1; + } + let mut _input = String::new(); + // The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context. + // It shouldn't be needed in an app or game that almost certainly has one already. + #[cfg(target_os = "macos")] + { + let run_loop: id = unsafe { NSRunLoop::currentRunLoop() }; + unsafe { + let _: () = msg_send![run_loop, run]; + } + } + io::stdin().read_line(&mut _input)?; + Ok(()) +} diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 13db143..829a56d 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -4,7 +4,7 @@ use std::sync::Mutex; use lazy_static::lazy_static; use log::{info, trace}; -use winrt::ComInterface; +use winrt::*; use tts_winrt_bindings::windows::media::playback::{ CurrentMediaPlaybackItemChangedEventArgs, MediaPlaybackItem, MediaPlaybackList, @@ -60,7 +60,10 @@ impl WinRT { *backend_id += 1; let mut backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); backend_to_media_player.insert(bid, player.clone()); - player.media_ended(TypedEventHandler::new(|sender, _args| { + player.media_ended(TypedEventHandler::new(|sender: &MediaPlayer, _args| { + let source = sender.source()?; + let source: MediaPlaybackList = source.try_into()?; + source.items()?.clear()?; let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); let id = backend_to_media_player.iter().find(|v| v.1 == sender); if let Some(id) = id { @@ -80,6 +83,9 @@ impl WinRT { backend_to_playback_list.insert(bid, playback_list.clone()); playback_list.current_item_changed(TypedEventHandler::new( |sender: &MediaPlaybackList, args: &CurrentMediaPlaybackItemChangedEventArgs| { + println!("Changed"); + println!("{:?}, {:?}", args.old_item()?, args.new_item()?); + //sender.items()?.clear()?; let backend_to_playback_list = BACKEND_TO_PLAYBACK_LIST.lock().unwrap(); let id = backend_to_playback_list.iter().find(|v| v.1 == sender); if let Some(id) = id { From 80fa5d458310bf9a5d99ba50ec9c24431d947d26 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 11 Nov 2020 10:29:49 -0600 Subject: [PATCH 039/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 505884f..e8ba057 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.11.1" +version = "0.11.2" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 590d6369fbb4e5bc5f0376ce2c8d7a266e047807 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 10:25:21 -0600 Subject: [PATCH 040/196] Remove debugging printlns. --- src/backends/winrt.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 829a56d..07fd6ab 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -83,9 +83,6 @@ impl WinRT { backend_to_playback_list.insert(bid, playback_list.clone()); playback_list.current_item_changed(TypedEventHandler::new( |sender: &MediaPlaybackList, args: &CurrentMediaPlaybackItemChangedEventArgs| { - println!("Changed"); - println!("{:?}, {:?}", args.old_item()?, args.new_item()?); - //sender.items()?.clear()?; let backend_to_playback_list = BACKEND_TO_PLAYBACK_LIST.lock().unwrap(); let id = backend_to_playback_list.iter().find(|v| v.1 == sender); if let Some(id) = id { From 1cbeab6ea908e8caf301056b00162aaa635d22ae Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 10:25:45 -0600 Subject: [PATCH 041/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e8ba057..e671264 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.11.2" +version = "0.11.3" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 3500e88117521f30684e0c48cf3e5cc9187e2832 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 12:22:18 -0600 Subject: [PATCH 042/196] Do we need LLVM for this? --- .github/workflows/test.yml | 113 ++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 57 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0497a4c..b6871d5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,57 +1,56 @@ -name: Test - -on: - push: - pull_request: - -jobs: - build_linux: - name: Build Linux - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - run: | - sudo apt-get update - sudo apt-get install -y libspeechd-dev - rustup update - cargo check --examples --release - - build_web: - name: Build Web - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - run: | - rustup update - rustup target add wasm32-unknown-unknown - cargo check --examples --release --target wasm32-unknown-unknown - - build_windows: - name: Build Windows - runs-on: windows-latest - steps: - - uses: actions/checkout@v2 - - run: | - choco install -y llvm - rustup update - cargo check --examples --release --all-features - - build_macos: - name: Build MacOS - runs-on: macos-latest - steps: - - uses: actions/checkout@v2 - - run: | - rustup update - cargo check --examples --release - - build_ios: - name: Build iOS - runs-on: macos-latest - steps: - - uses: actions/checkout@v2 - - run: | - rustup update - rustup target add aarch64-apple-ios x86_64-apple-ios - cargo install cargo-lipo - cargo lipo --release +name: Test + +on: + push: + pull_request: + +jobs: + build_linux: + name: Build Linux + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: | + sudo apt-get update + sudo apt-get install -y libspeechd-dev + rustup update + cargo check --examples --release + + build_web: + name: Build Web + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: | + rustup update + rustup target add wasm32-unknown-unknown + cargo check --examples --release --target wasm32-unknown-unknown + + build_windows: + name: Build Windows + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - run: | + rustup update + cargo check --examples --release --all-features + + build_macos: + name: Build MacOS + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - run: | + rustup update + cargo check --examples --release + + build_ios: + name: Build iOS + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - run: | + rustup update + rustup target add aarch64-apple-ios x86_64-apple-ios + cargo install cargo-lipo + cargo lipo --release From 10010f9bc982d81776775af444cd3c84c944f694 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 13:15:37 -0600 Subject: [PATCH 043/196] Add caching to builds, and remove unneeded LLVM install. --- .github/workflows/release.yml | 188 ++++++++++++++++++---------------- .github/workflows/test.yml | 5 + 2 files changed, 102 insertions(+), 91 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 621ce7a..1500594 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,91 +1,97 @@ -name: Release - -on: - push: - tags: - - "v*" - -jobs: - build_linux: - name: Build Linux - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - run: | - sudo apt-get update - sudo apt-get install -y libspeechd-dev - rustup update - cargo check --examples --release - rustup target add wasm32-unknown-unknown - cargo check --examples --release --target wasm32-unknown-unknown - - build_web: - name: Build Web - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - run: | - rustup update - rustup target add wasm32-unknown-unknown - cargo check --examples --release --target wasm32-unknown-unknown - - build_windows: - name: Build Windows - runs-on: windows-latest - steps: - - uses: actions/checkout@v2 - - run: | - choco install -y llvm - rustup update - cargo check --examples --all-features --release - - build_macos: - name: Build MacOS - runs-on: macos-latest - steps: - - uses: actions/checkout@v2 - - run: | - rustup update - cargo check --examples --release - - build_ios: - name: Build iOS - runs-on: macos-latest - steps: - - uses: actions/checkout@v2 - - run: | - rustup update - rustup target add aarch64-apple-ios x86_64-apple-ios - cargo install cargo-lipo - cargo lipo --release - - publish_winrt_bindings: - name: Publish winrt_bindings - runs-on: windows-latest - needs: [build_windows] - env: - CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} - steps: - - uses: actions/checkout@v2 - - run: | - choco install -y llvm - rustup update - cargo login $CARGO_TOKEN - cd winrt_bindings - cargo package - cargo publish || true - - publish: - name: Publish - runs-on: ubuntu-latest - needs: [build_linux, build_web, build_windows, build_macos, build_ios] - env: - CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} - steps: - - uses: actions/checkout@v2 - - run: | - sudo apt-get update - sudo apt-get install -y libspeechd-dev - rustup update - cargo login $CARGO_TOKEN - cargo publish +name: Release + +on: + push: + tags: + - "v*" + +jobs: + build_linux: + name: Build Linux + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 + - run: | + sudo apt-get update + sudo apt-get install -y libspeechd-dev + rustup update + cargo check --examples --release + rustup target add wasm32-unknown-unknown + cargo check --examples --release --target wasm32-unknown-unknown + + build_web: + name: Build Web + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 + - run: | + rustup update + rustup target add wasm32-unknown-unknown + cargo check --examples --release --target wasm32-unknown-unknown + + build_windows: + name: Build Windows + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 + - run: | + rustup update + cargo check --examples --all-features --release + + build_macos: + name: Build MacOS + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 + - run: | + rustup update + cargo check --examples --release + + build_ios: + name: Build iOS + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 + - run: | + rustup update + rustup target add aarch64-apple-ios x86_64-apple-ios + cargo install cargo-lipo + cargo lipo --release + + publish_winrt_bindings: + name: Publish winrt_bindings + runs-on: windows-latest + needs: [build_windows] + env: + CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} + steps: + - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 + - run: | + choco install -y llvm + rustup update + cargo login $CARGO_TOKEN + cd winrt_bindings + cargo package + cargo publish || true + + publish: + name: Publish + runs-on: ubuntu-latest + needs: [build_linux, build_web, build_windows, build_macos, build_ios] + env: + CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} + steps: + - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 + - run: | + sudo apt-get update + sudo apt-get install -y libspeechd-dev + rustup update + cargo login $CARGO_TOKEN + cargo publish diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b6871d5..9e052b8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 - run: | sudo apt-get update sudo apt-get install -y libspeechd-dev @@ -21,6 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 - run: | rustup update rustup target add wasm32-unknown-unknown @@ -31,6 +33,7 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 - run: | rustup update cargo check --examples --release --all-features @@ -40,6 +43,7 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 - run: | rustup update cargo check --examples --release @@ -49,6 +53,7 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 - run: | rustup update rustup target add aarch64-apple-ios x86_64-apple-ios From b6ef11b60fa802f9b6cb19dd1dc9c42690f26234 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 15:00:37 -0600 Subject: [PATCH 044/196] Clean up release job. --- .github/workflows/release.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1500594..d51be12 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,8 +17,6 @@ jobs: sudo apt-get install -y libspeechd-dev rustup update cargo check --examples --release - rustup target add wasm32-unknown-unknown - cargo check --examples --release --target wasm32-unknown-unknown build_web: name: Build Web @@ -73,7 +71,6 @@ jobs: - uses: actions/checkout@v2 - uses: Swatinem/rust-cache@v1 - run: | - choco install -y llvm rustup update cargo login $CARGO_TOKEN cd winrt_bindings From 2c73c75e00a0aade6711e634a218e317d8c749fa Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 15:15:43 -0600 Subject: [PATCH 045/196] Use Matrix build. --- .github/workflows/test.yml | 35 +++++++++-------------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9e052b8..e35fb20 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,17 +5,20 @@ on: pull_request: jobs: - build_linux: - name: Build Linux - runs-on: ubuntu-latest + build: + name: Build + strategy: + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - uses: Swatinem/rust-cache@v1 + - run: sudo apt-get update; sudo apt-get install -y libspeechd-dev + if: ${{ runner.os == 'Linux' }} - run: | - sudo apt-get update - sudo apt-get install -y libspeechd-dev rustup update - cargo check --examples --release + cargo check --examples --release --all-features build_web: name: Build Web @@ -28,26 +31,6 @@ jobs: rustup target add wasm32-unknown-unknown cargo check --examples --release --target wasm32-unknown-unknown - build_windows: - name: Build Windows - runs-on: windows-latest - steps: - - uses: actions/checkout@v2 - - uses: Swatinem/rust-cache@v1 - - run: | - rustup update - cargo check --examples --release --all-features - - build_macos: - name: Build MacOS - runs-on: macos-latest - steps: - - uses: actions/checkout@v2 - - uses: Swatinem/rust-cache@v1 - - run: | - rustup update - cargo check --examples --release - build_ios: name: Build iOS runs-on: macos-latest From 31571621924d478d45c96a740c3f69be4c98b7cf Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 15:50:22 -0600 Subject: [PATCH 046/196] Use Rust actions to (hopefully) speed things up. --- .github/workflows/test.yml | 47 ++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e35fb20..26c6b60 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,8 +5,8 @@ on: pull_request: jobs: - build: - name: Build + check: + name: Check strategy: matrix: os: [windows-latest, ubuntu-latest, macos-latest] @@ -16,9 +16,15 @@ jobs: - uses: Swatinem/rust-cache@v1 - run: sudo apt-get update; sudo apt-get install -y libspeechd-dev if: ${{ runner.os == 'Linux' }} - - run: | - rustup update - cargo check --examples --release --all-features + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - uses: actions-rs/cargo@v1 + with: + command: check + args: --all-features --examples build_web: name: Build Web @@ -26,10 +32,16 @@ jobs: steps: - uses: actions/checkout@v2 - uses: Swatinem/rust-cache@v1 - - run: | - rustup update - rustup target add wasm32-unknown-unknown - cargo check --examples --release --target wasm32-unknown-unknown + - uses: actions-rs/toolchain@v1 + with: + target: wasm32-unknown-unknown + profile: minimal + toolchain: stable + override: true + - uses: actions-rs/cargo@v1 + with: + command: check + args: --all-features --examples build_ios: name: Build iOS @@ -37,8 +49,15 @@ jobs: steps: - uses: actions/checkout@v2 - uses: Swatinem/rust-cache@v1 - - run: | - rustup update - rustup target add aarch64-apple-ios x86_64-apple-ios - cargo install cargo-lipo - cargo lipo --release + - uses: actions-rs/toolchain@v1 + with: + target: aarch64-apple-ios x86_64-apple-ios + profile: minimal + toolchain: stable + - uses: actions-rs/install@v0.1 + with: + crate: cargo-lipo + - uses: actions-rs/cargo@v1 + with: + command: lipo + args: --all-features --examples From f37133841a1f0df4815b3445bf6c04c7cc0572a1 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 16:00:02 -0600 Subject: [PATCH 047/196] Fix warnings. --- examples/ramble.rs | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/examples/ramble.rs b/examples/ramble.rs index 36265e7..85bde46 100644 --- a/examples/ramble.rs +++ b/examples/ramble.rs @@ -1,13 +1,5 @@ -use std::io; use std::{thread, time}; -#[cfg(target_os = "macos")] -use cocoa_foundation::base::id; -#[cfg(target_os = "macos")] -use cocoa_foundation::foundation::NSRunLoop; -#[cfg(target_os = "macos")] -use objc::{msg_send, sel, sel_impl}; - use tts::*; fn main() -> Result<(), Error> { @@ -15,21 +7,9 @@ fn main() -> Result<(), Error> { let mut tts = TTS::default()?; let mut phrase = 1; loop { - tts.speak(format!("Phrase {}", phrase), false); + tts.speak(format!("Phrase {}", phrase), false)?; let time = time::Duration::from_secs(5); thread::sleep(time); phrase += 1; } - let mut _input = String::new(); - // The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context. - // It shouldn't be needed in an app or game that almost certainly has one already. - #[cfg(target_os = "macos")] - { - let run_loop: id = unsafe { NSRunLoop::currentRunLoop() }; - unsafe { - let _: () = msg_send![run_loop, run]; - } - } - io::stdin().read_line(&mut _input)?; - Ok(()) } From d97796fff719fde5ed19a8aa7124638186ee0eb8 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 16:03:03 -0600 Subject: [PATCH 048/196] Eliminate a Clippy warning. --- src/backends/winrt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 07fd6ab..03d1d02 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -73,7 +73,7 @@ impl WinRT { if let Some(callback) = callbacks.utterance_end.as_mut() { let last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); if let Some(utterance_id) = last_spoken_utterance.get(&id) { - callback(utterance_id.clone()); + callback(*utterance_id); } } } From be96aacd7a2be6cfc378147fce2d2e7c01e1d974 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 16:06:18 -0600 Subject: [PATCH 049/196] Add --target. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 26c6b60..2b18e28 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: check - args: --all-features --examples + args: --all-features --examples --target wasm32-unknown-unknown build_ios: name: Build iOS From 3224cbdf5a8051c11f23093dcb977cd579d2fc2d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 16:10:20 -0600 Subject: [PATCH 050/196] We don't need iOS-specific builds since the macOS checks already handle this. --- .github/workflows/test.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b18e28..dcae437 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,22 +42,3 @@ jobs: with: command: check args: --all-features --examples --target wasm32-unknown-unknown - - build_ios: - name: Build iOS - runs-on: macos-latest - steps: - - uses: actions/checkout@v2 - - uses: Swatinem/rust-cache@v1 - - uses: actions-rs/toolchain@v1 - with: - target: aarch64-apple-ios x86_64-apple-ios - profile: minimal - toolchain: stable - - uses: actions-rs/install@v0.1 - with: - crate: cargo-lipo - - uses: actions-rs/cargo@v1 - with: - command: lipo - args: --all-features --examples From d5f92565e512cc39f95e314d686374a4a62331d8 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 16:16:14 -0600 Subject: [PATCH 051/196] Integrate Clippy. --- .github/workflows/test.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dcae437..9206178 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,6 +25,10 @@ jobs: with: command: check args: --all-features --examples + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features build_web: name: Build Web @@ -42,3 +46,7 @@ jobs: with: command: check args: --all-features --examples --target wasm32-unknown-unknown + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features --target wasm32-unknown-unknown From 2f0ced4eafd21a5bf1bc0662edb17a356d69c687 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 16:22:14 -0600 Subject: [PATCH 052/196] Install components and integrate rustfmt checks. --- .github/workflows/test.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9206178..bbcf5fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,11 +20,16 @@ jobs: with: profile: minimal toolchain: stable + components: rustfmt, clippy override: true - uses: actions-rs/cargo@v1 with: command: check args: --all-features --examples + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -41,11 +46,16 @@ jobs: target: wasm32-unknown-unknown profile: minimal toolchain: stable + components: rustfmt, clippy override: true - uses: actions-rs/cargo@v1 with: command: check args: --all-features --examples --target wasm32-unknown-unknown + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all --target wasm32-unknown-unknown -- --check - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} From f3705a18560ead779bfad01842218676ec8f7321 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 16:28:28 -0600 Subject: [PATCH 053/196] --target doesn't work here. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bbcf5fb..417a56e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,7 +55,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: fmt - args: --all --target wasm32-unknown-unknown -- --check + args: --all -- --check - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} From 80d51e1bffc8d9ebf3550ecca456dee3ce3cf723 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 16:36:33 -0600 Subject: [PATCH 054/196] Fix cargo fmt CI failure. --- build.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build.rs b/build.rs index 1aa98d1..8b1edc2 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,10 @@ fn main() { if std::env::var("TARGET").unwrap().contains("-apple") { println!("cargo:rustc-link-lib=framework=AVFoundation"); - if !std::env::var("CARGO_CFG_TARGET_OS").unwrap().contains("ios") { + if !std::env::var("CARGO_CFG_TARGET_OS") + .unwrap() + .contains("ios") + { println!("cargo:rustc-link-lib=framework=AppKit"); } } From 34db699972fed25f7eefca949b294c44a39f778c Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 16:42:44 -0600 Subject: [PATCH 055/196] Rename build to check. --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 417a56e..3d5cd7d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,8 +35,8 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} args: --all-features - build_web: - name: Build Web + check_web: + name: Check Web runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 From 9fb4f5e71e5103d0ba3881de754ae3fb3f97469a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 17 Nov 2020 16:47:46 -0600 Subject: [PATCH 056/196] Refactor release job. --- .github/workflows/release.yml | 121 ++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 55 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d51be12..87f292d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,89 +6,100 @@ on: - "v*" jobs: - build_linux: - name: Build Linux + check: + name: Check + strategy: + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 + - run: sudo apt-get update; sudo apt-get install -y libspeechd-dev + if: ${{ runner.os == 'Linux' }} + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + components: rustfmt, clippy + override: true + - uses: actions-rs/cargo@v1 + with: + command: check + args: --all-features --examples + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features + + check_web: + name: Check Web runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: Swatinem/rust-cache@v1 - - run: | - sudo apt-get update - sudo apt-get install -y libspeechd-dev - rustup update - cargo check --examples --release - - build_web: - name: Build Web - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: Swatinem/rust-cache@v1 - - run: | - rustup update - rustup target add wasm32-unknown-unknown - cargo check --examples --release --target wasm32-unknown-unknown - - build_windows: - name: Build Windows - runs-on: windows-latest - steps: - - uses: actions/checkout@v2 - - uses: Swatinem/rust-cache@v1 - - run: | - rustup update - cargo check --examples --all-features --release - - build_macos: - name: Build MacOS - runs-on: macos-latest - steps: - - uses: actions/checkout@v2 - - uses: Swatinem/rust-cache@v1 - - run: | - rustup update - cargo check --examples --release - - build_ios: - name: Build iOS - runs-on: macos-latest - steps: - - uses: actions/checkout@v2 - - uses: Swatinem/rust-cache@v1 - - run: | - rustup update - rustup target add aarch64-apple-ios x86_64-apple-ios - cargo install cargo-lipo - cargo lipo --release + - uses: actions-rs/toolchain@v1 + with: + target: wasm32-unknown-unknown + profile: minimal + toolchain: stable + components: rustfmt, clippy + override: true + - uses: actions-rs/cargo@v1 + with: + command: check + args: --all-features --examples --target wasm32-unknown-unknown + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features --target wasm32-unknown-unknown publish_winrt_bindings: name: Publish winrt_bindings runs-on: windows-latest - needs: [build_windows] + needs: [check] env: CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} 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 - run: | - rustup update cargo login $CARGO_TOKEN cd winrt_bindings - cargo package cargo publish || true publish: name: Publish runs-on: ubuntu-latest - needs: [build_linux, build_web, build_windows, build_macos, build_ios] + needs: [check, check_web] env: CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} steps: - uses: actions/checkout@v2 - uses: Swatinem/rust-cache@v1 + - uses: actions-rs/toolchain@v1 + with: + target: wasm32-unknown-unknown + profile: minimal + toolchain: stable + override: true - run: | sudo apt-get update sudo apt-get install -y libspeechd-dev - rustup update cargo login $CARGO_TOKEN cargo publish From fa2903606e67e8c1bb4f47a8c7bcfcc8812a9493 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 18 Nov 2020 08:27:09 -0600 Subject: [PATCH 057/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e671264..a4548ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.11.3" +version = "0.11.4" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 669c94af365d22432b79801096f7137d61610935 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 23 Nov 2020 17:20:55 -0600 Subject: [PATCH 058/196] We don't need autoplay. --- src/backends/winrt.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 03d1d02..a7468a8 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -53,7 +53,6 @@ impl WinRT { info!("Initializing WinRT backend"); let playback_list = MediaPlaybackList::new()?; let player = MediaPlayer::new()?; - player.set_auto_play(true)?; player.set_source(&playback_list)?; let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); let bid = BackendId::WinRT(*backend_id); From 728c409e25341433f96ff6523f44a675a635058a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 25 Nov 2020 09:54:46 -0600 Subject: [PATCH 059/196] Add example for unscientifically measuring latency of TTS. --- examples/latency.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 examples/latency.rs diff --git a/examples/latency.rs b/examples/latency.rs new file mode 100644 index 0000000..819becc --- /dev/null +++ b/examples/latency.rs @@ -0,0 +1,14 @@ +use std::io; + +use tts::*; + +fn main() -> Result<(), Error> { + env_logger::init(); + let mut tts = TTS::default()?; + println!("Press Enter and wait for speech."); + loop { + let mut _input = String::new(); + io::stdin().read_line(&mut _input)?; + tts.speak("Hello, world.", true)?; + } +} From f4952ad13257506690c5bc783db9dc517d4a1cbe Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 25 Nov 2020 10:07:28 -0600 Subject: [PATCH 060/196] UWP tweaks and optimizations. * Initialized TTS `MediaPlayer` in real-time mode. * Set media category to speech. * More aggressively drop locks to prevent deadlocks. * Remove checks of queued items that are no longer necessary. * Made `is_speaking` check both media player state and queued item count. * Return eagerly from `stop` if speech isn't in progress, thus eliminating more locks. --- src/backends/winrt.rs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index a7468a8..512eba4 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -8,7 +8,7 @@ use winrt::*; use tts_winrt_bindings::windows::media::playback::{ CurrentMediaPlaybackItemChangedEventArgs, MediaPlaybackItem, MediaPlaybackList, - MediaPlaybackState, MediaPlayer, + MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory, }; use tts_winrt_bindings::windows::media::speech_synthesis::SpeechSynthesizer; use tts_winrt_bindings::windows::{foundation::TypedEventHandler, media::core::MediaSource}; @@ -53,12 +53,16 @@ impl WinRT { info!("Initializing WinRT backend"); let playback_list = MediaPlaybackList::new()?; let player = MediaPlayer::new()?; + player.set_real_time_playback(true)?; + player.set_audio_category(MediaPlayerAudioCategory::Speech)?; player.set_source(&playback_list)?; let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); let bid = BackendId::WinRT(*backend_id); *backend_id += 1; + drop(backend_id); let mut backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); backend_to_media_player.insert(bid, player.clone()); + drop(backend_to_media_player); player.media_ended(TypedEventHandler::new(|sender: &MediaPlayer, _args| { let source = sender.source()?; let source: MediaPlaybackList = source.try_into()?; @@ -80,6 +84,7 @@ impl WinRT { }))?; let mut backend_to_playback_list = BACKEND_TO_PLAYBACK_LIST.lock().unwrap(); backend_to_playback_list.insert(bid, playback_list.clone()); + drop(backend_to_playback_list); playback_list.current_item_changed(TypedEventHandler::new( |sender: &MediaPlaybackList, args: &CurrentMediaPlaybackItemChangedEventArgs| { let backend_to_playback_list = BACKEND_TO_PLAYBACK_LIST.lock().unwrap(); @@ -155,17 +160,11 @@ impl Backend for WinRT { let content_type = stream.content_type()?; let source = MediaSource::create_from_stream(stream, content_type)?; let item = MediaPlaybackItem::create(source)?; - let item_index = self.playback_list.current_item_index()?; - let item_count = self.playback_list.items()?.size()?; - let state = self.player.playback_session()?.playback_state()?; - if interrupt && state != MediaPlaybackState::Paused { + if interrupt && self.is_speaking()? { self.stop()?; } - if state == MediaPlaybackState::Paused && item_index != 0 && item_index < item_count { - self.playback_list.items()?.clear()?; - } self.playback_list.items()?.append(&item)?; - if !self.is_speaking()? { + if self.player.playback_session()?.playback_state()? != MediaPlaybackState::Playing { self.player.play()?; } let mut uid = NEXT_UTTERANCE_ID.lock().unwrap(); @@ -179,10 +178,13 @@ impl Backend for WinRT { fn stop(&mut self) -> std::result::Result<(), Error> { trace!("stop()"); + if !self.is_speaking()? { + return Ok(()); + } self.playback_list.items()?.clear()?; - let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); let mut callbacks = CALLBACKS.lock().unwrap(); let callbacks = callbacks.get_mut(&self.id).unwrap(); + let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); if let Some(callback) = callbacks.utterance_stop.as_mut() { for mapping in &*mappings { callback(mapping.2); @@ -259,9 +261,10 @@ impl Backend for WinRT { } fn is_speaking(&self) -> std::result::Result { + let item_count = self.playback_list.items()?.size()?; let state = self.player.playback_session()?.playback_state()?; let playing = state == MediaPlaybackState::Playing; - Ok(playing) + Ok(item_count != 0 || playing) } } From 0bdf0fcfd3b444bab7cd12454ac86367ab62d208 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 25 Nov 2020 10:13:17 -0600 Subject: [PATCH 061/196] Account for macOS 11. --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index d305d3b..782f25f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -225,8 +225,9 @@ impl TTS { let version: Vec<&str> = str.split(" ").collect(); let version = version[1]; 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 minor_version >= 14 { + if major_version >= 11 || minor_version >= 14 { TTS::new(Backends::AvFoundation) } else { TTS::new(Backends::AppKit) From 184becfd1a1cf3472753be593072b99384983be0 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 25 Nov 2020 10:16:33 -0600 Subject: [PATCH 062/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a4548ff..460f4a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.11.4" +version = "0.11.5" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From ad67682235c85dc851f4dba81070e77bc08e1c69 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 3 Dec 2020 13:21:24 -0600 Subject: [PATCH 063/196] Implement Send and Sync for UtteranceId on most platforms. --- src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 782f25f..820ccd3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,6 +69,10 @@ pub enum UtteranceId { AvFoundation(id), } +unsafe impl Send for UtteranceId {} + +unsafe impl Sync for UtteranceId {} + pub struct Features { pub stop: bool, pub rate: bool, From 49e8c0e5dc6336ed3dd9b81adb52057521f56ed5 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 3 Dec 2020 13:31:09 -0600 Subject: [PATCH 064/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 460f4a5..7d4f78b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.11.5" +version = "0.12.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From a905439d9cf5eb4c9f523d7858e44b9e7235113f Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 7 Dec 2020 14:58:59 -0600 Subject: [PATCH 065/196] Add strategic backoff in WinRT backend to (hopefully) eliminate a deadlock. --- Cargo.toml | 1 + src/backends/winrt.rs | 30 ++++++++++++++++++++++-------- src/lib.rs | 8 ++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7d4f78b..6e1eb71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ crate-type = ["lib", "cdylib", "staticlib"] use_tolk = ["tolk"] [dependencies] +backoff = "0.2" dyn-clonable = "0.9" lazy_static = "1" log = "0.4" diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 512eba4..4e60abe 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -1,7 +1,8 @@ -#[cfg(windows)] -use std::collections::HashMap; use std::sync::Mutex; +#[cfg(windows)] +use std::{collections::HashMap, time::Duration}; +use backoff::{ExponentialBackoff, Operation}; use lazy_static::lazy_static; use log::{info, trace}; use winrt::*; @@ -133,6 +134,13 @@ impl WinRT { playback_list, }) } + + fn backoff(&self) -> ExponentialBackoff { + ExponentialBackoff { + initial_interval: Duration::from_millis(3), + ..Default::default() + } + } } impl Backend for WinRT { @@ -167,12 +175,18 @@ impl Backend for WinRT { if self.player.playback_session()?.playback_state()? != MediaPlaybackState::Playing { self.player.play()?; } - let mut uid = NEXT_UTTERANCE_ID.lock().unwrap(); - let utterance_id = UtteranceId::WinRT(*uid); - *uid += 1; - drop(uid); - let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); - mappings.push((self.id, item, utterance_id)); + let mut op = || { + let mut uid = NEXT_UTTERANCE_ID.try_lock()?; + let utterance_id = UtteranceId::WinRT(*uid); + *uid += 1; + Ok(utterance_id) + }; + let mut backoff = self.backoff(); + let utterance_id = op.retry(&mut backoff)?; + { + let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); + mappings.push((self.id, item, utterance_id)); + } Ok(Some(utterance_id)) } diff --git a/src/lib.rs b/src/lib.rs index 820ccd3..15504f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,6 +99,8 @@ impl Default for Features { pub enum Error { #[error("IO error: {0}")] IO(#[from] std::io::Error), + #[error("Backoff error")] + Backoff, #[error("Value not received")] NoneError, #[cfg(target_arch = "wasm32")] @@ -113,6 +115,12 @@ pub enum Error { OutOfRange, } +impl From> for Error { + fn from(_: backoff::Error) -> Self { + Error::Backoff + } +} + #[clonable] trait Backend: Clone + std::fmt::Debug { fn id(&self) -> Option; From d8f2b3fb00e9716d4c49911eb721c7cf381ab5e1 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 7 Dec 2020 21:35:07 -0600 Subject: [PATCH 066/196] I'll do my own queuing, MediaPlaybackList is either the wrong tool for the job or way too buggy. --- Cargo.toml | 1 - examples/99bottles.rs | 2 +- src/backends/winrt.rs | 249 +++++++++++++++++++++++------------------- src/lib.rs | 8 -- 4 files changed, 136 insertions(+), 124 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6e1eb71..7d4f78b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ crate-type = ["lib", "cdylib", "staticlib"] use_tolk = ["tolk"] [dependencies] -backoff = "0.2" dyn-clonable = "0.9" lazy_static = "1" log = "0.4" diff --git a/examples/99bottles.rs b/examples/99bottles.rs index 960d675..9371b82 100644 --- a/examples/99bottles.rs +++ b/examples/99bottles.rs @@ -19,7 +19,7 @@ fn main() -> Result<(), Error> { tts.speak(format!("{} bottles of beer,", bottles), false)?; tts.speak("Take one down, pass it around", false)?; tts.speak("Give us a bit to drink this...", false)?; - let time = time::Duration::from_secs(10); + let time = time::Duration::from_secs(15); thread::sleep(time); bottles -= 1; tts.speak(format!("{} bottles of beer on the wall,", bottles), false)?; diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 4e60abe..890ef63 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -1,14 +1,11 @@ -use std::sync::Mutex; #[cfg(windows)] -use std::{collections::HashMap, time::Duration}; +use std::collections::{HashMap, VecDeque}; +use std::sync::Mutex; -use backoff::{ExponentialBackoff, Operation}; use lazy_static::lazy_static; use log::{info, trace}; -use winrt::*; use tts_winrt_bindings::windows::media::playback::{ - CurrentMediaPlaybackItemChangedEventArgs, MediaPlaybackItem, MediaPlaybackList, MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory, }; use tts_winrt_bindings::windows::media::speech_synthesis::SpeechSynthesizer; @@ -27,120 +24,125 @@ pub struct WinRT { id: BackendId, synth: SpeechSynthesizer, player: MediaPlayer, - playback_list: MediaPlaybackList, + rate: f32, + pitch: f32, + volume: f32, +} + +struct Utterance { + id: UtteranceId, + text: String, + rate: f32, + pitch: f32, + volume: f32, } lazy_static! { static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); static ref NEXT_UTTERANCE_ID: Mutex = Mutex::new(0); - static ref UTTERANCE_MAPPINGS: Mutex> = - Mutex::new(Vec::new()); + static ref BACKEND_TO_SPEECH_SYNTHESIZER: Mutex> = { + let v: HashMap = HashMap::new(); + Mutex::new(v) + }; static ref BACKEND_TO_MEDIA_PLAYER: Mutex> = { let v: HashMap = HashMap::new(); Mutex::new(v) }; - static ref BACKEND_TO_PLAYBACK_LIST: Mutex> = { - let v: HashMap = HashMap::new(); - Mutex::new(v) - }; - static ref LAST_SPOKEN_UTTERANCE: Mutex> = { - let v: HashMap = HashMap::new(); - Mutex::new(v) + static ref UTTERANCES: Mutex>> = { + let utterances: HashMap> = HashMap::new(); + Mutex::new(utterances) }; } impl WinRT { pub fn new() -> std::result::Result { info!("Initializing WinRT backend"); - let playback_list = MediaPlaybackList::new()?; + let synth = SpeechSynthesizer::new()?; let player = MediaPlayer::new()?; player.set_real_time_playback(true)?; player.set_audio_category(MediaPlayerAudioCategory::Speech)?; - player.set_source(&playback_list)?; let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); let bid = BackendId::WinRT(*backend_id); *backend_id += 1; drop(backend_id); + { + let mut utterances = UTTERANCES.lock().unwrap(); + utterances.insert(bid, VecDeque::new()); + } let mut backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); backend_to_media_player.insert(bid, player.clone()); drop(backend_to_media_player); - player.media_ended(TypedEventHandler::new(|sender: &MediaPlayer, _args| { - let source = sender.source()?; - let source: MediaPlaybackList = source.try_into()?; - source.items()?.clear()?; - let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); - let id = backend_to_media_player.iter().find(|v| v.1 == sender); - if let Some(id) = id { - let id = id.0; - let mut callbacks = CALLBACKS.lock().unwrap(); - let callbacks = callbacks.get_mut(&id).unwrap(); - if let Some(callback) = callbacks.utterance_end.as_mut() { - let last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); - if let Some(utterance_id) = last_spoken_utterance.get(&id) { - callback(*utterance_id); + let mut backend_to_speech_synthesizer = BACKEND_TO_SPEECH_SYNTHESIZER.lock().unwrap(); + backend_to_speech_synthesizer.insert(bid, synth.clone()); + drop(backend_to_speech_synthesizer); + let bid_clone = bid; + player.media_ended(TypedEventHandler::new( + move |sender: &MediaPlayer, _args| { + let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); + let id = backend_to_media_player.iter().find(|v| v.1 == sender); + if let Some((id, _)) = id { + let mut utterances = UTTERANCES.lock().unwrap(); + if let Some(utterances) = utterances.get_mut(id) { + if let Some(utterance) = utterances.pop_front() { + let mut callbacks = CALLBACKS.lock().unwrap(); + let callbacks = callbacks.get_mut(id).unwrap(); + if let Some(callback) = callbacks.utterance_end.as_mut() { + callback(utterance.id); + } + if let Some(utterance) = utterances.front() { + let backend_to_speech_synthesizer = + BACKEND_TO_SPEECH_SYNTHESIZER.lock().unwrap(); + let id = backend_to_speech_synthesizer + .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())?; + let stream = tts + .synthesize_text_to_stream_async(utterance.text.as_str())? + .get()?; + let content_type = stream.content_type()?; + let source = + MediaSource::create_from_stream(stream, content_type)?; + sender.set_source(source)?; + sender.play()?; + if let Some(callback) = callbacks.utterance_begin.as_mut() { + callback(utterance.id); + } + } + } + } } } - } - Ok(()) - }))?; - let mut backend_to_playback_list = BACKEND_TO_PLAYBACK_LIST.lock().unwrap(); - backend_to_playback_list.insert(bid, playback_list.clone()); - drop(backend_to_playback_list); - playback_list.current_item_changed(TypedEventHandler::new( - |sender: &MediaPlaybackList, args: &CurrentMediaPlaybackItemChangedEventArgs| { - let backend_to_playback_list = BACKEND_TO_PLAYBACK_LIST.lock().unwrap(); - let id = backend_to_playback_list.iter().find(|v| v.1 == sender); + /*let source = sender.source()?; + let source: MediaPlaybackList = source.try_into()?; + source.items()?.clear()?; + let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); + let id = backend_to_media_player.iter().find(|v| v.1 == sender); if let Some(id) = id { let id = id.0; let mut callbacks = CALLBACKS.lock().unwrap(); let callbacks = callbacks.get_mut(&id).unwrap(); - let old_item = args.old_item()?; - if !old_item.is_null() { - let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); - if let Some(callback) = callbacks.utterance_end.as_mut() { - for mapping in &*mappings { - if mapping.1 == old_item { - callback(mapping.2); - } - } - mappings.retain(|v| v.1 != old_item); + if let Some(callback) = callbacks.utterance_end.as_mut() { + let last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); + if let Some(utterance_id) = last_spoken_utterance.get(&id) { + callback(*utterance_id); } } - let new_item = args.new_item()?; - if !new_item.is_null() { - let mut last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); - let mappings = UTTERANCE_MAPPINGS.lock().unwrap(); - for mapping in &*mappings { - if mapping.1 == new_item { - last_spoken_utterance.insert(*id, mapping.2); - } - } - if let Some(callback) = callbacks.utterance_begin.as_mut() { - for mapping in &*mappings { - if mapping.1 == new_item { - callback(mapping.2); - } - } - } - } - } + }*/ Ok(()) }, ))?; Ok(Self { id: bid, - synth: SpeechSynthesizer::new()?, + synth, player, - playback_list, + rate: 1., + pitch: 1., + volume: 1., }) } - - fn backoff(&self) -> ExponentialBackoff { - ExponentialBackoff { - initial_interval: Duration::from_millis(3), - ..Default::default() - } - } } impl Backend for WinRT { @@ -164,28 +166,46 @@ impl Backend for WinRT { text: &str, interrupt: bool, ) -> std::result::Result, Error> { - 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)?; if interrupt && self.is_speaking()? { self.stop()?; } - self.playback_list.items()?.append(&item)?; - if self.player.playback_session()?.playback_state()? != MediaPlaybackState::Playing { - self.player.play()?; - } - let mut op = || { - let mut uid = NEXT_UTTERANCE_ID.try_lock()?; + let utterance_id = { + let mut uid = NEXT_UTTERANCE_ID.lock().unwrap(); let utterance_id = UtteranceId::WinRT(*uid); *uid += 1; - Ok(utterance_id) + utterance_id }; - let mut backoff = self.backoff(); - let utterance_id = op.retry(&mut backoff)?; + let mut no_utterances = false; { - let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); - mappings.push((self.id, item, utterance_id)); + let mut utterances = UTTERANCES.lock().unwrap(); + if let Some(utterances) = utterances.get_mut(&self.id) { + no_utterances = utterances.is_empty(); + let utterance = Utterance { + id: utterance_id, + text: text.into(), + rate: self.rate, + pitch: self.pitch, + volume: self.volume, + }; + 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()?; + let mut callbacks = CALLBACKS.lock().unwrap(); + let callbacks = callbacks.get_mut(&self.id).unwrap(); + if let Some(callback) = callbacks.utterance_begin.as_mut() { + callback(utterance_id); + } } Ok(Some(utterance_id)) } @@ -195,16 +215,20 @@ impl Backend for WinRT { if !self.is_speaking()? { return Ok(()); } - self.playback_list.items()?.clear()?; - let mut callbacks = CALLBACKS.lock().unwrap(); - let callbacks = callbacks.get_mut(&self.id).unwrap(); - let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); - if let Some(callback) = callbacks.utterance_stop.as_mut() { - for mapping in &*mappings { - callback(mapping.2); + let mut utterances = UTTERANCES.lock().unwrap(); + if let Some(utterances) = utterances.get(&self.id) { + let mut callbacks = CALLBACKS.lock().unwrap(); + let callbacks = callbacks.get_mut(&self.id).unwrap(); + if let Some(callback) = callbacks.utterance_stop.as_mut() { + for utterance in utterances { + callback(utterance.id); + } } } - mappings.retain(|v| v.0 != self.id); + if let Some(utterances) = utterances.get_mut(&self.id) { + utterances.clear(); + } + self.player.pause()?; Ok(()) } @@ -226,7 +250,7 @@ impl Backend for WinRT { } fn set_rate(&mut self, rate: f32) -> std::result::Result<(), Error> { - self.synth.options()?.set_speaking_rate(rate.into())?; + self.rate = rate; Ok(()) } @@ -248,7 +272,7 @@ impl Backend for WinRT { } fn set_pitch(&mut self, pitch: f32) -> std::result::Result<(), Error> { - self.synth.options()?.set_audio_pitch(pitch.into())?; + self.pitch = pitch; Ok(()) } @@ -270,28 +294,25 @@ impl Backend for WinRT { } fn set_volume(&mut self, volume: f32) -> std::result::Result<(), Error> { - self.synth.options()?.set_audio_volume(volume.into())?; + self.volume = volume; Ok(()) } fn is_speaking(&self) -> std::result::Result { - let item_count = self.playback_list.items()?.size()?; - let state = self.player.playback_session()?.playback_state()?; - let playing = state == MediaPlaybackState::Playing; - Ok(item_count != 0 || playing) + let utterances = UTTERANCES.lock().unwrap(); + let utterances = utterances.get(&self.id).unwrap(); + Ok(!utterances.is_empty()) } } impl Drop for WinRT { fn drop(&mut self) { let id = self.id; - let mut backend_to_playback_list = BACKEND_TO_PLAYBACK_LIST.lock().unwrap(); - backend_to_playback_list.remove(&id); let mut backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); backend_to_media_player.remove(&id); - let mut last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); - last_spoken_utterance.remove(&id); - let mut mappings = UTTERANCE_MAPPINGS.lock().unwrap(); - mappings.retain(|v| v.0 != id); + let mut backend_to_speech_synthesizer = BACKEND_TO_SPEECH_SYNTHESIZER.lock().unwrap(); + backend_to_speech_synthesizer.remove(&id); + let mut utterances = UTTERANCES.lock().unwrap(); + utterances.remove(&id); } } diff --git a/src/lib.rs b/src/lib.rs index 15504f6..820ccd3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,8 +99,6 @@ impl Default for Features { pub enum Error { #[error("IO error: {0}")] IO(#[from] std::io::Error), - #[error("Backoff error")] - Backoff, #[error("Value not received")] NoneError, #[cfg(target_arch = "wasm32")] @@ -115,12 +113,6 @@ pub enum Error { OutOfRange, } -impl From> for Error { - fn from(_: backoff::Error) -> Self { - Error::Backoff - } -} - #[clonable] trait Backend: Clone + std::fmt::Debug { fn id(&self) -> Option; From 22007fbf7996dc71243d34595a611247f96a919d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 7 Dec 2020 21:46:46 -0600 Subject: [PATCH 067/196] Bump version. --- Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7d4f78b..e896672 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.12.0" +version = "0.12.1" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -12,7 +12,6 @@ edition = "2018" crate-type = ["lib", "cdylib", "staticlib"] [features] - use_tolk = ["tolk"] [dependencies] From 6d1744735083b24cb481dea854644916a506cdf3 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 7 Dec 2020 22:39:30 -0600 Subject: [PATCH 068/196] Remove unnecessary dependencies and bump versions. --- Cargo.toml | 4 ++-- winrt_bindings/Cargo.toml | 26 +++++++++++++------------- winrt_bindings/build.rs | 24 ++++++++++++------------ 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e896672..fd7424c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.12.1" +version = "0.12.2" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -26,7 +26,7 @@ env_logger = "0.8" [target.'cfg(windows)'.dependencies] tolk = { version = "0.3", optional = true } winrt = "0.7" -tts_winrt_bindings = { version = "0.1", path="winrt_bindings" } +tts_winrt_bindings = { version = "0.2", path="winrt_bindings" } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.7" diff --git a/winrt_bindings/Cargo.toml b/winrt_bindings/Cargo.toml index 10e797e..c0afe2c 100644 --- a/winrt_bindings/Cargo.toml +++ b/winrt_bindings/Cargo.toml @@ -1,13 +1,13 @@ -[package] -name = "tts_winrt_bindings" -version = "0.1.0" -authors = ["Nolan Darilek "] -description = "Internal crate used by `tts`" -license = "MIT" -edition = "2018" - -[dependencies] -winrt = "0.7" - -[build-dependencies] -winrt = "0.7" +[package] +name = "tts_winrt_bindings" +version = "0.2.0" +authors = ["Nolan Darilek "] +description = "Internal crate used by `tts`" +license = "MIT" +edition = "2018" + +[dependencies] +winrt = "0.7" + +[build-dependencies] +winrt = "0.7" diff --git a/winrt_bindings/build.rs b/winrt_bindings/build.rs index 9a1eb63..2cd927d 100644 --- a/winrt_bindings/build.rs +++ b/winrt_bindings/build.rs @@ -1,12 +1,12 @@ -winrt::build!( - dependencies - os - types - windows::media::core::MediaSource - windows::media::playback::{MediaPlaybackItem, MediaPlaybackList, MediaPlaybackState, MediaPlayer} - windows::media::speech_synthesis::SpeechSynthesizer -); - -fn main() { - build(); -} +winrt::build!( + dependencies + os + types + windows::media::core::MediaSource + windows::media::playback::{MediaPlaybackState, MediaPlayer} + windows::media::speech_synthesis::SpeechSynthesizer +); + +fn main() { + build(); +} From 5849e340c96d349606ed7a600f25754957ed85bd Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Sun, 27 Dec 2020 09:41:11 -0600 Subject: [PATCH 069/196] Add initial Android stubs. --- Cargo.toml | 3 ++ src/backends/android.rs | 114 ++++++++++++++++++++++++++++++++++++++++ src/backends/mod.rs | 6 +++ src/lib.rs | 8 +++ 4 files changed, 131 insertions(+) create mode 100644 src/backends/android.rs diff --git a/Cargo.toml b/Cargo.toml index fd7424c..16cfdc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,3 +39,6 @@ objc = "0.2" [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" \ No newline at end of file diff --git a/src/backends/android.rs b/src/backends/android.rs new file mode 100644 index 0000000..a2d1fc4 --- /dev/null +++ b/src/backends/android.rs @@ -0,0 +1,114 @@ +#[cfg(target_os = "android")] +use std::sync::Mutex; + +use lazy_static::lazy_static; +use log::info; + +use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; + +lazy_static! { + static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); +} + +#[derive(Clone, Debug)] +pub(crate) struct Android(BackendId); + +impl Android { + pub(crate) fn new() -> Self { + info!("Initializing Android backend"); + let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); + let bid = BackendId::Android(*backend_id); + *backend_id += 1; + Self(bid) + } +} + +impl Backend for Android { + fn id(&self) -> Option { + Some(self.0) + } + + fn supported_features(&self) -> Features { + Features { + stop: false, + rate: false, + pitch: false, + volume: false, + is_speaking: false, + utterance_callbacks: false, + } + } + + fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error> { + println!("Speaking {}, {:?}", text, interrupt); + Ok(None) + } + + fn stop(&mut self) -> Result<(), Error> { + todo!() + } + + fn min_rate(&self) -> f32 { + todo!() + } + + fn max_rate(&self) -> f32 { + todo!() + } + + fn normal_rate(&self) -> f32 { + todo!() + } + + fn get_rate(&self) -> Result { + todo!() + } + + fn set_rate(&mut self, rate: f32) -> Result<(), Error> { + todo!() + } + + fn min_pitch(&self) -> f32 { + todo!() + } + + fn max_pitch(&self) -> f32 { + todo!() + } + + fn normal_pitch(&self) -> f32 { + todo!() + } + + fn get_pitch(&self) -> Result { + todo!() + } + + fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> { + todo!() + } + + fn min_volume(&self) -> f32 { + todo!() + } + + fn max_volume(&self) -> f32 { + todo!() + } + + fn normal_volume(&self) -> f32 { + todo!() + } + + fn get_volume(&self) -> Result { + todo!() + } + + fn set_volume(&mut self, volume: f32) -> Result<(), Error> { + todo!() + } + + fn is_speaking(&self) -> Result { + todo!() + } +} diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 408e5bf..4e61063 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -16,6 +16,9 @@ mod appkit; #[cfg(any(target_os = "macos", target_os = "ios"))] mod av_foundation; +#[cfg(target_os = "android")] +mod android; + #[cfg(target_os = "linux")] pub(crate) use self::speech_dispatcher::*; @@ -30,3 +33,6 @@ pub(crate) use self::appkit::*; #[cfg(any(target_os = "macos", target_os = "ios"))] pub(crate) use self::av_foundation::*; + +#[cfg(target_os = "android")] +pub(crate) use self::android::*; diff --git a/src/lib.rs b/src/lib.rs index 820ccd3..7bebbeb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,8 @@ pub enum Backends { AppKit, #[cfg(any(target_os = "macos", target_os = "ios"))] AvFoundation, + #[cfg(target_os = "android")] + Android, } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -55,6 +57,8 @@ pub enum BackendId { WinRT(u64), #[cfg(any(target_os = "macos", target_os = "ios"))] AvFoundation(u64), + #[cfg(target_os = "android")] + Android(u64), } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -193,6 +197,8 @@ 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()))), + #[cfg(target_os = "android")] + Backends::Android => Ok(TTS(Box::new(backends::Android::new()))), }; if let Ok(backend) = backend { if let Some(id) = backend.0.id() { @@ -239,6 +245,8 @@ impl TTS { }; #[cfg(target_os = "ios")] let tts = TTS::new(Backends::AvFoundation); + #[cfg(target_os = "android")] + let tts = TTS::new(Backends::Android); tts } From 187cd71eeb2df2c2d040074670b818e03af6cf1f Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Sun, 27 Dec 2020 10:42:41 -0600 Subject: [PATCH 070/196] Add Android example. --- Cargo.toml | 7 ++- examples/hello_world_android.rs | 89 +++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 examples/hello_world_android.rs diff --git a/Cargo.toml b/Cargo.toml index 16cfdc2..7682ed5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,4 +41,9 @@ 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" \ No newline at end of file +jni = "0.18" +ndk-glue = "0.2" + +[[example]] +name = "hello_world_android" +crate-type = ["cdylib"] diff --git a/examples/hello_world_android.rs b/examples/hello_world_android.rs new file mode 100644 index 0000000..8243936 --- /dev/null +++ b/examples/hello_world_android.rs @@ -0,0 +1,89 @@ +use std::io; + +#[cfg(target_os = "macos")] +use cocoa_foundation::base::id; +#[cfg(target_os = "macos")] +use cocoa_foundation::foundation::NSRunLoop; +#[cfg(target_os = "macos")] +use objc::{msg_send, sel, sel_impl}; + +use tts::*; + +// Use a separate function so the same examples run everywhere. +fn run() -> 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(Box::new(|utterance| { + println!("Started speaking {:?}", utterance) + })))?; + tts.on_utterance_end(Some(Box::new(|utterance| { + println!("Finished speaking {:?}", utterance) + })))?; + tts.on_utterance_stop(Some(Box::new(|utterance| { + println!("Stopped speaking {:?}", utterance) + })))?; + } + let Features { is_speaking, .. } = tts.supported_features(); + if is_speaking { + println!("Are we speaking? {}", tts.is_speaking()?); + } + tts.speak("Hello, world.", false)?; + let Features { rate, .. } = tts.supported_features(); + if rate { + let original_rate = tts.get_rate()?; + tts.speak(format!("Current rate: {}", original_rate), false)?; + tts.set_rate(tts.max_rate())?; + tts.speak("This is very fast.", false)?; + tts.set_rate(tts.min_rate())?; + tts.speak("This is very slow.", false)?; + tts.set_rate(tts.normal_rate())?; + tts.speak("This is the normal rate.", false)?; + tts.set_rate(original_rate)?; + } + let Features { pitch, .. } = tts.supported_features(); + if pitch { + let original_pitch = tts.get_pitch()?; + tts.set_pitch(tts.max_pitch())?; + tts.speak("This is high-pitch.", false)?; + tts.set_pitch(tts.min_pitch())?; + tts.speak("This is low pitch.", false)?; + tts.set_pitch(tts.normal_pitch())?; + tts.speak("This is normal pitch.", false)?; + tts.set_pitch(original_pitch)?; + } + let Features { volume, .. } = tts.supported_features(); + if volume { + let original_volume = tts.get_volume()?; + tts.set_volume(tts.max_volume())?; + tts.speak("This is loud!", false)?; + tts.set_volume(tts.min_volume())?; + tts.speak("This is quiet.", false)?; + tts.set_volume(tts.normal_volume())?; + tts.speak("This is normal volume.", false)?; + tts.set_volume(original_volume)?; + } + tts.speak("Goodbye.", false)?; + let mut _input = String::new(); + // The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context. + // It shouldn't be needed in an app or game that almost certainly has one already. + #[cfg(target_os = "macos")] + { + let run_loop: id = unsafe { NSRunLoop::currentRunLoop() }; + unsafe { + let _: () = msg_send![run_loop, run]; + } + } + io::stdin().read_line(&mut _input)?; + Ok(()) +} + +#[cfg(target_os = "android")] +#[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))] +fn main() { + run().expect("Failed to run"); +} From cb91760468b613f462744f5d9fbaf3e69988bf3a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 28 Dec 2020 05:39:20 -0600 Subject: [PATCH 071/196] Set Android API versions, and add Makefile.toml convenience script for getting Android logs. --- Cargo.toml | 4 ++++ Makefile.toml | 3 +++ 2 files changed, 7 insertions(+) create mode 100644 Makefile.toml diff --git a/Cargo.toml b/Cargo.toml index 7682ed5..f1ec87f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,10 @@ web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "Spee jni = "0.18" ndk-glue = "0.2" +[package.metadata.android] +target_sdk_version = 29 +min_sdk_version = 21 + [[example]] name = "hello_world_android" crate-type = ["cdylib"] diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 0000000..7673a80 --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,3 @@ +[tasks.log-android] +command = "adb" +args = ["logcat", "RustStdoutStderr:D", "*:S"] \ No newline at end of file From fc204319164b610ab78757a01ca45b785bbfbc4a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 29 Dec 2020 11:15:24 -0600 Subject: [PATCH 072/196] Refactor Android example to full, self-contained app. --- Cargo.toml | 11 +- examples/android/.gitignore | 16 ++ examples/android/.idea/.gitignore | 3 + examples/android/.idea/compiler.xml | 6 + examples/android/.idea/gradle.xml | 21 +++ examples/android/.idea/jarRepositories.xml | 25 +++ examples/android/.idea/misc.xml | 9 + examples/android/.idea/vcs.xml | 6 + examples/android/app/.gitignore | 1 + examples/android/app/build.gradle | 55 ++++++ examples/android/app/proguard-rules.pro | 21 +++ examples/android/app/src/main/.gitignore | 1 + .../android/app/src/main/AndroidManifest.xml | 13 ++ .../app/src/main/java/rs/tts/MainActivity.kt | 8 + .../app/src/main/res/values/strings.xml | 3 + examples/android/build.gradle | 29 +++ examples/android/cargo.toml | 14 ++ examples/android/gradle.properties | 21 +++ .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + examples/android/gradlew | 172 ++++++++++++++++++ examples/android/gradlew.bat | 84 +++++++++ examples/android/settings.gradle | 1 + .../src/lib.rs} | 25 +-- src/backends/android.rs | 8 +- src/lib.rs | 8 +- 26 files changed, 530 insertions(+), 37 deletions(-) create mode 100644 examples/android/.gitignore create mode 100644 examples/android/.idea/.gitignore create mode 100644 examples/android/.idea/compiler.xml create mode 100644 examples/android/.idea/gradle.xml create mode 100644 examples/android/.idea/jarRepositories.xml create mode 100644 examples/android/.idea/misc.xml create mode 100644 examples/android/.idea/vcs.xml create mode 100644 examples/android/app/.gitignore create mode 100644 examples/android/app/build.gradle create mode 100644 examples/android/app/proguard-rules.pro create mode 100644 examples/android/app/src/main/.gitignore create mode 100644 examples/android/app/src/main/AndroidManifest.xml create mode 100644 examples/android/app/src/main/java/rs/tts/MainActivity.kt create mode 100644 examples/android/app/src/main/res/values/strings.xml create mode 100644 examples/android/build.gradle create mode 100644 examples/android/cargo.toml create mode 100644 examples/android/gradle.properties create mode 100644 examples/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 examples/android/gradlew create mode 100644 examples/android/gradlew.bat create mode 100644 examples/android/settings.gradle rename examples/{hello_world_android.rs => android/src/lib.rs} (74%) diff --git a/Cargo.toml b/Cargo.toml index f1ec87f..16cfdc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,13 +41,4 @@ 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" - -[package.metadata.android] -target_sdk_version = 29 -min_sdk_version = 21 - -[[example]] -name = "hello_world_android" -crate-type = ["cdylib"] +jni = "0.18" \ No newline at end of file diff --git a/examples/android/.gitignore b/examples/android/.gitignore new file mode 100644 index 0000000..26fa1c5 --- /dev/null +++ b/examples/android/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +Cargo.lock diff --git a/examples/android/.idea/.gitignore b/examples/android/.idea/.gitignore new file mode 100644 index 0000000..eaf91e2 --- /dev/null +++ b/examples/android/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/examples/android/.idea/compiler.xml b/examples/android/.idea/compiler.xml new file mode 100644 index 0000000..851fce6 --- /dev/null +++ b/examples/android/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/examples/android/.idea/gradle.xml b/examples/android/.idea/gradle.xml new file mode 100644 index 0000000..c272f95 --- /dev/null +++ b/examples/android/.idea/gradle.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/examples/android/.idea/jarRepositories.xml b/examples/android/.idea/jarRepositories.xml new file mode 100644 index 0000000..17c8136 --- /dev/null +++ b/examples/android/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/android/.idea/misc.xml b/examples/android/.idea/misc.xml new file mode 100644 index 0000000..673075e --- /dev/null +++ b/examples/android/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/examples/android/.idea/vcs.xml b/examples/android/.idea/vcs.xml new file mode 100644 index 0000000..c8ade07 --- /dev/null +++ b/examples/android/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/examples/android/app/.gitignore b/examples/android/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/examples/android/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/examples/android/app/build.gradle b/examples/android/app/build.gradle new file mode 100644 index 0000000..8cefd06 --- /dev/null +++ b/examples/android/app/build.gradle @@ -0,0 +1,55 @@ +plugins { + id "com.android.application" + id "kotlin-android" +} + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + defaultConfig { + applicationId "rs.tts" + minSdkVersion 21 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "androidx.core:core-ktx:1.2.0" + implementation "com.google.android.material:material:1.1.0" + implementation "androidx.constraintlayout:constraintlayout:1.1.3" +} + +apply plugin: "com.github.willir.rust.cargo-ndk-android" + +cargoNdk { + module = "." +} + +project.afterEvaluate { + android.applicationVariants.all { variant -> + task "run${variant.name.capitalize()}"(type: Exec, dependsOn: "install${variant.name.capitalize()}", group: "run") { + commandLine = ["adb", "shell", "monkey", "-p", variant.applicationId + " 1"] + doLast { + println "Launching ${variant.applicationId}" + } + } + } +} diff --git a/examples/android/app/proguard-rules.pro b/examples/android/app/proguard-rules.pro new file mode 100644 index 0000000..64b4a05 --- /dev/null +++ b/examples/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/examples/android/app/src/main/.gitignore b/examples/android/app/src/main/.gitignore new file mode 100644 index 0000000..e1230da --- /dev/null +++ b/examples/android/app/src/main/.gitignore @@ -0,0 +1 @@ +jniLibs diff --git a/examples/android/app/src/main/AndroidManifest.xml b/examples/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..86dda62 --- /dev/null +++ b/examples/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/android/app/src/main/java/rs/tts/MainActivity.kt b/examples/android/app/src/main/java/rs/tts/MainActivity.kt new file mode 100644 index 0000000..2b58854 --- /dev/null +++ b/examples/android/app/src/main/java/rs/tts/MainActivity.kt @@ -0,0 +1,8 @@ +package rs.tts + +import android.app.NativeActivity +import android.speech.tts.TextToSpeech.OnInitListener + +class MainActivity : NativeActivity(), OnInitListener { + override fun onInit(status:Int) {} +} \ No newline at end of file diff --git a/examples/android/app/src/main/res/values/strings.xml b/examples/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..524d08f --- /dev/null +++ b/examples/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + TTS-RS + \ No newline at end of file diff --git a/examples/android/build.gradle b/examples/android/build.gradle new file mode 100644 index 0000000..dd9a15c --- /dev/null +++ b/examples/android/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + ext.kotlin_version = "1.3.72" + repositories { + google() + jcenter() + maven { + url "https://plugins.gradle.org/m2/" + } + } + dependencies { + classpath "com.android.tools.build:gradle:4.1.1" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "gradle.plugin.com.github.willir.rust:plugin:0.3.3" + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/examples/android/cargo.toml b/examples/android/cargo.toml new file mode 100644 index 0000000..a7ff289 --- /dev/null +++ b/examples/android/cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "hello_world" +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 + +[lib] +crate-type = ["dylib"] + +[dependencies] +ndk-glue = "0.2" +tts = { path = "../.." } \ No newline at end of file diff --git a/examples/android/gradle.properties b/examples/android/gradle.properties new file mode 100644 index 0000000..a38cbbb --- /dev/null +++ b/examples/android/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official \ No newline at end of file diff --git a/examples/android/gradle/wrapper/gradle-wrapper.jar b/examples/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 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

<5G2M1WZP5 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 F{|7XC?P&l2 literal 0 HcmV?d00001 diff --git a/examples/android/gradle/wrapper/gradle-wrapper.properties b/examples/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..7b15717 --- /dev/null +++ b/examples/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Dec 28 17:32:22 CST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip diff --git a/examples/android/gradlew b/examples/android/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/examples/android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/examples/android/gradlew.bat b/examples/android/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/examples/android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/android/settings.gradle b/examples/android/settings.gradle new file mode 100644 index 0000000..1fb859b --- /dev/null +++ b/examples/android/settings.gradle @@ -0,0 +1 @@ +include ":app" \ No newline at end of file diff --git a/examples/hello_world_android.rs b/examples/android/src/lib.rs similarity index 74% rename from examples/hello_world_android.rs rename to examples/android/src/lib.rs index 8243936..8a256d6 100644 --- a/examples/hello_world_android.rs +++ b/examples/android/src/lib.rs @@ -1,17 +1,6 @@ -use std::io; - -#[cfg(target_os = "macos")] -use cocoa_foundation::base::id; -#[cfg(target_os = "macos")] -use cocoa_foundation::foundation::NSRunLoop; -#[cfg(target_os = "macos")] -use objc::{msg_send, sel, sel_impl}; - use tts::*; -// Use a separate function so the same examples run everywhere. fn run() -> Result<(), Error> { - env_logger::init(); let mut tts = TTS::default()?; let Features { utterance_callbacks, @@ -68,22 +57,10 @@ fn run() -> Result<(), Error> { tts.set_volume(original_volume)?; } tts.speak("Goodbye.", false)?; - let mut _input = String::new(); - // The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context. - // It shouldn't be needed in an app or game that almost certainly has one already. - #[cfg(target_os = "macos")] - { - let run_loop: id = unsafe { NSRunLoop::currentRunLoop() }; - unsafe { - let _: () = msg_send![run_loop, run]; - } - } - io::stdin().read_line(&mut _input)?; Ok(()) } -#[cfg(target_os = "android")] #[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))] -fn main() { +pub fn main() { run().expect("Failed to run"); } diff --git a/src/backends/android.rs b/src/backends/android.rs index a2d1fc4..9e3f842 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -14,12 +14,16 @@ lazy_static! { pub(crate) struct Android(BackendId); impl Android { - pub(crate) fn new() -> Self { + pub(crate) fn new() -> Result { info!("Initializing Android backend"); let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); let bid = BackendId::Android(*backend_id); *backend_id += 1; - Self(bid) + let native_activity = ndk_glue::native_activity(); + let vm_ptr = native_activity.vm(); + let vm = unsafe { jni::JavaVM::from_raw(vm_ptr) }?; + let env = vm.attach_current_thread()?; + Ok(Self(bid)) } } diff --git a/src/lib.rs b/src/lib.rs index 7bebbeb..ed32cf9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,6 +115,9 @@ pub enum Error { UnsupportedFeature, #[error("Out of range")] OutOfRange, + #[cfg(target_os = "android")] + #[error("JNI error: [0])]")] + JNI(#[from] jni::errors::Error), } #[clonable] @@ -198,7 +201,10 @@ impl TTS { #[cfg(any(target_os = "macos", target_os = "ios"))] Backends::AvFoundation => Ok(TTS(Box::new(backends::AvFoundation::new()))), #[cfg(target_os = "android")] - Backends::Android => Ok(TTS(Box::new(backends::Android::new()))), + Backends::Android => { + let tts = backends::Android::new()?; + Ok(TTS(Box::new(tts))) + } }; if let Ok(backend) = backend { if let Some(id) = backend.0.id() { From 965bea0adf16acf5a34977822c9490015d236fdf Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 29 Dec 2020 14:10:39 -0600 Subject: [PATCH 073/196] TTS seems to initialize now. --- Cargo.toml | 3 ++- .../app/src/main/java/rs/tts/MainActivity.kt | 9 ++++++++- examples/android/src/lib.rs | 1 + src/backends/android.rs | 19 +++++++++++++++---- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 16cfdc2..9624cd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,4 +41,5 @@ 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" \ No newline at end of file +jni = "0.18" +ndk-glue = "0.2" \ No newline at end of file diff --git a/examples/android/app/src/main/java/rs/tts/MainActivity.kt b/examples/android/app/src/main/java/rs/tts/MainActivity.kt index 2b58854..6b75440 100644 --- a/examples/android/app/src/main/java/rs/tts/MainActivity.kt +++ b/examples/android/app/src/main/java/rs/tts/MainActivity.kt @@ -1,8 +1,15 @@ package rs.tts import android.app.NativeActivity +import android.speech.tts.TextToSpeech import android.speech.tts.TextToSpeech.OnInitListener class MainActivity : NativeActivity(), OnInitListener { - override fun onInit(status:Int) {} + override fun onInit(status:Int) { + if(status == TextToSpeech.SUCCESS) { + println("Successfully initialized TTS!") + } else { + println("Failed to initialize TTS.") + } + } } \ No newline at end of file diff --git a/examples/android/src/lib.rs b/examples/android/src/lib.rs index 8a256d6..87014cc 100644 --- a/examples/android/src/lib.rs +++ b/examples/android/src/lib.rs @@ -62,5 +62,6 @@ fn run() -> Result<(), Error> { #[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))] pub fn main() { + println!("xxxx In library"); run().expect("Failed to run"); } diff --git a/src/backends/android.rs b/src/backends/android.rs index 9e3f842..1caee9a 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -1,6 +1,7 @@ #[cfg(target_os = "android")] use std::sync::Mutex; +use jni::objects::JValue; use lazy_static::lazy_static; use log::info; @@ -11,25 +12,35 @@ lazy_static! { } #[derive(Clone, Debug)] -pub(crate) struct Android(BackendId); +pub(crate) struct Android { + id: BackendId, +} impl Android { pub(crate) fn new() -> Result { info!("Initializing Android backend"); let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); - let bid = BackendId::Android(*backend_id); + let id = BackendId::Android(*backend_id); *backend_id += 1; let native_activity = ndk_glue::native_activity(); let vm_ptr = native_activity.vm(); let vm = unsafe { jni::JavaVM::from_raw(vm_ptr) }?; let env = vm.attach_current_thread()?; - Ok(Self(bid)) + let tts = env.new_object( + "android/speech/tts/TextToSpeech", + "(Landroid/content/Context;Landroid/speech/tts/TextToSpeech$OnInitListener;)V", + &[ + native_activity.activity().into(), + native_activity.activity().into(), + ], + )?; + Ok(Self { id }) } } impl Backend for Android { fn id(&self) -> Option { - Some(self.0) + Some(self.id) } fn supported_features(&self) -> Features { From da8260cba8ecce6626b52088af3c5458d22f641a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 29 Dec 2020 15:45:56 -0600 Subject: [PATCH 074/196] Store the TTS object in the struct. --- src/backends/android.rs | 41 +++++++++++++++++++++++++++-------------- src/lib.rs | 2 +- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/backends/android.rs b/src/backends/android.rs index 1caee9a..6b4e66a 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -1,7 +1,9 @@ #[cfg(target_os = "android")] +use std::collections::HashMap; use std::sync::Mutex; -use jni::objects::JValue; +use jni::objects::GlobalRef; +use jni::JavaVM; use lazy_static::lazy_static; use log::info; @@ -9,11 +11,13 @@ use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; lazy_static! { static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); + static ref VM: Mutex> = Mutex::new(None); } -#[derive(Clone, Debug)] +#[derive(Clone)] pub(crate) struct Android { id: BackendId, + tts: GlobalRef, } impl Android { @@ -23,18 +27,27 @@ impl Android { let id = BackendId::Android(*backend_id); *backend_id += 1; let native_activity = ndk_glue::native_activity(); - let vm_ptr = native_activity.vm(); - let vm = unsafe { jni::JavaVM::from_raw(vm_ptr) }?; - let env = vm.attach_current_thread()?; - let tts = env.new_object( - "android/speech/tts/TextToSpeech", - "(Landroid/content/Context;Landroid/speech/tts/TextToSpeech$OnInitListener;)V", - &[ - native_activity.activity().into(), - native_activity.activity().into(), - ], - )?; - Ok(Self { id }) + let mut vm = VM.lock().unwrap(); + if vm.is_none() { + let vm_ptr = native_activity.vm(); + let new_vm = unsafe { jni::JavaVM::from_raw(vm_ptr) }?; + *vm = Some(new_vm); + } + if let Some(vm) = &*vm { + let env = vm.attach_current_thread()?; + let tts = env.new_object( + "android/speech/tts/TextToSpeech", + "(Landroid/content/Context;Landroid/speech/tts/TextToSpeech$OnInitListener;)V", + &[ + native_activity.activity().into(), + native_activity.activity().into(), + ], + )?; + let tts = env.new_global_ref(tts)?; + Ok(Self { id, tts }) + } else { + Err(Error::NoneError) + } } } diff --git a/src/lib.rs b/src/lib.rs index ed32cf9..ff0a46d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -121,7 +121,7 @@ pub enum Error { } #[clonable] -trait Backend: Clone + std::fmt::Debug { +trait Backend: Clone { fn id(&self) -> Option; fn supported_features(&self) -> Features; fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error>; From 84926ea1105bdde5d4083cbec1d55e2b98eac112 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 29 Dec 2020 15:47:11 -0600 Subject: [PATCH 075/196] Store the TTS object in the struct. --- src/backends/android.rs | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/backends/android.rs b/src/backends/android.rs index 6b4e66a..2b28d20 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -27,27 +27,19 @@ impl Android { let id = BackendId::Android(*backend_id); *backend_id += 1; let native_activity = ndk_glue::native_activity(); - let mut vm = VM.lock().unwrap(); - if vm.is_none() { - let vm_ptr = native_activity.vm(); - let new_vm = unsafe { jni::JavaVM::from_raw(vm_ptr) }?; - *vm = Some(new_vm); - } - if let Some(vm) = &*vm { - let env = vm.attach_current_thread()?; - let tts = env.new_object( - "android/speech/tts/TextToSpeech", - "(Landroid/content/Context;Landroid/speech/tts/TextToSpeech$OnInitListener;)V", - &[ - native_activity.activity().into(), - native_activity.activity().into(), - ], - )?; - let tts = env.new_global_ref(tts)?; - Ok(Self { id, tts }) - } else { - Err(Error::NoneError) - } + let vm_ptr = native_activity.vm(); + let vm = unsafe { jni::JavaVM::from_raw(vm_ptr) }?; + let env = vm.attach_current_thread()?; + let tts = env.new_object( + "android/speech/tts/TextToSpeech", + "(Landroid/content/Context;Landroid/speech/tts/TextToSpeech$OnInitListener;)V", + &[ + native_activity.activity().into(), + native_activity.activity().into(), + ], + )?; + let tts = env.new_global_ref(tts)?; + Ok(Self { id, tts }) } } From f58f875fdfa73340389c6a8bc3c46c111b6de2ab Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 29 Dec 2020 15:48:18 -0600 Subject: [PATCH 076/196] Guess I don't need a global VM. --- src/backends/android.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/backends/android.rs b/src/backends/android.rs index 2b28d20..518b25e 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -1,9 +1,7 @@ #[cfg(target_os = "android")] -use std::collections::HashMap; use std::sync::Mutex; use jni::objects::GlobalRef; -use jni::JavaVM; use lazy_static::lazy_static; use log::info; @@ -11,7 +9,6 @@ use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; lazy_static! { static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); - static ref VM: Mutex> = Mutex::new(None); } #[derive(Clone)] From 32f57d85785052063170dcb30d279a8b8555613c Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 29 Dec 2020 16:24:08 -0600 Subject: [PATCH 077/196] Speak calls pass to Java, but don't work since something isn't bound to the engine. --- src/backends/android.rs | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/backends/android.rs b/src/backends/android.rs index 518b25e..65fa756 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -1,7 +1,9 @@ #[cfg(target_os = "android")] use std::sync::Mutex; -use jni::objects::GlobalRef; +use jni::objects::{GlobalRef, JObject}; +use jni::JNIEnv; +use jni::JavaVM; use lazy_static::lazy_static; use log::info; @@ -24,8 +26,7 @@ impl Android { let id = BackendId::Android(*backend_id); *backend_id += 1; let native_activity = ndk_glue::native_activity(); - let vm_ptr = native_activity.vm(); - let vm = unsafe { jni::JavaVM::from_raw(vm_ptr) }?; + let vm = Self::vm()?; let env = vm.attach_current_thread()?; let tts = env.new_object( "android/speech/tts/TextToSpeech", @@ -35,9 +36,17 @@ impl Android { native_activity.activity().into(), ], )?; + println!("Creating global ref"); let tts = env.new_global_ref(tts)?; + println!("Returning"); Ok(Self { id, tts }) } + + fn vm() -> Result { + let native_activity = ndk_glue::native_activity(); + let vm_ptr = native_activity.vm(); + unsafe { jni::JavaVM::from_raw(vm_ptr) } + } } impl Backend for Android { @@ -58,6 +67,26 @@ impl Backend for Android { fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error> { println!("Speaking {}, {:?}", text, interrupt); + let vm = Self::vm()?; + println!("Retrieved"); + let env = vm.attach_current_thread()?; + println!("attached"); + let tts = self.tts.as_obj(); + let text = env.new_string(text)?; + let queue_mode = if interrupt { 0 } else { 1 }; + println!("Calling"); + env.call_method( + tts, + "speak", + "(Ljava/lang/CharSequence;ILandroid/os/Bundle;Ljava/lang/String;)I", + &[ + text.into(), + queue_mode.into(), + JObject::null().into(), + JObject::null().into(), + ], + )?; + println!("Returning"); Ok(None) } From 1ac0b919816da48349005b1a07c9062f71d7c4c6 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 29 Dec 2020 19:25:56 -0600 Subject: [PATCH 078/196] Add ugly hack to prove that speech works. --- examples/android/src/lib.rs | 3 +++ src/backends/android.rs | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/android/src/lib.rs b/examples/android/src/lib.rs index 87014cc..da4ef23 100644 --- a/examples/android/src/lib.rs +++ b/examples/android/src/lib.rs @@ -1,7 +1,10 @@ +use std::thread; +use std::time::Duration; use tts::*; fn run() -> Result<(), Error> { let mut tts = TTS::default()?; + thread::sleep(Duration::from_secs(5)); let Features { utterance_callbacks, .. diff --git a/src/backends/android.rs b/src/backends/android.rs index 65fa756..6a2e200 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -2,7 +2,6 @@ use std::sync::Mutex; use jni::objects::{GlobalRef, JObject}; -use jni::JNIEnv; use jni::JavaVM; use lazy_static::lazy_static; use log::info; @@ -27,7 +26,7 @@ impl Android { *backend_id += 1; let native_activity = ndk_glue::native_activity(); let vm = Self::vm()?; - let env = vm.attach_current_thread()?; + let env = vm.attach_current_thread_permanently()?; let tts = env.new_object( "android/speech/tts/TextToSpeech", "(Landroid/content/Context;Landroid/speech/tts/TextToSpeech$OnInitListener;)V", @@ -69,7 +68,7 @@ impl Backend for Android { println!("Speaking {}, {:?}", text, interrupt); let vm = Self::vm()?; println!("Retrieved"); - let env = vm.attach_current_thread()?; + let env = vm.get_env()?; println!("attached"); let tts = self.tts.as_obj(); let text = env.new_string(text)?; From 5634fdb393d120261f5a7bf79de479e96d018602 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 09:23:13 -0600 Subject: [PATCH 079/196] Block initialization until TTS finishes initializing from Android. --- Cargo.toml | 2 +- examples/android/app/build.gradle | 1 + .../app/src/main/java/rs/tts/Bridge.java | 15 +++ .../app/src/main/java/rs/tts/MainActivity.kt | 12 +-- examples/android/cargo.toml | 1 + examples/android/src/lib.rs | 4 - src/backends/android.rs | 98 ++++++++++++++----- 7 files changed, 94 insertions(+), 39 deletions(-) create mode 100644 examples/android/app/src/main/java/rs/tts/Bridge.java diff --git a/Cargo.toml b/Cargo.toml index 9624cd7..e254177 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ exclude = ["*.cfg", "*.yml"] edition = "2018" [lib] -crate-type = ["lib", "cdylib", "staticlib"] +crate-type = ["lib", "dylib", "staticlib"] [features] use_tolk = ["tolk"] diff --git a/examples/android/app/build.gradle b/examples/android/app/build.gradle index 8cefd06..2e99b3c 100644 --- a/examples/android/app/build.gradle +++ b/examples/android/app/build.gradle @@ -33,6 +33,7 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "androidx.core:core-ktx:1.2.0" + implementation "androidx.annotation:annotation:1.1.0" implementation "com.google.android.material:material:1.1.0" implementation "androidx.constraintlayout:constraintlayout:1.1.3" } diff --git a/examples/android/app/src/main/java/rs/tts/Bridge.java b/examples/android/app/src/main/java/rs/tts/Bridge.java new file mode 100644 index 0000000..4850c25 --- /dev/null +++ b/examples/android/app/src/main/java/rs/tts/Bridge.java @@ -0,0 +1,15 @@ +package rs.tts; + +import android.speech.tts.TextToSpeech; + +@androidx.annotation.Keep +public class Bridge implements TextToSpeech.OnInitListener { + public int backendId; + + public Bridge(int backendId) { + this.backendId = backendId; + } + + public native void onInit(int status); + +} \ No newline at end of file diff --git a/examples/android/app/src/main/java/rs/tts/MainActivity.kt b/examples/android/app/src/main/java/rs/tts/MainActivity.kt index 6b75440..0bba51f 100644 --- a/examples/android/app/src/main/java/rs/tts/MainActivity.kt +++ b/examples/android/app/src/main/java/rs/tts/MainActivity.kt @@ -1,15 +1,11 @@ package rs.tts import android.app.NativeActivity -import android.speech.tts.TextToSpeech -import android.speech.tts.TextToSpeech.OnInitListener -class MainActivity : NativeActivity(), OnInitListener { - override fun onInit(status:Int) { - if(status == TextToSpeech.SUCCESS) { - println("Successfully initialized TTS!") - } else { - println("Failed to initialize TTS.") +class MainActivity : NativeActivity() { + companion object { + init { + System.loadLibrary("hello_world") } } } \ No newline at end of file diff --git a/examples/android/cargo.toml b/examples/android/cargo.toml index a7ff289..2748088 100644 --- a/examples/android/cargo.toml +++ b/examples/android/cargo.toml @@ -10,5 +10,6 @@ edition = "2018" crate-type = ["dylib"] [dependencies] +jni = "0.18" ndk-glue = "0.2" tts = { path = "../.." } \ No newline at end of file diff --git a/examples/android/src/lib.rs b/examples/android/src/lib.rs index da4ef23..8a256d6 100644 --- a/examples/android/src/lib.rs +++ b/examples/android/src/lib.rs @@ -1,10 +1,7 @@ -use std::thread; -use std::time::Duration; use tts::*; fn run() -> Result<(), Error> { let mut tts = TTS::default()?; - thread::sleep(Duration::from_secs(5)); let Features { utterance_callbacks, .. @@ -65,6 +62,5 @@ fn run() -> Result<(), Error> { #[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))] pub fn main() { - println!("xxxx In library"); run().expect("Failed to run"); } diff --git a/src/backends/android.rs b/src/backends/android.rs index 6a2e200..a70b054 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -1,15 +1,49 @@ #[cfg(target_os = "android")] -use std::sync::Mutex; +use std::collections::HashSet; +use std::os::raw::c_void; +use std::sync::{Mutex, RwLock}; +use std::thread; +use std::time::Duration; use jni::objects::{GlobalRef, JObject}; -use jni::JavaVM; +use jni::sys::{jint, JNI_VERSION_1_6}; +use jni::{JNIEnv, JavaVM}; use lazy_static::lazy_static; use log::info; use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; lazy_static! { + static ref BRIDGE: Mutex> = Mutex::new(None); static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); + static ref PENDING_INITIALIZATIONS: RwLock> = RwLock::new(HashSet::new()); +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "system" fn JNI_OnLoad(vm: JavaVM, _: *mut c_void) -> jint { + let env = vm.get_env().expect("Cannot get reference to the JNIEnv"); + let b = env + .find_class("rs/tts/Bridge") + .expect("Failed to find `Bridge`"); + let b = env + .new_global_ref(b) + .expect("Failed to create `Bridge` `GlobalRef`"); + let mut bridge = BRIDGE.lock().unwrap(); + *bridge = Some(b); + JNI_VERSION_1_6 +} + +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_rs_tts_Bridge_onInit(env: JNIEnv, obj: JObject, status: jint) { + let id = env + .get_field(obj, "backendId", "I") + .expect("Failed to get backend ID") + .i() + .expect("Failed to cast to int") as u64; + let mut pending = PENDING_INITIALIZATIONS.write().unwrap(); + (*pending).remove(&id); } #[derive(Clone)] @@ -22,23 +56,40 @@ impl Android { pub(crate) fn new() -> Result { info!("Initializing Android backend"); let mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); - let id = BackendId::Android(*backend_id); + let bid = *backend_id; + let id = BackendId::Android(bid); *backend_id += 1; + drop(backend_id); let native_activity = ndk_glue::native_activity(); let vm = Self::vm()?; let env = vm.attach_current_thread_permanently()?; - let tts = env.new_object( - "android/speech/tts/TextToSpeech", - "(Landroid/content/Context;Landroid/speech/tts/TextToSpeech$OnInitListener;)V", - &[ - native_activity.activity().into(), - native_activity.activity().into(), - ], - )?; - println!("Creating global ref"); - let tts = env.new_global_ref(tts)?; - println!("Returning"); - Ok(Self { id, tts }) + let bridge = BRIDGE.lock().unwrap(); + if let Some(bridge) = &*bridge { + let bridge = env.new_object(bridge, "(I)V", &[(bid as jint).into()])?; + let tts = env.new_object( + "android/speech/tts/TextToSpeech", + "(Landroid/content/Context;Landroid/speech/tts/TextToSpeech$OnInitListener;)V", + &[native_activity.activity().into(), bridge.into()], + )?; + { + let mut pending = PENDING_INITIALIZATIONS.write().unwrap(); + (*pending).insert(bid); + } + let tts = env.new_global_ref(tts)?; + // This hack makes my brain bleed. + loop { + { + let pending = PENDING_INITIALIZATIONS.read().unwrap(); + if !(*pending).contains(&bid) { + break; + } + } + thread::sleep(Duration::from_millis(5)); + } + Ok(Self { id, tts }) + } else { + Err(Error::NoneError) + } } fn vm() -> Result { @@ -65,15 +116,11 @@ impl Backend for Android { } fn speak(&mut self, text: &str, interrupt: bool) -> Result, Error> { - println!("Speaking {}, {:?}", text, interrupt); let vm = Self::vm()?; - println!("Retrieved"); let env = vm.get_env()?; - println!("attached"); let tts = self.tts.as_obj(); let text = env.new_string(text)?; let queue_mode = if interrupt { 0 } else { 1 }; - println!("Calling"); env.call_method( tts, "speak", @@ -85,7 +132,6 @@ impl Backend for Android { JObject::null().into(), ], )?; - println!("Returning"); Ok(None) } @@ -94,15 +140,15 @@ impl Backend for Android { } fn min_rate(&self) -> f32 { - todo!() + 0.1 } fn max_rate(&self) -> f32 { - todo!() + 10. } fn normal_rate(&self) -> f32 { - todo!() + 1. } fn get_rate(&self) -> Result { @@ -114,15 +160,15 @@ impl Backend for Android { } fn min_pitch(&self) -> f32 { - todo!() + 0.1 } fn max_pitch(&self) -> f32 { - todo!() + 2. } fn normal_pitch(&self) -> f32 { - todo!() + 1. } fn get_pitch(&self) -> Result { From 22ee9863d61894f6f01e39f92e5671da2b6a01ee Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 09:44:47 -0600 Subject: [PATCH 080/196] Return utterance IDs when speech succeeds. --- src/backends/android.rs | 18 +++++++++++++++--- src/lib.rs | 4 ++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/backends/android.rs b/src/backends/android.rs index a70b054..577a3a4 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -17,6 +17,7 @@ lazy_static! { static ref BRIDGE: Mutex> = Mutex::new(None); static ref NEXT_BACKEND_ID: Mutex = Mutex::new(0); static ref PENDING_INITIALIZATIONS: RwLock> = RwLock::new(HashSet::new()); + static ref NEXT_UTTERANCE_ID: Mutex = Mutex::new(0); } #[allow(non_snake_case)] @@ -121,7 +122,13 @@ impl Backend for Android { let tts = self.tts.as_obj(); let text = env.new_string(text)?; let queue_mode = if interrupt { 0 } else { 1 }; - env.call_method( + let mut utterance_id = NEXT_UTTERANCE_ID.lock().unwrap(); + let uid = *utterance_id; + *utterance_id += 1; + drop(utterance_id); + let id = UtteranceId::Android(uid); + let uid = env.new_string(uid.to_string())?; + let rv = env.call_method( tts, "speak", "(Ljava/lang/CharSequence;ILandroid/os/Bundle;Ljava/lang/String;)I", @@ -129,10 +136,15 @@ impl Backend for Android { text.into(), queue_mode.into(), JObject::null().into(), - JObject::null().into(), + uid.into(), ], )?; - Ok(None) + let rv = rv.i()? as i32; + if rv == 0 { + Ok(Some(id)) + } else { + Err(Error::OperationFailed) + } } fn stop(&mut self) -> Result<(), Error> { diff --git a/src/lib.rs b/src/lib.rs index ff0a46d..2e53b5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,6 +71,8 @@ pub enum UtteranceId { WinRT(u64), #[cfg(any(target_os = "macos", target_os = "ios"))] AvFoundation(id), + #[cfg(target_os = "android")] + Android(u64), } unsafe impl Send for UtteranceId {} @@ -105,6 +107,8 @@ pub enum Error { IO(#[from] std::io::Error), #[error("Value not received")] NoneError, + #[error("Operation failed")] + OperationFailed, #[cfg(target_arch = "wasm32")] #[error("JavaScript error: [0])]")] JavaScriptError(wasm_bindgen::JsValue), From e1c217183350075d2c0f17c09ce15cfcbfc50fde Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 09:49:13 -0600 Subject: [PATCH 081/196] Support stopping. --- src/backends/android.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/backends/android.rs b/src/backends/android.rs index 577a3a4..6cf59aa 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -107,7 +107,7 @@ impl Backend for Android { fn supported_features(&self) -> Features { Features { - stop: false, + stop: true, rate: false, pitch: false, volume: false, @@ -148,7 +148,16 @@ impl Backend for Android { } fn stop(&mut self) -> Result<(), Error> { - todo!() + let vm = Self::vm()?; + let env = vm.get_env()?; + let tts = self.tts.as_obj(); + let rv = env.call_method(tts, "stop", "()I", &[])?; + let rv = rv.i()? as i32; + if rv == 0 { + Ok(()) + } else { + Err(Error::OperationFailed) + } } fn min_rate(&self) -> f32 { From 2120de8756168eb5fea1123272d768e64f8a418d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 10:06:18 -0600 Subject: [PATCH 082/196] Support pitch. --- src/backends/android.rs | 49 ++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/src/backends/android.rs b/src/backends/android.rs index 6cf59aa..e449ec5 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -6,7 +6,7 @@ use std::thread; use std::time::Duration; use jni::objects::{GlobalRef, JObject}; -use jni::sys::{jint, JNI_VERSION_1_6}; +use jni::sys::{jfloat, jint, JNI_VERSION_1_6}; use jni::{JNIEnv, JavaVM}; use lazy_static::lazy_static; use log::info; @@ -51,6 +51,8 @@ pub unsafe extern "C" fn Java_rs_tts_Bridge_onInit(env: JNIEnv, obj: JObject, st pub(crate) struct Android { id: BackendId, tts: GlobalRef, + rate: f32, + pitch: f32, } impl Android { @@ -87,7 +89,12 @@ impl Android { } thread::sleep(Duration::from_millis(5)); } - Ok(Self { id, tts }) + Ok(Self { + id, + tts, + rate: 1., + pitch: 1., + }) } else { Err(Error::NoneError) } @@ -108,8 +115,8 @@ impl Backend for Android { fn supported_features(&self) -> Features { Features { stop: true, - rate: false, - pitch: false, + rate: true, + pitch: true, volume: false, is_speaking: false, utterance_callbacks: false, @@ -139,7 +146,7 @@ impl Backend for Android { uid.into(), ], )?; - let rv = rv.i()? as i32; + let rv = rv.i()?; if rv == 0 { Ok(Some(id)) } else { @@ -152,7 +159,7 @@ impl Backend for Android { let env = vm.get_env()?; let tts = self.tts.as_obj(); let rv = env.call_method(tts, "stop", "()I", &[])?; - let rv = rv.i()? as i32; + let rv = rv.i()?; if rv == 0 { Ok(()) } else { @@ -173,11 +180,22 @@ impl Backend for Android { } fn get_rate(&self) -> Result { - todo!() + Ok(self.rate) } fn set_rate(&mut self, rate: f32) -> Result<(), Error> { - todo!() + let vm = Self::vm()?; + let env = vm.get_env()?; + let tts = self.tts.as_obj(); + let rate = rate as jfloat; + let rv = env.call_method(tts, "setSpeechRate", "(F)I", &[rate.into()])?; + let rv = rv.i()?; + if rv == 0 { + self.rate = rate; + Ok(()) + } else { + Err(Error::OperationFailed) + } } fn min_pitch(&self) -> f32 { @@ -193,11 +211,22 @@ impl Backend for Android { } fn get_pitch(&self) -> Result { - todo!() + Ok(self.pitch) } fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> { - todo!() + let vm = Self::vm()?; + let env = vm.get_env()?; + let tts = self.tts.as_obj(); + let pitch = pitch as jfloat; + let rv = env.call_method(tts, "setPitch", "(F)I", &[pitch.into()])?; + let rv = rv.i()?; + if rv == 0 { + self.pitch = pitch; + Ok(()) + } else { + Err(Error::OperationFailed) + } } fn min_volume(&self) -> f32 { From 440154502b42f9d49e4d0459739602b4e5d5a878 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 10:07:27 -0600 Subject: [PATCH 083/196] Clear some unused variable warnings. --- src/backends/android.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backends/android.rs b/src/backends/android.rs index e449ec5..99f70be 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -37,7 +37,7 @@ pub extern "system" fn JNI_OnLoad(vm: JavaVM, _: *mut c_void) -> jint { #[no_mangle] #[allow(non_snake_case)] -pub unsafe extern "C" fn Java_rs_tts_Bridge_onInit(env: JNIEnv, obj: JObject, status: jint) { +pub unsafe extern "C" fn Java_rs_tts_Bridge_onInit(env: JNIEnv, obj: JObject, _status: jint) { let id = env .get_field(obj, "backendId", "I") .expect("Failed to get backend ID") @@ -245,7 +245,7 @@ impl Backend for Android { todo!() } - fn set_volume(&mut self, volume: f32) -> Result<(), Error> { + fn set_volume(&mut self, _volume: f32) -> Result<(), Error> { todo!() } From 733b17fe2cb14cee5455837fa13c094c6ad7857a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 10:10:00 -0600 Subject: [PATCH 084/196] Log TTS initialization failures. --- src/backends/android.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/backends/android.rs b/src/backends/android.rs index 99f70be..6060d83 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -9,7 +9,7 @@ use jni::objects::{GlobalRef, JObject}; use jni::sys::{jfloat, jint, JNI_VERSION_1_6}; use jni::{JNIEnv, JavaVM}; use lazy_static::lazy_static; -use log::info; +use log::{error, info}; use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; @@ -37,7 +37,7 @@ pub extern "system" fn JNI_OnLoad(vm: JavaVM, _: *mut c_void) -> jint { #[no_mangle] #[allow(non_snake_case)] -pub unsafe extern "C" fn Java_rs_tts_Bridge_onInit(env: JNIEnv, obj: JObject, _status: jint) { +pub unsafe extern "C" fn Java_rs_tts_Bridge_onInit(env: JNIEnv, obj: JObject, status: jint) { let id = env .get_field(obj, "backendId", "I") .expect("Failed to get backend ID") @@ -45,6 +45,9 @@ pub unsafe extern "C" fn Java_rs_tts_Bridge_onInit(env: JNIEnv, obj: JObject, _s .expect("Failed to cast to int") as u64; let mut pending = PENDING_INITIALIZATIONS.write().unwrap(); (*pending).remove(&id); + if status != 0 { + error!("Failed to initialize TTS engine"); + } } #[derive(Clone)] From c92b67127ca621ea27e241bf0f192ff520d9155a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 10:15:37 -0600 Subject: [PATCH 085/196] Support is_speaking. --- src/backends/android.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/backends/android.rs b/src/backends/android.rs index 6060d83..a1c5d07 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -121,7 +121,7 @@ impl Backend for Android { rate: true, pitch: true, volume: false, - is_speaking: false, + is_speaking: true, utterance_callbacks: false, } } @@ -253,6 +253,11 @@ impl Backend for Android { } fn is_speaking(&self) -> Result { - todo!() + let vm = Self::vm()?; + let env = vm.get_env()?; + let tts = self.tts.as_obj(); + let rv = env.call_method(tts, "isSpeaking", "()Z", &[])?; + let rv = rv.z()?; + Ok(rv) } } From 0ea46b29b24beddf371cb648f2ec29a22b02c739 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 11:37:46 -0600 Subject: [PATCH 086/196] Partially implement callbacks. Unfinished due to lazy_static inconsistencies. --- Makefile.toml | 6 ++ .../app/src/main/java/rs/tts/Bridge.java | 13 ++- examples/android/src/lib.rs | 4 +- src/backends/android.rs | 101 +++++++++++++++++- src/lib.rs | 2 + 5 files changed, 120 insertions(+), 6 deletions(-) diff --git a/Makefile.toml b/Makefile.toml index 7673a80..db3c4d3 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -1,3 +1,9 @@ +[tasks.run-android] +script = [ + "cd examples/android", + "./gradlew runDebug", +] + [tasks.log-android] command = "adb" args = ["logcat", "RustStdoutStderr:D", "*:S"] \ No newline at end of file diff --git a/examples/android/app/src/main/java/rs/tts/Bridge.java b/examples/android/app/src/main/java/rs/tts/Bridge.java index 4850c25..e6b81a4 100644 --- a/examples/android/app/src/main/java/rs/tts/Bridge.java +++ b/examples/android/app/src/main/java/rs/tts/Bridge.java @@ -1,15 +1,24 @@ package rs.tts; import android.speech.tts.TextToSpeech; +import android.speech.tts.UtteranceProgressListener; @androidx.annotation.Keep -public class Bridge implements TextToSpeech.OnInitListener { +public class Bridge extends UtteranceProgressListener implements TextToSpeech.OnInitListener { public int backendId; public Bridge(int backendId) { this.backendId = backendId; } - public native void onInit(int status); + public native void onInit(int status); + + public native void onStart(String utteranceId); + + public native void onStop(String utteranceId, Boolean interrupted); + + public native void onDone(String utteranceId); + + public native void onError(String utteranceId) ; } \ No newline at end of file diff --git a/examples/android/src/lib.rs b/examples/android/src/lib.rs index 8a256d6..08a156d 100644 --- a/examples/android/src/lib.rs +++ b/examples/android/src/lib.rs @@ -7,9 +7,9 @@ fn run() -> Result<(), Error> { .. } = tts.supported_features(); if utterance_callbacks { - tts.on_utterance_begin(Some(Box::new(|utterance| { + /*tts.on_utterance_begin(Some(Box::new(|utterance| { println!("Started speaking {:?}", utterance) - })))?; + })))?;*/ tts.on_utterance_end(Some(Box::new(|utterance| { println!("Finished speaking {:?}", utterance) })))?; diff --git a/src/backends/android.rs b/src/backends/android.rs index a1c5d07..16a1212 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -1,11 +1,12 @@ #[cfg(target_os = "android")] use std::collections::HashSet; +use std::ffi::{CStr, CString}; use std::os::raw::c_void; use std::sync::{Mutex, RwLock}; use std::thread; use std::time::Duration; -use jni::objects::{GlobalRef, JObject}; +use jni::objects::{GlobalRef, JObject, JString}; use jni::sys::{jfloat, jint, JNI_VERSION_1_6}; use jni::{JNIEnv, JavaVM}; use lazy_static::lazy_static; @@ -50,6 +51,96 @@ pub unsafe extern "C" fn Java_rs_tts_Bridge_onInit(env: JNIEnv, obj: JObject, st } } +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_rs_tts_Bridge_onStart( + env: JNIEnv, + obj: JObject, + utterance_id: JString, +) { + let backend_id = env + .get_field(obj, "backendId", "I") + .expect("Failed to get backend ID") + .i() + .expect("Failed to cast to int") as u64; + let backend_id = BackendId::Android(backend_id); + let utterance_id = CString::from(CStr::from_ptr( + env.get_string(utterance_id).unwrap().as_ptr(), + )) + .into_string() + .unwrap(); + let utterance_id = utterance_id.parse::().unwrap(); + let utterance_id = UtteranceId::Android(utterance_id); + println!("Retrieving callbacks for {:?}", backend_id); + let mut callbacks = CALLBACKS.lock().unwrap(); + println!("Callback keys: {:?}", callbacks.keys()); + if let Some(cb) = callbacks.get_mut(&backend_id) { + if let Some(f) = cb.utterance_begin.as_mut() { + f(utterance_id); + } + } +} + +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_rs_tts_Bridge_onStop( + env: JNIEnv, + obj: JObject, + utterance_id: JString, +) { + let id = env + .get_field(obj, "backendId", "I") + .expect("Failed to get backend ID") + .i() + .expect("Failed to cast to int") as u64; + let utterance_id = CString::from(CStr::from_ptr( + env.get_string(utterance_id).unwrap().as_ptr(), + )) + .into_string() + .unwrap(); + //println!("Call stop for {}", utterance_id); +} + +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_rs_tts_Bridge_onDone( + env: JNIEnv, + obj: JObject, + utterance_id: JString, +) { + let id = env + .get_field(obj, "backendId", "I") + .expect("Failed to get backend ID") + .i() + .expect("Failed to cast to int") as u64; + let utterance_id = CString::from(CStr::from_ptr( + env.get_string(utterance_id).unwrap().as_ptr(), + )) + .into_string() + .unwrap(); + //println!("Call done for {}", utterance_id); +} + +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_rs_tts_Bridge_onError( + env: JNIEnv, + obj: JObject, + utterance_id: JString, +) { + let id = env + .get_field(obj, "backendId", "I") + .expect("Failed to get backend ID") + .i() + .expect("Failed to cast to int") as u64; + let utterance_id = CString::from(CStr::from_ptr( + env.get_string(utterance_id).unwrap().as_ptr(), + )) + .into_string() + .unwrap(); + //println!("Call error for {}", utterance_id); +} + #[derive(Clone)] pub(crate) struct Android { id: BackendId, @@ -77,6 +168,12 @@ impl Android { "(Landroid/content/Context;Landroid/speech/tts/TextToSpeech$OnInitListener;)V", &[native_activity.activity().into(), bridge.into()], )?; + env.call_method( + tts, + "setOnUtteranceProgressListener", + "(Landroid/speech/tts/UtteranceProgressListener;)I", + &[bridge.into()], + )?; { let mut pending = PENDING_INITIALIZATIONS.write().unwrap(); (*pending).insert(bid); @@ -122,7 +219,7 @@ impl Backend for Android { pitch: true, volume: false, is_speaking: true, - utterance_callbacks: false, + utterance_callbacks: true, } } diff --git a/src/lib.rs b/src/lib.rs index 2e53b5e..15021d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -213,7 +213,9 @@ impl TTS { if let Ok(backend) = backend { if let Some(id) = backend.0.id() { let mut callbacks = CALLBACKS.lock().unwrap(); + println!("Initializing callbacks for {:?}", id); callbacks.insert(id, Callbacks::default()); + println!("Keys after: {:?}", callbacks.keys()); } Ok(backend) } else { From 8d6f40b1a5a755f64aaa99b470452f9d496dc338 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 12:19:44 -0600 Subject: [PATCH 087/196] Finish callback implementation. --- examples/android/src/lib.rs | 5 +++-- src/backends/android.rs | 42 ++++++++++++++++++++++++++----------- src/lib.rs | 2 -- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/examples/android/src/lib.rs b/examples/android/src/lib.rs index 08a156d..7db2211 100644 --- a/examples/android/src/lib.rs +++ b/examples/android/src/lib.rs @@ -7,9 +7,9 @@ fn run() -> Result<(), Error> { .. } = tts.supported_features(); if utterance_callbacks { - /*tts.on_utterance_begin(Some(Box::new(|utterance| { + tts.on_utterance_begin(Some(Box::new(|utterance| { println!("Started speaking {:?}", utterance) - })))?;*/ + })))?; tts.on_utterance_end(Some(Box::new(|utterance| { println!("Finished speaking {:?}", utterance) })))?; @@ -57,6 +57,7 @@ fn run() -> Result<(), Error> { tts.set_volume(original_volume)?; } tts.speak("Goodbye.", false)?; + loop {} Ok(()) } diff --git a/src/backends/android.rs b/src/backends/android.rs index 16a1212..55449c3 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -71,13 +71,10 @@ pub unsafe extern "C" fn Java_rs_tts_Bridge_onStart( .unwrap(); let utterance_id = utterance_id.parse::().unwrap(); let utterance_id = UtteranceId::Android(utterance_id); - println!("Retrieving callbacks for {:?}", backend_id); let mut callbacks = CALLBACKS.lock().unwrap(); - println!("Callback keys: {:?}", callbacks.keys()); - if let Some(cb) = callbacks.get_mut(&backend_id) { - if let Some(f) = cb.utterance_begin.as_mut() { - f(utterance_id); - } + let cb = callbacks.get_mut(&backend_id).unwrap(); + if let Some(f) = cb.utterance_begin.as_mut() { + f(utterance_id); } } @@ -88,17 +85,24 @@ pub unsafe extern "C" fn Java_rs_tts_Bridge_onStop( obj: JObject, utterance_id: JString, ) { - let id = env + let backend_id = env .get_field(obj, "backendId", "I") .expect("Failed to get backend ID") .i() .expect("Failed to cast to int") as u64; + let backend_id = BackendId::Android(backend_id); let utterance_id = CString::from(CStr::from_ptr( env.get_string(utterance_id).unwrap().as_ptr(), )) .into_string() .unwrap(); - //println!("Call stop for {}", utterance_id); + let utterance_id = utterance_id.parse::().unwrap(); + let utterance_id = UtteranceId::Android(utterance_id); + let mut callbacks = CALLBACKS.lock().unwrap(); + let cb = callbacks.get_mut(&backend_id).unwrap(); + if let Some(f) = cb.utterance_end.as_mut() { + f(utterance_id); + } } #[no_mangle] @@ -108,17 +112,24 @@ pub unsafe extern "C" fn Java_rs_tts_Bridge_onDone( obj: JObject, utterance_id: JString, ) { - let id = env + let backend_id = env .get_field(obj, "backendId", "I") .expect("Failed to get backend ID") .i() .expect("Failed to cast to int") as u64; + let backend_id = BackendId::Android(backend_id); let utterance_id = CString::from(CStr::from_ptr( env.get_string(utterance_id).unwrap().as_ptr(), )) .into_string() .unwrap(); - //println!("Call done for {}", utterance_id); + let utterance_id = utterance_id.parse::().unwrap(); + let utterance_id = UtteranceId::Android(utterance_id); + let mut callbacks = CALLBACKS.lock().unwrap(); + let cb = callbacks.get_mut(&backend_id).unwrap(); + if let Some(f) = cb.utterance_stop.as_mut() { + f(utterance_id); + } } #[no_mangle] @@ -128,17 +139,24 @@ pub unsafe extern "C" fn Java_rs_tts_Bridge_onError( obj: JObject, utterance_id: JString, ) { - let id = env + let backend_id = env .get_field(obj, "backendId", "I") .expect("Failed to get backend ID") .i() .expect("Failed to cast to int") as u64; + let backend_id = BackendId::Android(backend_id); let utterance_id = CString::from(CStr::from_ptr( env.get_string(utterance_id).unwrap().as_ptr(), )) .into_string() .unwrap(); - //println!("Call error for {}", utterance_id); + let utterance_id = utterance_id.parse::().unwrap(); + let utterance_id = UtteranceId::Android(utterance_id); + let mut callbacks = CALLBACKS.lock().unwrap(); + let cb = callbacks.get_mut(&backend_id).unwrap(); + if let Some(f) = cb.utterance_end.as_mut() { + f(utterance_id); + } } #[derive(Clone)] diff --git a/src/lib.rs b/src/lib.rs index 15021d8..2e53b5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -213,9 +213,7 @@ impl TTS { if let Ok(backend) = backend { if let Some(id) = backend.0.id() { let mut callbacks = CALLBACKS.lock().unwrap(); - println!("Initializing callbacks for {:?}", id); callbacks.insert(id, Callbacks::default()); - println!("Keys after: {:?}", callbacks.keys()); } Ok(backend) } else { From a01fd93502a9fd09b7c47c1f22e29b6b525e32b4 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 12:28:07 -0600 Subject: [PATCH 088/196] Build Android example as part of CI. --- .github/workflows/test.yml | 21 +++++++++++++++++++++ Makefile.toml | 8 +++++++- examples/android/src/lib.rs | 3 +++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3d5cd7d..0fbeda6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,3 +60,24 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} args: --all-features --target wasm32-unknown-unknown + + check_android: + name: Check Android + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + components: rustfmt, clippy + override: true + - uses: actions-rs/install@v0.1 + with: + crate: cargo-make + use-tool-cache: true + - uses: actions-rs/cargo@v1 + with: + command: make + args: build-android-example diff --git a/Makefile.toml b/Makefile.toml index db3c4d3..6697f15 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -1,4 +1,10 @@ -[tasks.run-android] +[tasks.build-android-example] +script = [ + "cd examples/android", + "./gradlew assembleDebug", +] + +[tasks.run-android-example] script = [ "cd examples/android", "./gradlew runDebug", diff --git a/examples/android/src/lib.rs b/examples/android/src/lib.rs index 7db2211..6bf79d6 100644 --- a/examples/android/src/lib.rs +++ b/examples/android/src/lib.rs @@ -1,5 +1,8 @@ use tts::*; +// The `loop {}` below only simulates an app loop. +// Without it, the `TTS` instance gets dropped before callbacks can run. +#[allow(unreachable_code)] fn run() -> Result<(), Error> { let mut tts = TTS::default()?; let Features { From cee577755666ab0b793d16921b98a4a37eb7082f Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 12:41:28 -0600 Subject: [PATCH 089/196] README/packaging tweaks for Android. --- README.md | 15 ++++++++++++++- examples/android/cargo.toml | 1 - src/lib.rs | 3 ++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f06defc..90fab74 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,20 @@ This library provides a high-level Text-To-Speech (TTS) interface supporting var * Screen readers/SAPI via Tolk (requires `use_tolk` Cargo feature) * WinRT * Linux via [Speech Dispatcher](https://freebsoft.org/speechd) -* MacOS +* MacOS/iOS * AppKit on MacOS 10.13 and below * AVFoundation on MacOS 10.14 and above, and iOS +* Android * WebAssembly + +## Android Setup + +On most platforms, this library is plug-and-play. Because of JNI's complexity, Android setup is a bit more involved. In general, look to the Android example for guidance. Here are some rough steps to get going: + +* Set up _Cargo.toml_ as the example does. Be sure to depend on `ndk-glue`. +* Place _Bridge.java_ appropriately in your app. This is needed to support various Android TTS callbacks. +* Create a main activity similar to _MainActivity.kt_. In particular, you need to derive `android.app.NativeActivity`, and you need a `System.loadLibrary(...)` call appropriate for your app. `System.loadLibrary(...)` is needed to trigger `JNI_OnLoad`. +* * Even though you've loaded the library in your main activity, add a metadata tag to your activity in _AndroidManifest.xml_ referencing it. Yes, this is redundant but necessary. +* Set if your various build.gradle scripts to reference the plugins, dependencies, etc. from the example. In particular, you'll want to set up [cargo-ndk-android-gradle](https://github.com/willir/cargo-ndk-android-gradle/) and either [depend on androidx.annotation](https://developer.android.com/reference/androidx/annotation/package-summary) or otherwise configure your app to keep the class _rs.tts.Bridge_. + +And I think that should about do it. Good luck! \ No newline at end of file diff --git a/examples/android/cargo.toml b/examples/android/cargo.toml index 2748088..a7ff289 100644 --- a/examples/android/cargo.toml +++ b/examples/android/cargo.toml @@ -10,6 +10,5 @@ edition = "2018" crate-type = ["dylib"] [dependencies] -jni = "0.18" ndk-glue = "0.2" tts = { path = "../.." } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 2e53b5e..81fb009 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,9 +5,10 @@ * * Screen readers/SAPI via Tolk (requires `use_tolk` Cargo feature) * * WinRT * * Linux via [Speech Dispatcher](https://freebsoft.org/speechd) - * * MacOS + * * MacOS/iOS * * AppKit on MacOS 10.13 and below * * AVFoundation on MacOS 10.14 and above, and iOS + * * Android * * WebAssembly */ From cf39be85afc24af6cf62c657ff29c5c3447b853f Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 12:43:15 -0600 Subject: [PATCH 090/196] Looks like the tool cache isn't supported or is failing. Comment out for now. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0fbeda6..f8032ba 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: - uses: actions-rs/install@v0.1 with: crate: cargo-make - use-tool-cache: true + # use-tool-cache: true - uses: actions-rs/cargo@v1 with: command: make From 914a7a19725132441c64dddd788cdc252d15493a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 12:51:55 -0600 Subject: [PATCH 091/196] Make script executable. --- examples/android/gradlew | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 examples/android/gradlew diff --git a/examples/android/gradlew b/examples/android/gradlew old mode 100644 new mode 100755 From adfb2146ac5e30a2cd208b95133206f4eadf241d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 12:53:21 -0600 Subject: [PATCH 092/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e254177..da6ad60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.12.2" +version = "0.13.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From d806c44c76707a9d7092ef90d5ca3bd766d6b129 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 13:09:29 -0600 Subject: [PATCH 093/196] My brain hurts and I can't figure out how to set this CI action up right now. But it works, so release. --- .github/workflows/test.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f8032ba..f907b7c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -77,7 +77,3 @@ jobs: with: crate: cargo-make # use-tool-cache: true - - uses: actions-rs/cargo@v1 - with: - command: make - args: build-android-example From 7eb74729fc4a3b290fb62e694a2f29518e7f8295 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 14:00:25 -0600 Subject: [PATCH 094/196] Use cargo-apk to test Android build. --- .github/workflows/test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f907b7c..0612f47 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -75,5 +75,9 @@ jobs: override: true - uses: actions-rs/install@v0.1 with: - crate: cargo-make + crate: cargo-apk # use-tool-cache: true + - uses: actions-rs/cargo@v1 + with: + command: apk + args: build From 699d0d23e99a00843408e33387a93821f66ec3c7 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 14:15:14 -0600 Subject: [PATCH 095/196] Add necessary targets. --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0612f47..59a3e88 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,6 +71,7 @@ jobs: with: profile: minimal toolchain: stable + target: aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android components: rustfmt, clippy override: true - uses: actions-rs/install@v0.1 From 69af3465b35afef0b8730bf223c8a9d0937318d6 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 14:21:32 -0600 Subject: [PATCH 096/196] We don't need dylib on Android, and it breaks WinRT. Revert. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index da6ad60..a4a5c5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ exclude = ["*.cfg", "*.yml"] edition = "2018" [lib] -crate-type = ["lib", "dylib", "staticlib"] +crate-type = ["lib", "cdylib", "staticlib"] [features] use_tolk = ["tolk"] From 06eb32b6d4fc770477f84e1662b3d2f19435f075 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 14:23:31 -0600 Subject: [PATCH 097/196] Make module imports more consistent. --- src/backends/mod.rs | 7 +++++-- src/lib.rs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 4e61063..c36c233 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -5,7 +5,7 @@ mod speech_dispatcher; mod tolk; #[cfg(windows)] -pub(crate) mod winrt; +mod winrt; #[cfg(target_arch = "wasm32")] mod web; @@ -25,8 +25,11 @@ pub(crate) use self::speech_dispatcher::*; #[cfg(all(windows, feature = "use_tolk"))] pub(crate) use self::tolk::*; +#[cfg(windows)] +pub(crate) use self::winrt::*; + #[cfg(target_arch = "wasm32")] -pub use self::web::*; +pub(crate) use self::web::*; #[cfg(target_os = "macos")] pub(crate) use self::appkit::*; diff --git a/src/lib.rs b/src/lib.rs index 81fb009..a4b94f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -198,7 +198,7 @@ impl TTS { } #[cfg(windows)] Backends::WinRT => { - let tts = backends::winrt::WinRT::new()?; + let tts = backends::WinRT::new()?; Ok(TTS(Box::new(tts))) } #[cfg(target_os = "macos")] From 22cff2ddd187c1c275711a748db1753ac3b92ad0 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 14:23:58 -0600 Subject: [PATCH 098/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a4a5c5f..cb08133 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.13.0" +version = "0.13.1" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 15b7b33ed35497258d7e807d3be84c3d6a389777 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 14:31:27 -0600 Subject: [PATCH 099/196] Are these CSVs? --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 59a3e88..55aa3b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,7 +71,7 @@ jobs: with: profile: minimal toolchain: stable - target: aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android + target: aarch64-linux-android, armv7-linux-androideabi, i686-linux-android, x86_64-linux-android components: rustfmt, clippy override: true - uses: actions-rs/install@v0.1 From 3e1f5af61a3f079e7cbbe939fe3fda4b198813da Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 30 Dec 2020 14:50:36 -0600 Subject: [PATCH 100/196] Call rustup manually, since the action doesn't seem to work. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55aa3b4..50d9534 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,13 +71,13 @@ jobs: with: profile: minimal toolchain: stable - target: aarch64-linux-android, armv7-linux-androideabi, i686-linux-android, x86_64-linux-android components: rustfmt, clippy override: true - uses: actions-rs/install@v0.1 with: crate: cargo-apk # use-tool-cache: true + - run: rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android - uses: actions-rs/cargo@v1 with: command: apk From 296fa89f5d48c2dc1971c0ac064f3ec34d7e17cb Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 21 Jan 2021 10:49:11 -0600 Subject: [PATCH 101/196] Bump version, and use automatically-provided feature provided by optional tolk dependency. --- Cargo.toml | 5 +---- README.md | 2 +- src/backends/mod.rs | 4 ++-- src/backends/tolk.rs | 2 +- src/lib.rs | 10 +++++----- 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cb08133..3a4bfc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.13.1" +version = "0.14.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -11,9 +11,6 @@ edition = "2018" [lib] crate-type = ["lib", "cdylib", "staticlib"] -[features] -use_tolk = ["tolk"] - [dependencies] dyn-clonable = "0.9" lazy_static = "1" diff --git a/README.md b/README.md index 90fab74..790e4f2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ This library provides a high-level Text-To-Speech (TTS) interface supporting various backends. Currently supported backends are: * Windows - * Screen readers/SAPI via Tolk (requires `use_tolk` Cargo feature) + * Screen readers/SAPI via Tolk (requires `tolk` Cargo feature) * WinRT * Linux via [Speech Dispatcher](https://freebsoft.org/speechd) * MacOS/iOS diff --git a/src/backends/mod.rs b/src/backends/mod.rs index c36c233..aad23ca 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -1,7 +1,7 @@ #[cfg(target_os = "linux")] mod speech_dispatcher; -#[cfg(all(windows, feature = "use_tolk"))] +#[cfg(all(windows, feature = "tolk"))] mod tolk; #[cfg(windows)] @@ -22,7 +22,7 @@ mod android; #[cfg(target_os = "linux")] pub(crate) use self::speech_dispatcher::*; -#[cfg(all(windows, feature = "use_tolk"))] +#[cfg(all(windows, feature = "tolk"))] pub(crate) use self::tolk::*; #[cfg(windows)] diff --git a/src/backends/tolk.rs b/src/backends/tolk.rs index f9281f7..ae16e4f 100644 --- a/src/backends/tolk.rs +++ b/src/backends/tolk.rs @@ -1,4 +1,4 @@ -#[cfg(all(windows, feature = "use_tolk"))] +#[cfg(all(windows, feature = "tolk"))] use log::{info, trace}; use tolk::Tolk as TolkPtr; diff --git a/src/lib.rs b/src/lib.rs index a4b94f5..fa538df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ * a Text-To-Speech (TTS) library providing high-level interfaces to a variety of backends. * Currently supported backends are: * * Windows - * * Screen readers/SAPI via Tolk (requires `use_tolk` Cargo feature) + * * Screen readers/SAPI via Tolk (requires `tolk` Cargo feature) * * WinRT * * Linux via [Speech Dispatcher](https://freebsoft.org/speechd) * * MacOS/iOS @@ -36,7 +36,7 @@ pub enum Backends { SpeechDispatcher, #[cfg(target_arch = "wasm32")] Web, - #[cfg(all(windows, feature = "use_tolk"))] + #[cfg(all(windows, feature = "tolk"))] Tolk, #[cfg(windows)] WinRT, @@ -187,7 +187,7 @@ impl TTS { let tts = backends::Web::new()?; Ok(TTS(Box::new(tts))) } - #[cfg(all(windows, feature = "use_tolk"))] + #[cfg(all(windows, feature = "tolk"))] Backends::Tolk => { let tts = backends::Tolk::new(); if let Some(tts) = tts { @@ -225,13 +225,13 @@ impl TTS { pub fn default() -> Result { #[cfg(target_os = "linux")] let tts = TTS::new(Backends::SpeechDispatcher); - #[cfg(all(windows, feature = "use_tolk"))] + #[cfg(all(windows, feature = "tolk"))] let tts = if let Ok(tts) = TTS::new(Backends::Tolk) { Ok(tts) } else { TTS::new(Backends::WinRT) }; - #[cfg(all(windows, not(feature = "use_tolk")))] + #[cfg(all(windows, not(feature = "tolk")))] let tts = TTS::new(Backends::WinRT); #[cfg(target_arch = "wasm32")] let tts = TTS::new(Backends::Web); From 42879dfa1f8731f8153dff4084a2d387308da1a5 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 22 Jan 2021 11:28:42 -0600 Subject: [PATCH 102/196] Refactor to new windows crate. --- Cargo.toml | 4 +- src/backends/winrt.rs | 88 +++++++++++++++++---------------------- src/lib.rs | 2 +- winrt_bindings/Cargo.toml | 6 +-- winrt_bindings/build.rs | 11 ++--- winrt_bindings/src/lib.rs | 2 +- 6 files changed, 48 insertions(+), 65 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3a4bfc6..258aa5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,8 +22,8 @@ env_logger = "0.8" [target.'cfg(windows)'.dependencies] tolk = { version = "0.3", optional = true } -winrt = "0.7" -tts_winrt_bindings = { version = "0.2", path="winrt_bindings" } +windows = "0.2" +tts_winrt_bindings = { version = "0.3", path="winrt_bindings" } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.7" diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 890ef63..d9b7704 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -13,8 +13,8 @@ use tts_winrt_bindings::windows::{foundation::TypedEventHandler, media::core::Me use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; -impl From for Error { - fn from(e: winrt::Error) -> Self { +impl From for Error { + fn from(e: windows::Error) -> Self { Error::WinRT(e) } } @@ -77,60 +77,48 @@ impl WinRT { drop(backend_to_speech_synthesizer); let bid_clone = bid; player.media_ended(TypedEventHandler::new( - move |sender: &MediaPlayer, _args| { - let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); - let id = backend_to_media_player.iter().find(|v| v.1 == sender); - if let Some((id, _)) = id { - let mut utterances = UTTERANCES.lock().unwrap(); - if let Some(utterances) = utterances.get_mut(id) { - if let Some(utterance) = utterances.pop_front() { - let mut callbacks = CALLBACKS.lock().unwrap(); - let callbacks = callbacks.get_mut(id).unwrap(); - if let Some(callback) = callbacks.utterance_end.as_mut() { - callback(utterance.id); - } - if let Some(utterance) = utterances.front() { - let backend_to_speech_synthesizer = - BACKEND_TO_SPEECH_SYNTHESIZER.lock().unwrap(); - let id = backend_to_speech_synthesizer - .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())?; - let stream = tts - .synthesize_text_to_stream_async(utterance.text.as_str())? - .get()?; - let content_type = stream.content_type()?; - let source = - MediaSource::create_from_stream(stream, content_type)?; - sender.set_source(source)?; - sender.play()?; - if let Some(callback) = callbacks.utterance_begin.as_mut() { - callback(utterance.id); + move |sender: &Option, _args| { + if let Some(sender) = sender { + let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); + let id = backend_to_media_player.iter().find(|v| v.1 == sender); + if let Some((id, _)) = id { + let mut utterances = UTTERANCES.lock().unwrap(); + if let Some(utterances) = utterances.get_mut(id) { + if let Some(utterance) = utterances.pop_front() { + let mut callbacks = CALLBACKS.lock().unwrap(); + let callbacks = callbacks.get_mut(id).unwrap(); + if let Some(callback) = callbacks.utterance_end.as_mut() { + callback(utterance.id); + } + if let Some(utterance) = utterances.front() { + let backend_to_speech_synthesizer = + BACKEND_TO_SPEECH_SYNTHESIZER.lock().unwrap(); + let id = backend_to_speech_synthesizer + .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())?; + let stream = tts + .synthesize_text_to_stream_async( + utterance.text.as_str(), + )? + .get()?; + let content_type = stream.content_type()?; + let source = + MediaSource::create_from_stream(stream, content_type)?; + sender.set_source(source)?; + sender.play()?; + if let Some(callback) = callbacks.utterance_begin.as_mut() { + callback(utterance.id); + } } } } } } } - /*let source = sender.source()?; - let source: MediaPlaybackList = source.try_into()?; - source.items()?.clear()?; - let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap(); - let id = backend_to_media_player.iter().find(|v| v.1 == sender); - if let Some(id) = id { - let id = id.0; - let mut callbacks = CALLBACKS.lock().unwrap(); - let callbacks = callbacks.get_mut(&id).unwrap(); - if let Some(callback) = callbacks.utterance_end.as_mut() { - let last_spoken_utterance = LAST_SPOKEN_UTTERANCE.lock().unwrap(); - if let Some(utterance_id) = last_spoken_utterance.get(&id) { - callback(*utterance_id); - } - } - }*/ Ok(()) }, ))?; diff --git a/src/lib.rs b/src/lib.rs index fa538df..d2b80d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,7 +115,7 @@ pub enum Error { JavaScriptError(wasm_bindgen::JsValue), #[cfg(windows)] #[error("WinRT error")] - WinRT(winrt::Error), + WinRT(windows::Error), #[error("Unsupported feature")] UnsupportedFeature, #[error("Out of range")] diff --git a/winrt_bindings/Cargo.toml b/winrt_bindings/Cargo.toml index c0afe2c..eaf4bb2 100644 --- a/winrt_bindings/Cargo.toml +++ b/winrt_bindings/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "tts_winrt_bindings" -version = "0.2.0" +version = "0.3.0" authors = ["Nolan Darilek "] description = "Internal crate used by `tts`" license = "MIT" edition = "2018" [dependencies] -winrt = "0.7" +windows = "0.2" [build-dependencies] -winrt = "0.7" +windows = "0.2" diff --git a/winrt_bindings/build.rs b/winrt_bindings/build.rs index 2cd927d..654291a 100644 --- a/winrt_bindings/build.rs +++ b/winrt_bindings/build.rs @@ -1,12 +1,7 @@ -winrt::build!( - dependencies - os - types +fn main() { + windows::build!( windows::media::core::MediaSource windows::media::playback::{MediaPlaybackState, MediaPlayer} windows::media::speech_synthesis::SpeechSynthesizer -); - -fn main() { - build(); + ); } diff --git a/winrt_bindings/src/lib.rs b/winrt_bindings/src/lib.rs index 056780e..42af6ba 100644 --- a/winrt_bindings/src/lib.rs +++ b/winrt_bindings/src/lib.rs @@ -1 +1 @@ -include!(concat!(env!("OUT_DIR"), "/winrt.rs")); +::windows::include_bindings!(); From d9ca83ca15ccf5ecd598e5995a6cfa8750bceda6 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 11 Mar 2021 12:33:31 -0600 Subject: [PATCH 103/196] WIP: Bump dependencies. Currently broken. --- Cargo.toml | 6 +++--- winrt_bindings/Cargo.toml | 4 ++-- winrt_bindings/build.rs | 7 ++++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 258aa5f..8325028 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ env_logger = "0.8" [target.'cfg(windows)'.dependencies] tolk = { version = "0.3", optional = true } -windows = "0.2" +windows = "0.4" tts_winrt_bindings = { version = "0.3", path="winrt_bindings" } [target.'cfg(target_os = "linux")'.dependencies] @@ -38,5 +38,5 @@ 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" \ No newline at end of file +jni = "0.19" +ndk-glue = "0.3" \ No newline at end of file diff --git a/winrt_bindings/Cargo.toml b/winrt_bindings/Cargo.toml index eaf4bb2..da14f55 100644 --- a/winrt_bindings/Cargo.toml +++ b/winrt_bindings/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT" edition = "2018" [dependencies] -windows = "0.2" +windows = "0.4" [build-dependencies] -windows = "0.2" +windows = "0.4" diff --git a/winrt_bindings/build.rs b/winrt_bindings/build.rs index 654291a..6a4341e 100644 --- a/winrt_bindings/build.rs +++ b/winrt_bindings/build.rs @@ -1,7 +1,8 @@ fn main() { windows::build!( - windows::media::core::MediaSource - windows::media::playback::{MediaPlaybackState, MediaPlayer} - windows::media::speech_synthesis::SpeechSynthesizer + windows::foundation::TypedEventHandler, + windows::media::core::MediaSource, + windows::media::playback::{MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory}, + windows::media::speech_synthesis::SpeechSynthesizer, ); } From 00a16c5dd5167aac353e991ec4264ade936d5189 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 11 Mar 2021 12:54:50 -0600 Subject: [PATCH 104/196] Add missing types. --- winrt_bindings/build.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/winrt_bindings/build.rs b/winrt_bindings/build.rs index 6a4341e..3080588 100644 --- a/winrt_bindings/build.rs +++ b/winrt_bindings/build.rs @@ -1,8 +1,9 @@ fn main() { windows::build!( - windows::foundation::TypedEventHandler, + windows::foundation::{EventRegistrationToken, IAsyncOperation, TypedEventHandler}, windows::media::core::MediaSource, - windows::media::playback::{MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory}, - windows::media::speech_synthesis::SpeechSynthesizer, + windows::media::playback::{MediaPlaybackSession, MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory}, + windows::media::speech_synthesis::{SpeechSynthesisStream, SpeechSynthesizer, SpeechSynthesizerOptions}, + windows::storage::streams::IRandomAccessStream, ); } From c21d4a6a38de84048124e8768f9c63b20cf7d3bd Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 11 Mar 2021 13:21:41 -0600 Subject: [PATCH 105/196] Eliminate separate winrt_bindings crate since it no longer seems necessary for fast builds. --- Cargo.toml | 4 +++- build.rs | 10 +++++++++- .../src/lib.rs => src/backends/winrt/bindings.rs | 2 +- src/backends/{winrt.rs => winrt/mod.rs} | 13 +++++++++---- winrt_bindings/Cargo.toml | 13 ------------- winrt_bindings/build.rs | 9 --------- 6 files changed, 22 insertions(+), 29 deletions(-) rename winrt_bindings/src/lib.rs => src/backends/winrt/bindings.rs (96%) rename src/backends/{winrt.rs => winrt/mod.rs} (97%) delete mode 100644 winrt_bindings/Cargo.toml delete mode 100644 winrt_bindings/build.rs diff --git a/Cargo.toml b/Cargo.toml index 8325028..3502664 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,9 @@ env_logger = "0.8" [target.'cfg(windows)'.dependencies] tolk = { version = "0.3", optional = true } windows = "0.4" -tts_winrt_bindings = { version = "0.3", path="winrt_bindings" } + +[target.'cfg(windows)'.build-dependencies] +windows = "0.4" [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.7" diff --git a/build.rs b/build.rs index 8b1edc2..6df6312 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,13 @@ fn main() { - if std::env::var("TARGET").unwrap().contains("-apple") { + 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::speech_synthesis::{SpeechSynthesisStream, SpeechSynthesizer, SpeechSynthesizerOptions}, + windows::storage::streams::IRandomAccessStream, + ); + } else if std::env::var("TARGET").unwrap().contains("-apple") { println!("cargo:rustc-link-lib=framework=AVFoundation"); if !std::env::var("CARGO_CFG_TARGET_OS") .unwrap() 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 97% rename from src/backends/winrt.rs rename to src/backends/winrt/mod.rs index d9b7704..5aec81d 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt/mod.rs @@ -5,11 +5,16 @@ 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::{MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory}, + speech_synthesis::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}; diff --git a/winrt_bindings/Cargo.toml b/winrt_bindings/Cargo.toml deleted file mode 100644 index da14f55..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.4" - -[build-dependencies] -windows = "0.4" diff --git a/winrt_bindings/build.rs b/winrt_bindings/build.rs deleted file mode 100644 index 3080588..0000000 --- a/winrt_bindings/build.rs +++ /dev/null @@ -1,9 +0,0 @@ -fn main() { - windows::build!( - windows::foundation::{EventRegistrationToken, IAsyncOperation, TypedEventHandler}, - windows::media::core::MediaSource, - windows::media::playback::{MediaPlaybackSession, MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory}, - windows::media::speech_synthesis::{SpeechSynthesisStream, SpeechSynthesizer, SpeechSynthesizerOptions}, - windows::storage::streams::IRandomAccessStream, - ); -} From 6784bb8861780e373098155306774aeba25f7b9d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 11 Mar 2021 13:23:08 -0600 Subject: [PATCH 106/196] Remove bindings publish from CI. --- .github/workflows/release.yml | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 87f292d..6bdc02d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,27 +62,6 @@ jobs: 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 - 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 - publish: name: Publish runs-on: ubuntu-latest From 2fd98c0a52528f64adcfcb84cb18e621d2936ac5 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 11 Mar 2021 13:28:30 -0600 Subject: [PATCH 107/196] Add windows crate to generic build-dependencies so the build script works everywhere. --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3502664..358fff2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,9 @@ lazy_static = "1" log = "0.4" thiserror = "1" +[build-dependencies] +windows = "0.4" + [dev-dependencies] env_logger = "0.8" @@ -24,9 +27,6 @@ env_logger = "0.8" tolk = { version = "0.3", optional = true } windows = "0.4" -[target.'cfg(windows)'.build-dependencies] -windows = "0.4" - [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.7" From 6664ca89e3b002a7c881443c4727ebfa1e4ffcc2 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 11 Mar 2021 13:38:44 -0600 Subject: [PATCH 108/196] Revert "Add windows crate to generic build-dependencies so the build script works everywhere." This reverts commit 2fd98c0a52528f64adcfcb84cb18e621d2936ac5. --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 358fff2..3502664 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,6 @@ lazy_static = "1" log = "0.4" thiserror = "1" -[build-dependencies] -windows = "0.4" - [dev-dependencies] env_logger = "0.8" @@ -27,6 +24,9 @@ env_logger = "0.8" tolk = { version = "0.3", optional = true } windows = "0.4" +[target.'cfg(windows)'.build-dependencies] +windows = "0.4" + [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.7" From c9279804b759c3dbf7a126fdd4c5a731b6d88997 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 11 Mar 2021 13:41:03 -0600 Subject: [PATCH 109/196] Different approach. --- build.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.rs b/build.rs index 6df6312..889acf1 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,5 @@ fn main() { + #[cfg(windows)] if std::env::var("TARGET").unwrap().contains("windows") { windows::build!( windows::foundation::{EventRegistrationToken, IAsyncOperation, TypedEventHandler}, @@ -7,7 +8,8 @@ fn main() { windows::media::speech_synthesis::{SpeechSynthesisStream, SpeechSynthesizer, SpeechSynthesizerOptions}, windows::storage::streams::IRandomAccessStream, ); - } else if std::env::var("TARGET").unwrap().contains("-apple") { + } + if std::env::var("TARGET").unwrap().contains("-apple") { println!("cargo:rustc-link-lib=framework=AVFoundation"); if !std::env::var("CARGO_CFG_TARGET_OS") .unwrap() From 8ba1f91617d242de7e9c106a89ada367b7257630 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 11 Mar 2021 13:41:26 -0600 Subject: [PATCH 110/196] Ignore DLL files. --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 2bd324b08b9aa9d56effb97dbc622d69269c23d1 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 11 Mar 2021 13:44:00 -0600 Subject: [PATCH 111/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3502664..9015098 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.14.0" +version = "0.15.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From a22ee537275170362173cca65107b6844865007c Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 12 Mar 2021 05:48:14 -0600 Subject: [PATCH 112/196] Fix Clippy warnings. --- src/backends/av_foundation.rs | 2 +- src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index dbca640..a87bf69 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -116,7 +116,7 @@ impl AvFoundation { AvFoundation { id: BackendId::AvFoundation(*backend_id), delegate: delegate_obj, - synth: synth, + synth, rate: 0.5, volume: 1., pitch: 1., diff --git a/src/lib.rs b/src/lib.rs index d2b80d1..89a1136 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -243,9 +243,9 @@ 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 { From 1d075f7ececc3a153bf23111014a798e7d82dbbe Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 12 Mar 2021 05:50:08 -0600 Subject: [PATCH 113/196] When speech is interrupted on AVFoundation, only stop if already speaking. May address a possible deadlock. --- src/backends/av_foundation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index a87bf69..6e5f38d 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -145,7 +145,7 @@ 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; From 1f510120a57083f2baecbd2b6aad37988399b1b1 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 12 Mar 2021 05:58:30 -0600 Subject: [PATCH 114/196] Add trace logging in AVFoundation backend. --- src/backends/av_foundation.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 6e5f38d..3dcc7dc 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -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 { From 81eba9959458295590a8bf48d2a886df8b82a035 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 12 Mar 2021 06:20:05 -0600 Subject: [PATCH 115/196] Add cast to (hopefully) get AppKit compiling on M1 macs. --- src/backends/appkit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/appkit.rs b/src/backends/appkit.rs index 97f6cb4..8035e26 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 == YES as i8) } } From e91637a67cbd908d922e16e0b462dfa0cc3c834c Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 12 Mar 2021 06:28:02 -0600 Subject: [PATCH 116/196] Add even more trace logging. --- src/backends/av_foundation.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 3dcc7dc..b071370 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -226,6 +226,7 @@ impl Backend for AvFoundation { } fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> { + trace!("set_pitch({})", pitch); self.pitch = pitch; Ok(()) } @@ -247,11 +248,13 @@ 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) } From 290eb06d02ae9cc880b09467448a89293b17dc40 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 12 Mar 2021 06:38:46 -0600 Subject: [PATCH 117/196] Even more trace logging. --- src/backends/av_foundation.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index b071370..5f93394 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -168,13 +168,21 @@ impl Backend for AvFoundation { } let utterance: id; unsafe { + trace!("Allocating utterance string"); let str = NSString::alloc(nil).init_str(text); + trace!("Allocating utterance"); utterance = msg_send![class!(AVSpeechUtterance), alloc]; + trace!("Initializing utterance"); let _: () = 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))) } From cdfb7ddb7782f0faa00f23a8074da1571b114b32 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 12 Mar 2021 06:59:49 -0600 Subject: [PATCH 118/196] Even more bloody logging. --- src/backends/av_foundation.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 5f93394..d7a747b 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -125,12 +125,16 @@ 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, From 00485d6cd8eda67ea66527be8de38d7b243a9309 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 12 Mar 2021 08:36:52 -0600 Subject: [PATCH 119/196] Enable 'exception' feature to hopefully catch and surface ObjC exceptions. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9015098..ed29fcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ 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" From bd8e2ee20a17d786dd7fd2b858d29c065be2310f Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 15 Mar 2021 13:02:32 -0500 Subject: [PATCH 120/196] Compare against ObjC NO to ensure correctness. --- src/backends/appkit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/appkit.rs b/src/backends/appkit.rs index 8035e26..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 as i8) + Ok(is_speaking != NO as i8) } } From c65c0022d821ae8ce65617c72dcfa701e166a726 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 15 Mar 2021 13:03:41 -0500 Subject: [PATCH 121/196] (Hopefully) initialize utterances correctly. --- src/backends/av_foundation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index d7a747b..a10830d 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -177,7 +177,7 @@ impl Backend for AvFoundation { trace!("Allocating utterance"); utterance = msg_send![class!(AVSpeechUtterance), alloc]; trace!("Initializing utterance"); - let _: () = msg_send![utterance, initWithString: str]; + 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); From 45255a804973d62cf78e5d389423aa3d70ff180c Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 15 Mar 2021 13:04:38 -0500 Subject: [PATCH 122/196] Fix another possibly broken comparison. --- src/backends/av_foundation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index a10830d..98c7870 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -268,7 +268,7 @@ impl Backend for AvFoundation { 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 != 0) } } From ed2d2e76c33f5c17e5eb10cc73fd9d12a31d739a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 15 Mar 2021 13:06:49 -0500 Subject: [PATCH 123/196] And this is what happens when I don't test on actual hardware. --- src/backends/av_foundation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 98c7870..566bc66 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -170,7 +170,7 @@ impl Backend for AvFoundation { if interrupt && self.is_speaking()? { self.stop()?; } - let utterance: id; + let mut utterance: id; unsafe { trace!("Allocating utterance string"); let str = NSString::alloc(nil).init_str(text); From 8c2aae7afd6fe4497e628389d8af235d56ab9c10 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 15 Mar 2021 13:46:22 -0500 Subject: [PATCH 124/196] Try another initialization fix. --- src/backends/av_foundation.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 566bc66..b9a8888 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -173,7 +173,8 @@ impl Backend for AvFoundation { let mut utterance: id; unsafe { trace!("Allocating utterance string"); - let str = NSString::alloc(nil).init_str(text); + let mut str = NSString::alloc(nil); + str = str.init_str(text); trace!("Allocating utterance"); utterance = msg_send![class!(AVSpeechUtterance), alloc]; trace!("Initializing utterance"); From 50528ce2d1e1e1e8f0e80e2e6a43e0cc950bbb17 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 15 Mar 2021 13:47:17 -0500 Subject: [PATCH 125/196] Another comparison check fix. --- src/backends/av_foundation.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index b9a8888..45a6fbe 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}; @@ -269,7 +269,7 @@ impl Backend for AvFoundation { fn is_speaking(&self) -> Result { trace!("is_speaking()"); let is_speaking: i8 = unsafe { msg_send![self.synth, isSpeaking] }; - Ok(is_speaking != 0) + Ok(is_speaking != NO) } } From fb7f1dddfcf9d734e4d6eeadaf0589135ee5193d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 15 Mar 2021 14:02:05 -0500 Subject: [PATCH 126/196] *sigh* Fix stupid M1/ARM casting issue. I hate Apple. --- src/backends/av_foundation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/av_foundation.rs b/src/backends/av_foundation.rs index 45a6fbe..68f95c9 100644 --- a/src/backends/av_foundation.rs +++ b/src/backends/av_foundation.rs @@ -269,7 +269,7 @@ impl Backend for AvFoundation { fn is_speaking(&self) -> Result { trace!("is_speaking()"); let is_speaking: i8 = unsafe { msg_send![self.synth, isSpeaking] }; - Ok(is_speaking != NO) + Ok(is_speaking != NO as i8) } } From 25f8211661acaef7c41c80ae0e8a6d68df5bcb30 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 16 Mar 2021 14:18:49 -0500 Subject: [PATCH 127/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ed29fcd..e37f780 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.15.0" +version = "0.15.1" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 153075ebab7e620410b3d0c367652c52e091e25a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 16 Mar 2021 17:33:01 -0500 Subject: [PATCH 128/196] Add web example Closes #1 --- Makefile.toml | 14 ++++- examples/web/.cargo/config | 2 + examples/web/.gitignore | 1 + examples/web/Cargo.toml | 11 ++++ examples/web/index.html | 12 ++++ examples/web/src/main.rs | 111 +++++++++++++++++++++++++++++++++++++ 6 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 examples/web/.cargo/config create mode 100644 examples/web/.gitignore create mode 100644 examples/web/Cargo.toml create mode 100644 examples/web/index.html create mode 100644 examples/web/src/main.rs diff --git a/Makefile.toml b/Makefile.toml index 6697f15..d5cda6b 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -12,4 +12,16 @@ script = [ [tasks.log-android] command = "adb" -args = ["logcat", "RustStdoutStderr:D", "*:S"] \ No newline at end of file +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.run-web-example] +dependencies = ["install-trunk", "install-wasm-bindgen-cli"] +cwd = "examples/web" +command = "trunk" +args = ["serve"] \ No newline at end of file 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..5f35742 --- /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); +} From acccdfeadae72bc92348e9680a124123711b154a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 16 Mar 2021 17:36:29 -0500 Subject: [PATCH 129/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e37f780..532af58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.15.1" +version = "0.15.2" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From ef96042b1265b8bd68f92f8b4b4f43247c29bd31 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 31 Mar 2021 10:38:32 -0500 Subject: [PATCH 130/196] Bump Windows dependency and update accordingly. Also, fix an acronym Clippy warning. --- Cargo.toml | 4 +-- build.rs | 10 +++--- src/backends/winrt/mod.rs | 76 +++++++++++++++++++-------------------- src/lib.rs | 18 +++++----- 4 files changed, 53 insertions(+), 55 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 532af58..74f24fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,10 +22,10 @@ env_logger = "0.8" [target.'cfg(windows)'.dependencies] tolk = { version = "0.3", optional = true } -windows = "0.4" +windows = "0.7" [target.'cfg(windows)'.build-dependencies] -windows = "0.4" +windows = "0.7" [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.7" diff --git a/build.rs b/build.rs index 889acf1..e2e2035 100644 --- a/build.rs +++ b/build.rs @@ -2,11 +2,11 @@ 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::speech_synthesis::{SpeechSynthesisStream, SpeechSynthesizer, SpeechSynthesizerOptions}, - windows::storage::streams::IRandomAccessStream, + 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") { diff --git a/src/backends/winrt/mod.rs b/src/backends/winrt/mod.rs index 5aec81d..9b2d0f0 100644 --- a/src/backends/winrt/mod.rs +++ b/src/backends/winrt/mod.rs @@ -7,12 +7,12 @@ use log::{info, trace}; mod bindings; -use bindings::windows::{ - foundation::TypedEventHandler, - media::{ - core::MediaSource, - playback::{MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory}, - speech_synthesis::SpeechSynthesizer, +use bindings::Windows::{ + Foundation::TypedEventHandler, + Media::{ + Core::MediaSource, + Playback::{MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory}, + SpeechSynthesis::SpeechSynthesizer, }, }; @@ -20,12 +20,12 @@ 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, @@ -59,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); { @@ -81,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(); @@ -102,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); } @@ -138,7 +136,7 @@ impl WinRT { } } -impl Backend for WinRT { +impl Backend for WinRt { fn id(&self) -> Option { Some(self.id) } @@ -164,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 }; @@ -184,16 +182,16 @@ impl Backend for WinRT { } } if no_utterances - && self.player.playback_session()?.playback_state()? != MediaPlaybackState::Playing + && self.player.PlaybackSession()?.PlaybackState()? != 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()?; + 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() { @@ -221,7 +219,7 @@ impl Backend for WinRT { if let Some(utterances) = utterances.get_mut(&self.id) { utterances.clear(); } - self.player.pause()?; + self.player.Pause()?; Ok(()) } @@ -238,7 +236,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) } @@ -260,7 +258,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) } @@ -282,7 +280,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) } @@ -298,7 +296,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 89a1136..998fd02 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,7 +39,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"))] @@ -55,7 +55,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")] @@ -69,7 +69,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")] @@ -105,7 +105,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")] @@ -115,7 +115,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")] @@ -197,8 +197,8 @@ impl TTS { } } #[cfg(windows)] - Backends::WinRT => { - let tts = backends::WinRT::new()?; + Backends::WinRt => { + let tts = backends::WinRt::new()?; Ok(TTS(Box::new(tts))) } #[cfg(target_os = "macos")] @@ -229,10 +229,10 @@ impl TTS { 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); #[cfg(target_os = "macos")] From 57f91105ec7bc86d0eedb09e8d82a46c8e6cc093 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 31 Mar 2021 10:40:42 -0500 Subject: [PATCH 131/196] s/TTS/Tts/ as per Clippy's acronym warnings. --- examples/99bottles.rs | 2 +- examples/hello_world.rs | 2 +- examples/latency.rs | 2 +- examples/ramble.rs | 2 +- src/lib.rs | 36 ++++++++++++++++++------------------ 5 files changed, 22 insertions(+), 22 deletions(-) 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/hello_world.rs b/examples/hello_world.rs index f3fa7a8..f737f3f 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -11,7 +11,7 @@ use tts::*; fn main() -> Result<(), Error> { env_logger::init(); - let mut tts = TTS::default()?; + let mut tts = Tts::default()?; 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/src/lib.rs b/src/lib.rs index 998fd02..fc50015 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -168,17 +168,17 @@ 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()))), @@ -191,7 +191,7 @@ impl TTS { 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) } @@ -199,7 +199,7 @@ impl TTS { #[cfg(windows)] Backends::WinRt => { let tts = backends::WinRt::new()?; - Ok(TTS(Box::new(tts))) + Ok(Tts(Box::new(tts))) } #[cfg(target_os = "macos")] Backends::AppKit => Ok(TTS(Box::new(backends::AppKit::new()))), @@ -222,19 +222,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. @@ -249,15 +249,15 @@ impl TTS { 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 } @@ -529,7 +529,7 @@ impl TTS { } } -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(); From 336c266ed47927eab8b574b03c396bc906935ef7 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 31 Mar 2021 10:53:08 -0500 Subject: [PATCH 132/196] Missed a few... --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fc50015..d67ad48 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -181,11 +181,11 @@ impl Tts { 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 => { From 6dbf9b7ddce8f5af968f7ebf58f99f1ebd6feaa0 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 31 Mar 2021 11:01:26 -0500 Subject: [PATCH 133/196] Find/replace is failing me today. --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d67ad48..3cc5e67 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -202,9 +202,9 @@ impl Tts { 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()?; From d9639c049bdcafc8d4d36ca86223bd7f1a4bf1bd Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 31 Mar 2021 11:03:14 -0500 Subject: [PATCH 134/196] S/TTS/Tts/ here as well. --- examples/android/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, .. From 1011704b824f955a57f61170ccf63cb41fb8f8d3 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Wed, 31 Mar 2021 11:12:42 -0500 Subject: [PATCH 135/196] And again, VSCode's find/replace didn't catch this. *grumble* --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 3cc5e67..1901556 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -208,7 +208,7 @@ impl Tts { #[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 { From debab7de17ea2cb8eb60cccaa1de3550387531a5 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Sat, 3 Apr 2021 11:11:37 -0500 Subject: [PATCH 136/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 74f24fb..4c57063 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.15.2" +version = "0.16.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From f7239366f0278ffb0cd7ce7dead9ffa06cff6de0 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Sat, 3 Apr 2021 11:49:44 -0500 Subject: [PATCH 137/196] Add command to build the web example. --- Makefile.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Makefile.toml b/Makefile.toml index d5cda6b..97655f3 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -20,6 +20,12 @@ 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" From d5a692008a52edaa366828e3d992843a8eab6105 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Sat, 3 Apr 2021 11:50:11 -0500 Subject: [PATCH 138/196] Add action to ensure that web example compiles. --- .github/workflows/test.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 50d9534..65df44c 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: + profile: minimal + toolchain: stable + components: rustfmt, clippy + override: true + - uses: actions-rs/install@v0.1 + with: + crate: cargo-make + - run: rustup target add wasm32-unknown-unknown + - uses: actions-rs/cargo@v1 + with: + command: make + args: From a879b3dca3696821781c7de96751b5192895aada Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Sat, 3 Apr 2021 11:58:23 -0500 Subject: [PATCH 139/196] Get web example compiling. --- examples/web/src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/web/src/main.rs b/examples/web/src/main.rs index 5f35742..fb03c38 100644 --- a/examples/web/src/main.rs +++ b/examples/web/src/main.rs @@ -1,12 +1,12 @@ #![allow(clippy::wildcard_imports)] use seed::{prelude::*, *}; -use tts::TTS; +use tts::Tts; #[derive(Clone)] struct Model { text: String, - tts: TTS, + tts: Tts, } #[derive(Clone)] @@ -19,7 +19,7 @@ enum Msg { } fn init(_: Url, _: &mut impl Orders) -> Model { - let tts = TTS::default().unwrap(); + let tts = Tts::default().unwrap(); Model { text: Default::default(), tts, From 26d06fc635078aabc500b7e65100883529e9438f Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Sat, 3 Apr 2021 12:11:53 -0500 Subject: [PATCH 140/196] No really, build the web example. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 65df44c..699673a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -102,4 +102,4 @@ jobs: - uses: actions-rs/cargo@v1 with: command: make - args: + args: [build-web-example] From 316b1bceec72589b5953fbddddafe2750bb263b4 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Sat, 3 Apr 2021 12:58:19 -0500 Subject: [PATCH 141/196] Use target as part of toolchain installation. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 699673a..550378b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,6 +91,7 @@ jobs: - uses: Swatinem/rust-cache@v1 - uses: actions-rs/toolchain@v1 with: + target: wasm32-unknown-unknown profile: minimal toolchain: stable components: rustfmt, clippy @@ -98,7 +99,6 @@ jobs: - uses: actions-rs/install@v0.1 with: crate: cargo-make - - run: rustup target add wasm32-unknown-unknown - uses: actions-rs/cargo@v1 with: command: make From e4b53d17aaf0f91cc48f90bf37173bd68c5ee929 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Sat, 3 Apr 2021 12:59:32 -0500 Subject: [PATCH 142/196] Check web example as part of release process. --- .github/workflows/release.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6bdc02d..2b0ea83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,6 +61,13 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} args: --all-features --target wasm32-unknown-unknown + - uses: actions-rs/install@v0.1 + with: + crate: cargo-make + - uses: actions-rs/cargo@v1 + with: + command: make + args: [build-web-example] publish: name: Publish From 7b8da53d812d042cc7f445b0a521282a9764989e Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 5 Apr 2021 07:45:08 -0500 Subject: [PATCH 143/196] Args shouldn't be a list. --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b0ea83..06b847b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,7 +67,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: make - args: [build-web-example] + args: build-web-example publish: name: Publish diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 550378b..ff68b02 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -102,4 +102,4 @@ jobs: - uses: actions-rs/cargo@v1 with: command: make - args: [build-web-example] + args: build-web-example From 4088eb12a172a70e581f5fb627e505bacbe34029 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 11 May 2021 19:37:56 -0500 Subject: [PATCH 144/196] Add ability to detect screen readers. Windows-only for now, and requires the `tolk` feature. --- examples/hello_world.rs | 5 +++++ src/lib.rs | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/examples/hello_world.rs b/examples/hello_world.rs index f737f3f..1db3ed3 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -12,6 +12,11 @@ use tts::*; fn main() -> Result<(), Error> { env_logger::init(); 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/src/lib.rs b/src/lib.rs index 1901556..50af8ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -527,6 +527,23 @@ impl Tts { Err(Error::UnsupportedFeature) } } + + /* + * Returns `true` if a screen reader is available to provide speech. + */ + pub fn screen_reader_available() -> bool { + if cfg!(target_os = "windows") { + #[cfg(feature = "tolk")] + { + let tolk = tolk::Tolk::new(); + return tolk.detect_screen_reader().is_some(); + } + #[cfg(not(feature = "tolk"))] + return false; + } else { + false + } + } } impl Drop for Tts { From 86b2e07f156e67bc591e0be24379d59395b9f132 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 11 May 2021 19:38:39 -0500 Subject: [PATCH 145/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4c57063..0749349 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.16.0" +version = "0.17.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 8f5f58028ac367ac4565e9b89c0e7b85da3d2dba Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 11 May 2021 20:18:14 -0500 Subject: [PATCH 146/196] Use attributes instead. --- src/lib.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 50af8ac..9b742a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -531,8 +531,10 @@ impl Tts { /* * Returns `true` if a screen reader is available to provide speech. */ + #[allow(unreachable_code)] pub fn screen_reader_available() -> bool { - if cfg!(target_os = "windows") { + #[cfg(target_os = "windows")] + { #[cfg(feature = "tolk")] { let tolk = tolk::Tolk::new(); @@ -540,9 +542,8 @@ impl Tts { } #[cfg(not(feature = "tolk"))] return false; - } else { - false } + false } } From d67bf8344a24ac233258186e0ec2dd96b06981dd Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 11 May 2021 20:21:03 -0500 Subject: [PATCH 147/196] Bump dependencies. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0749349..496e772 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,10 +22,10 @@ env_logger = "0.8" [target.'cfg(windows)'.dependencies] tolk = { version = "0.3", optional = true } -windows = "0.7" +windows = "0.9" [target.'cfg(windows)'.build-dependencies] -windows = "0.7" +windows = "0.9" [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.7" From d85d56c3eeed548832cf3806b1e23e40e63814db Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 11 May 2021 23:53:15 -0500 Subject: [PATCH 148/196] Bump version to work around Tolk crash. --- Cargo.toml | 4 ++-- src/backends/tolk.rs | 2 +- src/lib.rs | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 496e772..229c438 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.17.0" +version = "0.17.1" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -21,7 +21,7 @@ thiserror = "1" env_logger = "0.8" [target.'cfg(windows)'.dependencies] -tolk = { version = "0.3", optional = true } +tolk = { version = "0.4", optional = true } windows = "0.9" [target.'cfg(windows)'.build-dependencies] diff --git a/src/backends/tolk.rs b/src/backends/tolk.rs index ae16e4f..9b93a5e 100644 --- a/src/backends/tolk.rs +++ b/src/backends/tolk.rs @@ -11,7 +11,7 @@ impl Tolk { pub(crate) fn new() -> Option { info!("Initializing Tolk backend"); let tolk = TolkPtr::new(); - if tolk.detect_screen_reader().is_some() { + if tolk::Tolk::detect_screen_reader().is_some() { Some(Tolk(tolk)) } else { None diff --git a/src/lib.rs b/src/lib.rs index 9b742a7..6c0b5f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -537,8 +537,7 @@ impl Tts { { #[cfg(feature = "tolk")] { - let tolk = tolk::Tolk::new(); - return tolk.detect_screen_reader().is_some(); + return tolk::Tolk::detect_screen_reader().is_some(); } #[cfg(not(feature = "tolk"))] return false; From ca7789f157fa96ce06ffced8309e6ef043dfee23 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 20 May 2021 13:59:02 -0500 Subject: [PATCH 149/196] Bump version and Tolk dependency. --- Cargo.toml | 4 ++-- src/backends/tolk.rs | 6 ++++-- src/lib.rs | 5 ++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 229c438..bc3795d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.17.1" +version = "0.17.2" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -21,7 +21,7 @@ thiserror = "1" env_logger = "0.8" [target.'cfg(windows)'.dependencies] -tolk = { version = "0.4", optional = true } +tolk = { version = "0.5", optional = true } windows = "0.9" [target.'cfg(windows)'.build-dependencies] diff --git a/src/backends/tolk.rs b/src/backends/tolk.rs index 9b93a5e..51431a8 100644 --- a/src/backends/tolk.rs +++ b/src/backends/tolk.rs @@ -1,17 +1,19 @@ #[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 { info!("Initializing Tolk backend"); let tolk = TolkPtr::new(); - if tolk::Tolk::detect_screen_reader().is_some() { + if tolk.detect_screen_reader().is_some() { Some(Tolk(tolk)) } else { None diff --git a/src/lib.rs b/src/lib.rs index 6c0b5f8..d62c245 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; @@ -537,7 +539,8 @@ impl Tts { { #[cfg(feature = "tolk")] { - return tolk::Tolk::detect_screen_reader().is_some(); + let tolk = Tolk::new(); + return tolk.detect_screen_reader().is_some(); } #[cfg(not(feature = "tolk"))] return false; From c4038149a8b561ec72fd7f36de01772afa9737bc Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 20 May 2021 17:07:55 -0500 Subject: [PATCH 150/196] Remove a conditional that blocked playback in some circumstances on the WinRT backend. --- src/backends/winrt/mod.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/backends/winrt/mod.rs b/src/backends/winrt/mod.rs index 9b2d0f0..af9ca34 100644 --- a/src/backends/winrt/mod.rs +++ b/src/backends/winrt/mod.rs @@ -11,7 +11,7 @@ use bindings::Windows::{ Foundation::TypedEventHandler, Media::{ Core::MediaSource, - Playback::{MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory}, + Playback::{MediaPlayer, MediaPlayerAudioCategory}, SpeechSynthesis::SpeechSynthesizer, }, }; @@ -181,9 +181,7 @@ impl Backend for WinRt { utterances.push_back(utterance); } } - if no_utterances - && self.player.PlaybackSession()?.PlaybackState()? != MediaPlaybackState::Playing - { + if no_utterances { self.synth.Options()?.SetSpeakingRate(self.rate.into())?; self.synth.Options()?.SetAudioPitch(self.pitch.into())?; self.synth.Options()?.SetAudioVolume(self.volume.into())?; From dc3129b79c917f509d81495ade1b27f6f0cf8e03 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 20 May 2021 17:08:23 -0500 Subject: [PATCH 151/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bc3795d..fcf5bfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.17.2" +version = "0.17.3" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 92538fbdb869eeb29d8114f8e7338bb662308679 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 1 Nov 2021 10:36:15 -0500 Subject: [PATCH 152/196] Upgrade windows-rs to 0.23. --- Cargo.toml | 14 ++++++++++---- build.rs | 10 ---------- src/backends/{winrt/mod.rs => winrt.rs} | 9 +++------ src/backends/winrt/bindings.rs | 1 - src/lib.rs | 2 +- 5 files changed, 14 insertions(+), 22 deletions(-) rename src/backends/{winrt/mod.rs => winrt.rs} (98%) delete mode 100644 src/backends/winrt/bindings.rs diff --git a/Cargo.toml b/Cargo.toml index fcf5bfd..1b52894 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,12 +20,18 @@ thiserror = "1" [dev-dependencies] env_logger = "0.8" +[dependencies.windows] +version = "0.23" +features = [ + "Foundation", + "Media_Core", + "Media_Playback", + "Media_SpeechSynthesis", + "Storage_Streams", +] + [target.'cfg(windows)'.dependencies] 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" diff --git a/build.rs b/build.rs index e2e2035..8b1edc2 100644 --- a/build.rs +++ b/build.rs @@ -1,14 +1,4 @@ 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/src/backends/winrt/mod.rs b/src/backends/winrt.rs similarity index 98% rename from src/backends/winrt/mod.rs rename to src/backends/winrt.rs index af9ca34..00ccc51 100644 --- a/src/backends/winrt/mod.rs +++ b/src/backends/winrt.rs @@ -4,10 +4,7 @@ use std::sync::Mutex; use lazy_static::lazy_static; use log::{info, trace}; - -mod bindings; - -use bindings::Windows::{ +use windows::{ Foundation::TypedEventHandler, Media::{ Core::MediaSource, @@ -18,8 +15,8 @@ use bindings::Windows::{ use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; -impl From for Error { - fn from(e: windows::Error) -> Self { +impl From for Error { + fn from(e: windows::runtime::Error) -> Self { Error::WinRt(e) } } diff --git a/src/backends/winrt/bindings.rs b/src/backends/winrt/bindings.rs deleted file mode 100644 index 7915760..0000000 --- a/src/backends/winrt/bindings.rs +++ /dev/null @@ -1 +0,0 @@ -::windows::include_bindings!(); diff --git a/src/lib.rs b/src/lib.rs index d62c245..f91ceac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,7 +117,7 @@ pub enum Error { JavaScriptError(wasm_bindgen::JsValue), #[cfg(windows)] #[error("WinRT error")] - WinRt(windows::Error), + WinRt(windows::runtime::Error), #[error("Unsupported feature")] UnsupportedFeature, #[error("Out of range")] From a703e790ecfaf872fac1a9f1df01fee2b9297ac9 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 1 Nov 2021 10:38:26 -0500 Subject: [PATCH 153/196] Bump edition and version. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1b52894..976f5fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "tts" -version = "0.17.3" +version = "0.18.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" license = "MIT" exclude = ["*.cfg", "*.yml"] -edition = "2018" +edition = "2021" [lib] crate-type = ["lib", "cdylib", "staticlib"] From f8dbc04c36e834d225acab4331b7a36ce4a4a470 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 1 Nov 2021 10:39:58 -0500 Subject: [PATCH 154/196] Bump dependencies. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 976f5fe..b95a3d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ log = "0.4" thiserror = "1" [dev-dependencies] -env_logger = "0.8" +env_logger = "0.9" [dependencies.windows] version = "0.23" @@ -47,4 +47,4 @@ web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "Spee [target.'cfg(target_os="android")'.dependencies] jni = "0.19" -ndk-glue = "0.3" \ No newline at end of file +ndk-glue = "0.4" \ No newline at end of file From c12f328cf278b180988d78c935d5c37780e8c9f4 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 8 Nov 2021 07:27:35 -0600 Subject: [PATCH 155/196] Bump dependency. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b95a3d0..07086bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ thiserror = "1" env_logger = "0.9" [dependencies.windows] -version = "0.23" +version = "0.25" features = [ "Foundation", "Media_Core", From 562489e5afc120313908966d1271cbd5bab12327 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 8 Nov 2021 07:28:07 -0600 Subject: [PATCH 156/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 07086bf..97a4456 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.18.0" +version = "0.18.1" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From d5bdb9f4981a024b92decfb161d4d406ac14790c Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 15 Nov 2021 08:16:00 -0600 Subject: [PATCH 157/196] Bump version and dependency. --- Cargo.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 97a4456..3eac5e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.18.1" +version = "0.18.2" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -21,8 +21,9 @@ thiserror = "1" env_logger = "0.9" [dependencies.windows] -version = "0.25" +version = "0.26" features = [ + "std", "Foundation", "Media_Core", "Media_Playback", From 119678ae559e03a9728497fc273ebee6b502aab3 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 16 Nov 2021 11:13:31 -0600 Subject: [PATCH 158/196] Update to windows 0.27 and bump version. --- Cargo.toml | 5 +++-- src/backends/winrt.rs | 4 ++-- src/lib.rs | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3eac5e1..db2b561 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.18.2" +version = "0.18.3" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -21,8 +21,9 @@ thiserror = "1" env_logger = "0.9" [dependencies.windows] -version = "0.26" +version = "0.27" features = [ + "alloc", "std", "Foundation", "Media_Core", diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index 00ccc51..e7c8634 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -15,8 +15,8 @@ use windows::{ use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; -impl From for Error { - fn from(e: windows::runtime::Error) -> Self { +impl From for Error { + fn from(e: windows::core::Error) -> Self { Error::WinRt(e) } } diff --git a/src/lib.rs b/src/lib.rs index f91ceac..4247435 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,7 +117,7 @@ pub enum Error { JavaScriptError(wasm_bindgen::JsValue), #[cfg(windows)] #[error("WinRT error")] - WinRt(windows::runtime::Error), + WinRt(windows::core::Error), #[error("Unsupported feature")] UnsupportedFeature, #[error("Out of range")] From 57ffbf0e4fa17de7810168894e1088d90e26991a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 16 Nov 2021 11:36:24 -0600 Subject: [PATCH 159/196] Make windows dependency platform-specific and add alloc feature. --- Cargo.toml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index db2b561..c2542a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,20 +20,9 @@ thiserror = "1" [dev-dependencies] env_logger = "0.9" -[dependencies.windows] -version = "0.27" -features = [ - "alloc", - "std", - "Foundation", - "Media_Core", - "Media_Playback", - "Media_SpeechSynthesis", - "Storage_Streams", -] - [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } +windows = { version = "0.27", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.7" From 47e164a0c8267c03384e5fe256ec00ab95c58917 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 19 Nov 2021 09:22:05 -0600 Subject: [PATCH 160/196] Support Speech Dispatcher initialization failures, and bump version. --- Cargo.toml | 4 ++-- src/backends/speech_dispatcher.rs | 9 ++++----- src/lib.rs | 11 +++++++++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c2542a9..fc478ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.18.3" +version = "0.19.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -25,7 +25,7 @@ tolk = { version = "0.5", optional = true } windows = { version = "0.27", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] -speech-dispatcher = "0.7" +speech-dispatcher = "0.8" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] cocoa-foundation = "0.1" diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index 39c7fd8..bd373c4 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -1,6 +1,5 @@ #[cfg(target_os = "linux")] -use std::collections::HashMap; -use std::sync::Mutex; +use std::{collections::HashMap, sync::Mutex}; use lazy_static::*; use log::{info, trace}; @@ -19,9 +18,9 @@ lazy_static! { } impl SpeechDispatcher { - pub(crate) fn new() -> Self { + pub(crate) fn new() -> std::result::Result { 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 mut speaking = SPEAKING.lock().unwrap(); speaking.insert(sd.0.client_id(), false); @@ -66,7 +65,7 @@ impl SpeechDispatcher { let mut speaking = SPEAKING.lock().unwrap(); speaking.insert(client_id, true); }))); - sd + Ok(sd) } } diff --git a/src/lib.rs b/src/lib.rs index 4247435..9488daa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,7 @@ use lazy_static::lazy_static; use libc::c_char; #[cfg(target_os = "macos")] use objc::{class, msg_send, sel, sel_impl}; +use speech_dispatcher::SpeechDispatcherError; use thiserror::Error; #[cfg(all(windows, feature = "tolk"))] use tolk::Tolk; @@ -113,8 +114,11 @@ pub enum Error { #[error("Operation failed")] OperationFailed, #[cfg(target_arch = "wasm32")] - #[error("JavaScript error: [0])]")] + #[error("JavaScript error: [0]")] JavaScriptError(wasm_bindgen::JsValue), + #[cfg(target_os = "linux")] + #[error("Speech Dispatcher error: {0}")] + SpeechDispatcher(#[from] SpeechDispatcherError), #[cfg(windows)] #[error("WinRT error")] WinRt(windows::core::Error), @@ -183,7 +187,10 @@ impl Tts { 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 => { + let tts = backends::SpeechDispatcher::new()?; + Ok(Tts(Box::new(tts))) + } #[cfg(target_arch = "wasm32")] Backends::Web => { let tts = backends::Web::new()?; From 89fd14d957529b5690ac4ffd0b1bff3fbe3a7c0a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 19 Nov 2021 09:24:58 -0600 Subject: [PATCH 161/196] Bump windows crate dependency. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index fc478ea..22d5acf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.27", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.28", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.8" From 94417b5351b6ba933c07ec16f6cc5793a2500c4d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 19 Nov 2021 09:25:37 -0600 Subject: [PATCH 162/196] Only import from speech_dispatcher when building for Linux. --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 9488daa..1b7b5db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,7 @@ use lazy_static::lazy_static; use libc::c_char; #[cfg(target_os = "macos")] use objc::{class, msg_send, sel, sel_impl}; +#[cfg(target_os = "linux")] use speech_dispatcher::SpeechDispatcherError; use thiserror::Error; #[cfg(all(windows, feature = "tolk"))] From d24d1a6a152eb3945e1ec491de4d4c52756d17bc Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 2 Dec 2021 09:24:22 -0600 Subject: [PATCH 163/196] Bump version and dependencies. --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 22d5acf..ef4beef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.19.0" +version = "0.19.1" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -25,7 +25,7 @@ tolk = { version = "0.5", optional = true } windows = { version = "0.28", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] -speech-dispatcher = "0.8" +speech-dispatcher = "0.9" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] cocoa-foundation = "0.1" @@ -38,4 +38,4 @@ web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "Spee [target.'cfg(target_os="android")'.dependencies] jni = "0.19" -ndk-glue = "0.4" \ No newline at end of file +ndk-glue = "0.5" \ No newline at end of file From ee8ec97ab4cefa824ec01303cb51ae5be18d6d36 Mon Sep 17 00:00:00 2001 From: Malloc Voidstar <1284317+AlyoshaVasilieva@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:10:39 -0800 Subject: [PATCH 164/196] Bump ndk-glue version in Android example --- examples/android/cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/android/cargo.toml b/examples/android/cargo.toml index a7ff289..3585cca 100644 --- a/examples/android/cargo.toml +++ b/examples/android/cargo.toml @@ -10,5 +10,5 @@ edition = "2018" crate-type = ["dylib"] [dependencies] -ndk-glue = "0.2" +ndk-glue = "0.5" tts = { path = "../.." } \ No newline at end of file From 5e9c98b0639cf03c1896f7164d75e5c2870ce49a Mon Sep 17 00:00:00 2001 From: Malloc Voidstar <1284317+AlyoshaVasilieva@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:31:21 -0800 Subject: [PATCH 165/196] Also bump gradle.plugin.com.github.willir.rust:plugin 0.3.3 doesn't work with cargo-ndk 2+ --- examples/android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/android/build.gradle b/examples/android/build.gradle index dd9a15c..27b39c3 100644 --- a/examples/android/build.gradle +++ b/examples/android/build.gradle @@ -11,7 +11,7 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:4.1.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "gradle.plugin.com.github.willir.rust:plugin:0.3.3" + classpath "gradle.plugin.com.github.willir.rust:plugin:0.3.4" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } From bed6cfa2066c8e0d1b552387d7ac2620422d60cb Mon Sep 17 00:00:00 2001 From: Malloc Voidstar <1284317+AlyoshaVasilieva@users.noreply.github.com> Date: Fri, 10 Dec 2021 10:47:12 -0800 Subject: [PATCH 166/196] Exit Android initialization loop with error when stuck 500ms is fairly arbitrary; my emulator took 35 to run that loop. --- src/backends/android.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/backends/android.rs b/src/backends/android.rs index 55449c3..c47d86a 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -4,7 +4,7 @@ use std::ffi::{CStr, CString}; use std::os::raw::c_void; use std::sync::{Mutex, RwLock}; use std::thread; -use std::time::Duration; +use std::time::{Duration, Instant}; use jni::objects::{GlobalRef, JObject, JString}; use jni::sys::{jfloat, jint, JNI_VERSION_1_6}; @@ -198,12 +198,18 @@ impl Android { } let tts = env.new_global_ref(tts)?; // This hack makes my brain bleed. + const MAX_WAIT_TIME: Duration = Duration::from_millis(500); + let start = Instant::now(); + // Wait a max of 500ms for initialization, then return an error to avoid hanging. loop { { let pending = PENDING_INITIALIZATIONS.read().unwrap(); if !(*pending).contains(&bid) { break; } + if start.elapsed() > MAX_WAIT_TIME { + return Err(Error::OperationFailed); + } } thread::sleep(Duration::from_millis(5)); } From 9ed03753c25264094e0b98c14a92f2012f89abcf Mon Sep 17 00:00:00 2001 From: Raimundo Saona <37874270+saona-raimundo@users.noreply.github.com> Date: Wed, 22 Dec 2021 13:28:00 +0100 Subject: [PATCH 167/196] Common traits for Features --- Cargo.toml | 1 + src/lib.rs | 23 +++++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ef4beef..4ed7590 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ dyn-clonable = "0.9" lazy_static = "1" log = "0.4" thiserror = "1" +serde = { version = "1.0", optional = true, features = ["derive"] } [dev-dependencies] env_logger = "0.9" diff --git a/src/lib.rs b/src/lib.rs index 1b7b5db..0dbdea4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ use std::boxed::Box; use std::collections::HashMap; #[cfg(target_os = "macos")] use std::ffi::CStr; +use std::fmt; use std::sync::Mutex; #[cfg(any(target_os = "macos", target_os = "ios"))] @@ -84,13 +85,15 @@ unsafe impl Send for UtteranceId {} unsafe impl Sync for UtteranceId {} +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Features { - pub stop: bool, - pub rate: bool, - pub pitch: bool, - pub volume: bool, pub is_speaking: bool, + pub pitch: bool, + pub rate: bool, + pub stop: bool, pub utterance_callbacks: bool, + pub volume: bool, } impl Default for Features { @@ -106,6 +109,18 @@ impl Default for Features { } } +impl fmt::Display for Features { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + writeln!(f, "{:#?}", self) + } +} + +impl Features { + pub fn new() -> Self { + Self::default() + } +} + #[derive(Debug, Error)] pub enum Error { #[error("IO error: {0}")] From e20170583dbb404999617bf064608eab7024aa69 Mon Sep 17 00:00:00 2001 From: Raimundo Saona <37874270+saona-raimundo@users.noreply.github.com> Date: Wed, 22 Dec 2021 15:38:11 +0100 Subject: [PATCH 168/196] Common traits for other structs --- src/lib.rs | 111 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 85 insertions(+), 26 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0dbdea4..8bb1141 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,50 +35,108 @@ use tolk::Tolk; mod backends; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Backends { - #[cfg(target_os = "linux")] - SpeechDispatcher, - #[cfg(target_arch = "wasm32")] - Web, - #[cfg(all(windows, feature = "tolk"))] - Tolk, - #[cfg(windows)] - WinRt, + #[cfg(target_os = "android")] + Android, #[cfg(target_os = "macos")] AppKit, #[cfg(any(target_os = "macos", target_os = "ios"))] AvFoundation, - #[cfg(target_os = "android")] - Android, + #[cfg(target_os = "linux")] + SpeechDispatcher, + #[cfg(all(windows, feature = "tolk"))] + Tolk, + #[cfg(target_arch = "wasm32")] + Web, + #[cfg(windows)] + WinRt, } -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +impl fmt::Display for Backends { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + #[cfg(target_os = "android")] + Backends::Android => writeln!(f, "Android"), + #[cfg(target_os = "macos")] + Backends::AppKit => writeln!(f, "AppKit"), + #[cfg(any(target_os = "macos", target_os = "ios"))] + Backends::AvFoundation => writeln!(f, "AVFoundation"), + #[cfg(target_os = "linux")] + Backends::SpeechDispatcher => writeln!(f, "Speech Dispatcher"), + #[cfg(all(windows, feature = "tolk"))] + Backends::Tolk => writeln!(f, "Tolk"), + #[cfg(target_arch = "wasm32")] + Backends::Web => writeln!(f, "Web"), + #[cfg(windows)] + Backends::WinRt => writeln!(f, "Windows Runtime"), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum BackendId { - #[cfg(target_os = "linux")] - SpeechDispatcher(u64), - #[cfg(target_arch = "wasm32")] - Web(u64), - #[cfg(windows)] - WinRt(u64), + #[cfg(target_os = "android")] + Android(u64), #[cfg(any(target_os = "macos", target_os = "ios"))] AvFoundation(u64), - #[cfg(target_os = "android")] - Android(u64), -} - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum UtteranceId { #[cfg(target_os = "linux")] SpeechDispatcher(u64), #[cfg(target_arch = "wasm32")] Web(u64), #[cfg(windows)] WinRt(u64), - #[cfg(any(target_os = "macos", target_os = "ios"))] - AvFoundation(id), +} + +impl fmt::Display for BackendId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + #[cfg(target_os = "android")] + BackendId::Android(id) => writeln!(f, "{}", id), + #[cfg(any(target_os = "macos", target_os = "ios"))] + BackendId::AvFoundation(id) => writeln!(f, "{}", id), + #[cfg(target_os = "linux")] + BackendId::SpeechDispatcher(id) => writeln!(f, "{}", id), + #[cfg(target_arch = "wasm32")] + BackendId::Web(id) => writeln!(f, "Web({})", id), + #[cfg(windows)] + BackendId::WinRt(id) => writeln!(f, "{}", id), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum UtteranceId { #[cfg(target_os = "android")] Android(u64), + #[cfg(any(target_os = "macos", target_os = "ios"))] + AvFoundation(id), + #[cfg(target_os = "linux")] + SpeechDispatcher(u64), + #[cfg(target_arch = "wasm32")] + Web(u64), + #[cfg(windows)] + WinRt(u64), +} + +impl fmt::Display for UtteranceId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + #[cfg(target_os = "android")] + UtteranceId::Android(id) => writeln!(f, "{}", id), + #[cfg(any(target_os = "macos", target_os = "ios"))] + UtteranceId::AvFoundation(id) => writeln!(f, "{}", id), + #[cfg(target_os = "linux")] + UtteranceId::SpeechDispatcher(id) => writeln!(f, "{}", id), + #[cfg(target_arch = "wasm32")] + UtteranceId::Web(id) => writeln!(f, "Web({})", id), + #[cfg(windows)] + UtteranceId::WinRt(id) => writeln!(f, "{}", id), + } + } } unsafe impl Send for UtteranceId {} @@ -122,6 +180,7 @@ impl Features { } #[derive(Debug, Error)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Error { #[error("IO error: {0}")] Io(#[from] std::io::Error), From 5331bc8daf0bc431676a9c7369d2ee36602e49f1 Mon Sep 17 00:00:00 2001 From: Raimundo Saona <37874270+saona-raimundo@users.noreply.github.com> Date: Thu, 23 Dec 2021 11:12:35 +0100 Subject: [PATCH 169/196] Undo implementation of serde traits for Error --- src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 8bb1141..9284ccb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -180,7 +180,6 @@ impl Features { } #[derive(Debug, Error)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Error { #[error("IO error: {0}")] Io(#[from] std::io::Error), From 2ea472e196fb93a52254a44ef0e956683df4f87d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 28 Dec 2021 10:09:54 -0600 Subject: [PATCH 170/196] Bump Windows dependency, crate version, and remove `Debug` derive. --- Cargo.toml | 4 ++-- src/backends/winrt.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ef4beef..68332a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.19.1" +version = "0.19.2" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -22,7 +22,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.28", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.29", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.9" diff --git a/src/backends/winrt.rs b/src/backends/winrt.rs index e7c8634..943a78c 100644 --- a/src/backends/winrt.rs +++ b/src/backends/winrt.rs @@ -21,7 +21,7 @@ impl From for Error { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct WinRt { id: BackendId, synth: SpeechSynthesizer, From 114fb55fc98f18cfb0a83a3fa92ec605d24142e5 Mon Sep 17 00:00:00 2001 From: Raimundo Saona <37874270+saona-raimundo@users.noreply.github.com> Date: Tue, 4 Jan 2022 13:43:32 +0100 Subject: [PATCH 171/196] Fixing macos and ios restrictions --- src/lib.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9284ccb..0d35ba0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -107,8 +107,19 @@ impl fmt::Display for BackendId { } } -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +// # Note +// +// Most trait implementations are blocked by cocoa_foundation::base::id; +// which is a type alias for objc::runtime::Object, which only implements Debug. +#[derive(Debug)] +#[cfg_attr( + not(any(target_os = "macos", target_os = "ios")), + derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord) +)] +#[cfg_attr( + all(feature = "serde", not(any(target_os = "macos", target_os = "ios"))), + derive(serde::Serialize, serde::Deserialize) +)] pub enum UtteranceId { #[cfg(target_os = "android")] Android(u64), @@ -122,13 +133,16 @@ pub enum UtteranceId { WinRt(u64), } +// # Note +// +// Display is not implemented by cocoa_foundation::base::id; +// which is a type alias for objc::runtime::Object, which only implements Debug. +#[cfg(not(any(target_os = "macos", target_os = "ios")))] impl fmt::Display for UtteranceId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { #[cfg(target_os = "android")] UtteranceId::Android(id) => writeln!(f, "{}", id), - #[cfg(any(target_os = "macos", target_os = "ios"))] - UtteranceId::AvFoundation(id) => writeln!(f, "{}", id), #[cfg(target_os = "linux")] UtteranceId::SpeechDispatcher(id) => writeln!(f, "{}", id), #[cfg(target_arch = "wasm32")] From cdc225418e82135b781ceb2374432667027bb0f2 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 10 Jan 2022 10:40:24 -0600 Subject: [PATCH 172/196] Bump Android dependencies. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2859f0a..59b6da4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,4 +39,4 @@ web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "Spee [target.'cfg(target_os="android")'.dependencies] jni = "0.19" -ndk-glue = "0.5" \ No newline at end of file +ndk-glue = "0.6" \ No newline at end of file From 9066d2b00510f2d9f2059760b79347fefcb16cee Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 10 Jan 2022 10:51:18 -0600 Subject: [PATCH 173/196] Make formatting more consistent. --- src/backends/android.rs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/backends/android.rs b/src/backends/android.rs index c47d86a..5856890 100644 --- a/src/backends/android.rs +++ b/src/backends/android.rs @@ -1,14 +1,18 @@ #[cfg(target_os = "android")] -use std::collections::HashSet; -use std::ffi::{CStr, CString}; -use std::os::raw::c_void; -use std::sync::{Mutex, RwLock}; -use std::thread; -use std::time::{Duration, Instant}; +use std::{ + collections::HashSet, + ffi::{CStr, CString}, + os::raw::c_void, + sync::{Mutex, RwLock}, + thread, + time::{Duration, Instant}, +}; -use jni::objects::{GlobalRef, JObject, JString}; -use jni::sys::{jfloat, jint, JNI_VERSION_1_6}; -use jni::{JNIEnv, JavaVM}; +use jni::{ + objects::{GlobalRef, JObject, JString}, + sys::{jfloat, jint, JNI_VERSION_1_6}, + JNIEnv, JavaVM, +}; use lazy_static::lazy_static; use log::{error, info}; From 1f466275cfa365acc400616cae010625aceeb19e Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 10 Jan 2022 10:52:44 -0600 Subject: [PATCH 174/196] Bump ndk-glue version once more. --- examples/android/cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/android/cargo.toml b/examples/android/cargo.toml index 3585cca..0b565e8 100644 --- a/examples/android/cargo.toml +++ b/examples/android/cargo.toml @@ -10,5 +10,5 @@ edition = "2018" crate-type = ["dylib"] [dependencies] -ndk-glue = "0.5" +ndk-glue = "0.6" tts = { path = "../.." } \ No newline at end of file From dc00aa427f3ec4382dc7af18ab7494476f706723 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 10 Jan 2022 11:02:12 -0600 Subject: [PATCH 175/196] Bump speech-dispatcher dependency and update for new return types. --- Cargo.toml | 2 +- src/backends/speech_dispatcher.rs | 12 ++++++------ src/lib.rs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 59b6da4..5f081d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ tolk = { version = "0.5", optional = true } windows = { version = "0.29", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] -speech-dispatcher = "0.9" +speech-dispatcher = "0.10" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] cocoa-foundation = "0.1" diff --git a/src/backends/speech_dispatcher.rs b/src/backends/speech_dispatcher.rs index bd373c4..57ce7b2 100644 --- a/src/backends/speech_dispatcher.rs +++ b/src/backends/speech_dispatcher.rs @@ -92,11 +92,11 @@ impl Backend for SpeechDispatcher { } let single_char = text.to_string().capacity() == 1; if single_char { - self.0.set_punctuation(Punctuation::All); + self.0.set_punctuation(Punctuation::All)?; } let id = self.0.say(Priority::Important, text); if single_char { - self.0.set_punctuation(Punctuation::None); + self.0.set_punctuation(Punctuation::None)?; } if let Some(id) = id { Ok(Some(UtteranceId::SpeechDispatcher(id))) @@ -107,7 +107,7 @@ impl Backend for SpeechDispatcher { fn stop(&mut self) -> Result<(), Error> { trace!("stop()"); - self.0.cancel(); + self.0.cancel()?; Ok(()) } @@ -128,7 +128,7 @@ impl Backend for SpeechDispatcher { } fn set_rate(&mut self, rate: f32) -> Result<(), Error> { - self.0.set_voice_rate(rate as i32); + self.0.set_voice_rate(rate as i32)?; Ok(()) } @@ -149,7 +149,7 @@ impl Backend for SpeechDispatcher { } fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> { - self.0.set_voice_pitch(pitch as i32); + self.0.set_voice_pitch(pitch as i32)?; Ok(()) } @@ -170,7 +170,7 @@ impl Backend for SpeechDispatcher { } fn set_volume(&mut self, volume: f32) -> Result<(), Error> { - self.0.set_volume(volume as i32); + self.0.set_volume(volume as i32)?; Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 0d35ba0..0962396 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,7 +28,7 @@ use libc::c_char; #[cfg(target_os = "macos")] use objc::{class, msg_send, sel, sel_impl}; #[cfg(target_os = "linux")] -use speech_dispatcher::SpeechDispatcherError; +use speech_dispatcher::{Error as SpeechDispatcherError}; use thiserror::Error; #[cfg(all(windows, feature = "tolk"))] use tolk::Tolk; From 050b97fde1f19aa03c96457fd9dfe2fb062e70a8 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 10 Jan 2022 11:03:18 -0600 Subject: [PATCH 176/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5f081d5..3c9ff98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.19.2" +version = "0.20.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 1e1c04d4e51231ca0d7beadc5f4789faf0f0f55e Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 10 Jan 2022 11:10:18 -0600 Subject: [PATCH 177/196] Fix rustfmt error. --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 0962396..b157cf7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,7 +28,7 @@ use libc::c_char; #[cfg(target_os = "macos")] use objc::{class, msg_send, sel, sel_impl}; #[cfg(target_os = "linux")] -use speech_dispatcher::{Error as SpeechDispatcherError}; +use speech_dispatcher::Error as SpeechDispatcherError; use thiserror::Error; #[cfg(all(windows, feature = "tolk"))] use tolk::Tolk; From 660072809db9863ba44ec30215d8df99d76f98e0 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 13 Jan 2022 14:28:22 -0600 Subject: [PATCH 178/196] Bump version and windows-rs dependency. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3c9ff98..173f027 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.20.0" +version = "0.20.1" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -23,7 +23,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.29", features = ["alloc", "std", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.30", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.10" From 44669236204e8c80b5662456be9bbaa87df76f0d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 27 Jan 2022 10:56:11 -0600 Subject: [PATCH 179/196] Bump speech-dispatcher dependency. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 173f027..0de8652 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ tolk = { version = "0.5", optional = true } windows = { version = "0.30", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] -speech-dispatcher = "0.10" +speech-dispatcher = "0.11" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] cocoa-foundation = "0.1" From cd2216390c68851aa68a072fc00f62fa7dbb7fcb Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Sat, 5 Feb 2022 09:48:25 -0600 Subject: [PATCH 180/196] Bump dependency and version. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0de8652..710ec4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.20.1" +version = "0.20.2" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -26,7 +26,7 @@ tolk = { version = "0.5", optional = true } windows = { version = "0.30", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] -speech-dispatcher = "0.11" +speech-dispatcher = "0.12" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] cocoa-foundation = "0.1" From 9f8b670fe036d47d410af00a243bc56d1cf5f090 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 11 Feb 2022 09:17:46 -0600 Subject: [PATCH 181/196] Bump dependency. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 710ec4a..9c83f2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.30", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.32", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.12" From f310306508f274b92a42f99fee942b10c172e642 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 11 Feb 2022 09:20:22 -0600 Subject: [PATCH 182/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9c83f2b..b4cf23e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.20.2" +version = "0.20.3" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From 90f7dae6a119f96f54ae4bc469d3d723dd961a80 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 7 Mar 2022 10:31:37 -0600 Subject: [PATCH 183/196] Bump windows dependency and version. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b4cf23e..a15c441 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.20.3" +version = "0.20.4" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -23,7 +23,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.32", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.33", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.12" From 00bd5e62ff419bfa02d4e471afbe8cefd55ec571 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 7 Mar 2022 17:54:26 -0600 Subject: [PATCH 184/196] Switch `TTS` to use `Arc>>` to address soundness issues. --- src/lib.rs | 68 +++++++++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b157cf7..bfda4bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,12 +12,12 @@ * * WebAssembly */ -use std::boxed::Box; use std::collections::HashMap; #[cfg(target_os = "macos")] use std::ffi::CStr; use std::fmt; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; +use std::{boxed::Box, sync::RwLock}; #[cfg(any(target_os = "macos", target_os = "ios"))] use cocoa_foundation::base::id; @@ -262,7 +262,7 @@ lazy_static! { } #[derive(Clone)] -pub struct Tts(Box); +pub struct Tts(Arc>>); unsafe impl Send for Tts {} @@ -296,7 +296,7 @@ impl Tts { #[cfg(windows)] Backends::WinRt => { let tts = backends::WinRt::new()?; - Ok(Tts(Box::new(tts))) + Ok(Tts(Arc::new(RwLock::new(Box::new(tts))))) } #[cfg(target_os = "macos")] Backends::AppKit => Ok(Tts(Box::new(backends::AppKit::new()))), @@ -309,7 +309,7 @@ impl Tts { } }; if let Ok(backend) = backend { - if let Some(id) = backend.0.id() { + if let Some(id) = backend.0.read().unwrap().id() { let mut callbacks = CALLBACKS.lock().unwrap(); callbacks.insert(id, Callbacks::default()); } @@ -362,7 +362,7 @@ impl Tts { * Returns the features supported by this TTS engine */ pub fn supported_features(&self) -> Features { - self.0.supported_features() + self.0.read().unwrap().supported_features() } /** @@ -373,7 +373,10 @@ impl Tts { text: S, interrupt: bool, ) -> Result, Error> { - self.0.speak(text.into().as_str(), interrupt) + self.0 + .write() + .unwrap() + .speak(text.into().as_str(), interrupt) } /** @@ -382,7 +385,7 @@ impl Tts { pub fn stop(&mut self) -> Result<&Self, Error> { let Features { stop, .. } = self.supported_features(); if stop { - self.0.stop()?; + self.0.write().unwrap().stop()?; Ok(self) } else { Err(Error::UnsupportedFeature) @@ -393,21 +396,21 @@ impl Tts { * Returns the minimum rate for this speech synthesizer. */ pub fn min_rate(&self) -> f32 { - self.0.min_rate() + self.0.read().unwrap().min_rate() } /** * Returns the maximum rate for this speech synthesizer. */ pub fn max_rate(&self) -> f32 { - self.0.max_rate() + self.0.read().unwrap().max_rate() } /** * Returns the normal rate for this speech synthesizer. */ pub fn normal_rate(&self) -> f32 { - self.0.normal_rate() + self.0.read().unwrap().normal_rate() } /** @@ -416,7 +419,7 @@ impl Tts { pub fn get_rate(&self) -> Result { let Features { rate, .. } = self.supported_features(); if rate { - self.0.get_rate() + self.0.read().unwrap().get_rate() } else { Err(Error::UnsupportedFeature) } @@ -430,10 +433,11 @@ impl Tts { rate: rate_feature, .. } = self.supported_features(); if rate_feature { - if rate < self.0.min_rate() || rate > self.0.max_rate() { + let mut backend = self.0.write().unwrap(); + if rate < backend.min_rate() || rate > backend.max_rate() { Err(Error::OutOfRange) } else { - self.0.set_rate(rate)?; + backend.set_rate(rate)?; Ok(self) } } else { @@ -445,21 +449,21 @@ impl Tts { * Returns the minimum pitch for this speech synthesizer. */ pub fn min_pitch(&self) -> f32 { - self.0.min_pitch() + self.0.read().unwrap().min_pitch() } /** * Returns the maximum pitch for this speech synthesizer. */ pub fn max_pitch(&self) -> f32 { - self.0.max_pitch() + self.0.read().unwrap().max_pitch() } /** * Returns the normal pitch for this speech synthesizer. */ pub fn normal_pitch(&self) -> f32 { - self.0.normal_pitch() + self.0.read().unwrap().normal_pitch() } /** @@ -468,7 +472,7 @@ impl Tts { pub fn get_pitch(&self) -> Result { let Features { pitch, .. } = self.supported_features(); if pitch { - self.0.get_pitch() + self.0.read().unwrap().get_pitch() } else { Err(Error::UnsupportedFeature) } @@ -483,10 +487,11 @@ impl Tts { .. } = self.supported_features(); if pitch_feature { - if pitch < self.0.min_pitch() || pitch > self.0.max_pitch() { + let mut backend = self.0.write().unwrap(); + if pitch < backend.min_pitch() || pitch > backend.max_pitch() { Err(Error::OutOfRange) } else { - self.0.set_pitch(pitch)?; + backend.set_pitch(pitch)?; Ok(self) } } else { @@ -498,21 +503,21 @@ impl Tts { * Returns the minimum volume for this speech synthesizer. */ pub fn min_volume(&self) -> f32 { - self.0.min_volume() + self.0.read().unwrap().min_volume() } /** * Returns the maximum volume for this speech synthesizer. */ pub fn max_volume(&self) -> f32 { - self.0.max_volume() + self.0.read().unwrap().max_volume() } /** * Returns the normal volume for this speech synthesizer. */ pub fn normal_volume(&self) -> f32 { - self.0.normal_volume() + self.0.read().unwrap().normal_volume() } /** @@ -521,7 +526,7 @@ impl Tts { pub fn get_volume(&self) -> Result { let Features { volume, .. } = self.supported_features(); if volume { - self.0.get_volume() + self.0.read().unwrap().get_volume() } else { Err(Error::UnsupportedFeature) } @@ -536,10 +541,11 @@ impl Tts { .. } = self.supported_features(); if volume_feature { - if volume < self.0.min_volume() || volume > self.0.max_volume() { + let mut backend = self.0.write().unwrap(); + if volume < backend.min_volume() || volume > backend.max_volume() { Err(Error::OutOfRange) } else { - self.0.set_volume(volume)?; + backend.set_volume(volume)?; Ok(self) } } else { @@ -553,7 +559,7 @@ impl Tts { pub fn is_speaking(&self) -> Result { let Features { is_speaking, .. } = self.supported_features(); if is_speaking { - self.0.is_speaking() + self.0.read().unwrap().is_speaking() } else { Err(Error::UnsupportedFeature) } @@ -572,7 +578,7 @@ impl Tts { } = self.supported_features(); if utterance_callbacks { let mut callbacks = CALLBACKS.lock().unwrap(); - let id = self.0.id().unwrap(); + let id = self.0.read().unwrap().id().unwrap(); let mut callbacks = callbacks.get_mut(&id).unwrap(); callbacks.utterance_begin = callback; Ok(()) @@ -594,7 +600,7 @@ impl Tts { } = self.supported_features(); if utterance_callbacks { let mut callbacks = CALLBACKS.lock().unwrap(); - let id = self.0.id().unwrap(); + let id = self.0.read().unwrap().id().unwrap(); let mut callbacks = callbacks.get_mut(&id).unwrap(); callbacks.utterance_end = callback; Ok(()) @@ -616,7 +622,7 @@ impl Tts { } = self.supported_features(); if utterance_callbacks { let mut callbacks = CALLBACKS.lock().unwrap(); - let id = self.0.id().unwrap(); + let id = self.0.read().unwrap().id().unwrap(); let mut callbacks = callbacks.get_mut(&id).unwrap(); callbacks.utterance_stop = callback; Ok(()) @@ -646,7 +652,7 @@ impl Tts { impl Drop for Tts { fn drop(&mut self) { - if let Some(id) = self.0.id() { + if let Some(id) = self.0.read().unwrap().id() { let mut callbacks = CALLBACKS.lock().unwrap(); callbacks.remove(&id); } From 31309553bb9933b645fd2ee48a7ed578fac0f423 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 7 Mar 2022 17:56:28 -0600 Subject: [PATCH 185/196] Ditto for Linux. --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index bfda4bd..c8abae7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -277,7 +277,7 @@ impl Tts { #[cfg(target_os = "linux")] Backends::SpeechDispatcher => { let tts = backends::SpeechDispatcher::new()?; - Ok(Tts(Box::new(tts))) + Ok(Tts(Arc::new(RwLock::new(Box::new(tts))))) } #[cfg(target_arch = "wasm32")] Backends::Web => { From 594479498039929021dfbe70f3f72cae63c14bb8 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 7 Mar 2022 18:31:25 -0600 Subject: [PATCH 186/196] Add `Arc>` for remaining platforms. --- src/lib.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c8abae7..62649fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -282,13 +282,13 @@ impl Tts { #[cfg(target_arch = "wasm32")] Backends::Web => { let tts = backends::Web::new()?; - Ok(Tts(Box::new(tts))) + Ok(Tts(Arc::new(RwLock::new(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(Arc::new(RwLock::new(Box::new(tts))))) } else { Err(Error::NoneError) } @@ -299,13 +299,17 @@ impl Tts { Ok(Tts(Arc::new(RwLock::new(Box::new(tts))))) } #[cfg(target_os = "macos")] - Backends::AppKit => Ok(Tts(Box::new(backends::AppKit::new()))), + Backends::AppKit => Ok(Tts(Arc::new(RwLock::new( + 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(Arc::new(RwLock::new(Box::new( + backends::AvFoundation::new(), + ))))), #[cfg(target_os = "android")] Backends::Android => { let tts = backends::Android::new()?; - Ok(Tts(Box::new(tts))) + Ok(Tts(Arc::new(RwLock::new(Box::new(tts))))) } }; if let Ok(backend) = backend { From 888e6a3dfac3f30ba4cf4ad3db7fa9a0b5623cd9 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 11:12:46 -0600 Subject: [PATCH 187/196] Correctly clean up callback references based on whether `Arc` remains cloned on drop. --- examples/clone_drop.rs | 89 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 8 ++-- 2 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 examples/clone_drop.rs diff --git a/examples/clone_drop.rs b/examples/clone_drop.rs new file mode 100644 index 0000000..9c466ef --- /dev/null +++ b/examples/clone_drop.rs @@ -0,0 +1,89 @@ +use std::io; + +#[cfg(target_os = "macos")] +use cocoa_foundation::base::id; +#[cfg(target_os = "macos")] +use cocoa_foundation::foundation::NSRunLoop; +#[cfg(target_os = "macos")] +use objc::{msg_send, sel, sel_impl}; + +use tts::*; + +fn main() -> Result<(), Error> { + env_logger::init(); + let tts = Tts::default()?; + let mut tts_clone = tts.clone(); + drop(tts); + 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, + .. + } = tts_clone.supported_features(); + if utterance_callbacks { + tts_clone.on_utterance_begin(Some(Box::new(|utterance| { + println!("Started speaking {:?}", utterance) + })))?; + tts_clone.on_utterance_end(Some(Box::new(|utterance| { + println!("Finished speaking {:?}", utterance) + })))?; + tts_clone.on_utterance_stop(Some(Box::new(|utterance| { + println!("Stopped speaking {:?}", utterance) + })))?; + } + let Features { is_speaking, .. } = tts_clone.supported_features(); + if is_speaking { + println!("Are we speaking? {}", tts_clone.is_speaking()?); + } + tts_clone.speak("Hello, world.", false)?; + let Features { rate, .. } = tts_clone.supported_features(); + if rate { + let original_rate = tts_clone.get_rate()?; + tts_clone.speak(format!("Current rate: {}", original_rate), false)?; + tts_clone.set_rate(tts_clone.max_rate())?; + tts_clone.speak("This is very fast.", false)?; + tts_clone.set_rate(tts_clone.min_rate())?; + tts_clone.speak("This is very slow.", false)?; + tts_clone.set_rate(tts_clone.normal_rate())?; + tts_clone.speak("This is the normal rate.", false)?; + tts_clone.set_rate(original_rate)?; + } + let Features { pitch, .. } = tts_clone.supported_features(); + if pitch { + let original_pitch = tts_clone.get_pitch()?; + tts_clone.set_pitch(tts_clone.max_pitch())?; + tts_clone.speak("This is high-pitch.", false)?; + tts_clone.set_pitch(tts_clone.min_pitch())?; + tts_clone.speak("This is low pitch.", false)?; + tts_clone.set_pitch(tts_clone.normal_pitch())?; + tts_clone.speak("This is normal pitch.", false)?; + tts_clone.set_pitch(original_pitch)?; + } + let Features { volume, .. } = tts_clone.supported_features(); + if volume { + let original_volume = tts_clone.get_volume()?; + tts_clone.set_volume(tts_clone.max_volume())?; + tts_clone.speak("This is loud!", false)?; + tts_clone.set_volume(tts_clone.min_volume())?; + tts_clone.speak("This is quiet.", false)?; + tts_clone.set_volume(tts_clone.normal_volume())?; + tts_clone.speak("This is normal volume.", false)?; + tts_clone.set_volume(original_volume)?; + } + tts_clone.speak("Goodbye.", false)?; + let mut _input = String::new(); + // The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context. + // It shouldn't be needed in an app or game that almost certainly has one already. + #[cfg(target_os = "macos")] + { + let run_loop: id = unsafe { NSRunLoop::currentRunLoop() }; + unsafe { + let _: () = msg_send![run_loop, run]; + } + } + io::stdin().read_line(&mut _input)?; + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 62649fa..4e3f47e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -656,9 +656,11 @@ impl Tts { impl Drop for Tts { fn drop(&mut self) { - if let Some(id) = self.0.read().unwrap().id() { - let mut callbacks = CALLBACKS.lock().unwrap(); - callbacks.remove(&id); + if Arc::strong_count(&self.0) <= 1 { + if let Some(id) = self.0.read().unwrap().id() { + let mut callbacks = CALLBACKS.lock().unwrap(); + callbacks.remove(&id); + } } } } From cc8fd91c8691ca3fde7fa3bd245304ab8ad211c4 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 11:44:59 -0600 Subject: [PATCH 188/196] Set event callbacks on pre-clone value to ensure that they remain alive. --- examples/clone_drop.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/clone_drop.rs b/examples/clone_drop.rs index 9c466ef..5b3dd0f 100644 --- a/examples/clone_drop.rs +++ b/examples/clone_drop.rs @@ -12,8 +12,6 @@ use tts::*; fn main() -> Result<(), Error> { env_logger::init(); let tts = Tts::default()?; - let mut tts_clone = tts.clone(); - drop(tts); if Tts::screen_reader_available() { println!("A screen reader is available on this platform."); } else { @@ -22,18 +20,20 @@ fn main() -> Result<(), Error> { let Features { utterance_callbacks, .. - } = tts_clone.supported_features(); + } = tts.supported_features(); if utterance_callbacks { - tts_clone.on_utterance_begin(Some(Box::new(|utterance| { + tts.on_utterance_begin(Some(Box::new(|utterance| { println!("Started speaking {:?}", utterance) })))?; - tts_clone.on_utterance_end(Some(Box::new(|utterance| { + tts.on_utterance_end(Some(Box::new(|utterance| { println!("Finished speaking {:?}", utterance) })))?; - tts_clone.on_utterance_stop(Some(Box::new(|utterance| { + tts.on_utterance_stop(Some(Box::new(|utterance| { println!("Stopped speaking {:?}", utterance) })))?; } + let mut tts_clone = tts.clone(); + drop(tts); let Features { is_speaking, .. } = tts_clone.supported_features(); if is_speaking { println!("Are we speaking? {}", tts_clone.is_speaking()?); From ef0a78c745f8eebcf6e488d9d3ce4b8bc5e28a5a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 13:46:14 -0600 Subject: [PATCH 189/196] Bump speech-dispatcher, and support building with multiple versions. --- Cargo.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a15c441..d94fe04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,10 @@ edition = "2021" [lib] crate-type = ["lib", "cdylib", "staticlib"] +[features] +speech_dispatcher_0_10 = ["speech-dispatcher/0_10"] +default = ["speech_dispatcher_0_10"] + [dependencies] dyn-clonable = "0.9" lazy_static = "1" @@ -26,7 +30,7 @@ tolk = { version = "0.5", optional = true } windows = { version = "0.33", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] -speech-dispatcher = "0.12" +speech-dispatcher = { version = "0.13", default-features = false } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] cocoa-foundation = "0.1" From f275e506df85a1ee7b07c757dd599e991a51c1ab Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 13:57:05 -0600 Subject: [PATCH 190/196] Disable default-features on Linux since runners don't have speech-dispatcher 0.10 or greater. --- .github/workflows/test.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ff68b02..6cdf4d7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,10 +26,16 @@ jobs: with: command: check args: --all-features --examples + if: ${{ runner.os != 'Linux' }} + - uses: actions-rs/cargo@v1 + with: + command: check + args: --no-default-features --examples + if: ${{ runner.os == 'Linux' }} - uses: actions-rs/cargo@v1 with: command: fmt - args: --all -- --check + args: --all --check - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -55,7 +61,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: fmt - args: --all -- --check + args: --all --check - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} From 5c9c6495056d2c8f07f13cdfb615718038a7640a Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 14:07:18 -0600 Subject: [PATCH 191/196] Also disable default features for Linux when running Clippy. --- .github/workflows/test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6cdf4d7..0471d3c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,6 +40,12 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} args: --all-features + if: ${{ runner.os != 'Linux' }} + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --no-default-features + if: ${{ runner.os == 'Linux' }} check_web: name: Check Web From 539003205e378406344a0b107b0fe8184b2259f5 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 14:08:13 -0600 Subject: [PATCH 192/196] Appease Clippy. --- src/lib.rs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4e3f47e..af041d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -157,7 +157,7 @@ unsafe impl Send for UtteranceId {} unsafe impl Sync for UtteranceId {} -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Features { pub is_speaking: bool, @@ -168,19 +168,6 @@ pub struct Features { pub volume: bool, } -impl Default for Features { - fn default() -> Self { - Self { - stop: false, - rate: false, - pitch: false, - volume: false, - is_speaking: false, - utterance_callbacks: false, - } - } -} - impl fmt::Display for Features { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { writeln!(f, "{:#?}", self) From 3366f93e2b27d70e469692259f11e191a24f56b8 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 14:14:03 -0600 Subject: [PATCH 193/196] Fix release workflow to not build default features. --- .github/workflows/release.yml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 06b847b..89e04ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,14 +27,32 @@ jobs: with: command: check args: --all-features --examples + if: ${{ runner.os != 'Linux' }} + - uses: actions-rs/cargo@v1 + with: + command: check + args: --no-default-features --examples + if: ${{ runner.os == 'Linux' }} - uses: actions-rs/cargo@v1 with: command: fmt - args: --all -- --check + args: --all --check + if: ${{ runner.os == 'Linux' }} + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all --no-default-features --check + if: ${{ runner.os == 'Linux' }} - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} args: --all-features + if: ${{ runner.os != 'Linux' }} + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --no-default-features + if: ${{ runner.os == 'Linux' }} check_web: name: Check Web @@ -88,4 +106,4 @@ jobs: sudo apt-get update sudo apt-get install -y libspeechd-dev cargo login $CARGO_TOKEN - cargo publish + cargo publish --no-default-features From b435c8923975a1f55c89914d06ce5d66dc76955d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 14:14:47 -0600 Subject: [PATCH 194/196] Bump version. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d94fe04..476e954 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.20.4" +version = "0.21.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" From fd9f5ae60a37cb1536a450f1031b725b4487493f Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 10 Mar 2022 14:35:46 -0600 Subject: [PATCH 195/196] Branch not needed. --- .github/workflows/release.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89e04ee..a77851c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,12 +37,6 @@ jobs: with: command: fmt args: --all --check - if: ${{ runner.os == 'Linux' }} - - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all --no-default-features --check - if: ${{ runner.os == 'Linux' }} - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} From acecb1f3628f81f1545c846a44cf82ed26fdecc3 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Fri, 18 Mar 2022 08:47:06 -0500 Subject: [PATCH 196/196] Bump windows dependency and crate version. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 476e954..c3d5adc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.21.0" +version = "0.21.1" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -27,7 +27,7 @@ env_logger = "0.9" [target.'cfg(windows)'.dependencies] tolk = { version = "0.5", optional = true } -windows = { version = "0.33", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } +windows = { version = "0.34", features = ["alloc", "Foundation", "Media_Core", "Media_Playback", "Media_SpeechSynthesis", "Storage_Streams"] } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = { version = "0.13", default-features = false }