mirror of https://github.com/ndarilek/tts-rs.git
Compare commits
7 Commits
90f7dae6a1
...
f275e506df
Author | SHA1 | Date |
---|---|---|
Nolan Darilek | f275e506df | |
Nolan Darilek | ef0a78c745 | |
Nolan Darilek | cc8fd91c86 | |
Nolan Darilek | 888e6a3dfa | |
Nolan Darilek | 5944794980 | |
Nolan Darilek | 31309553bb | |
Nolan Darilek | 00bd5e62ff |
|
@ -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 }}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()?;
|
||||
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.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 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()?);
|
||||
}
|
||||
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(())
|
||||
}
|
90
src/lib.rs
90
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<dyn Backend>);
|
||||
pub struct Tts(Arc<RwLock<Box<dyn Backend>>>);
|
||||
|
||||
unsafe impl Send for Tts {}
|
||||
|
||||
|
@ -277,18 +277,18 @@ 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 => {
|
||||
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)
|
||||
}
|
||||
|
@ -296,20 +296,24 @@ 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()))),
|
||||
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 {
|
||||
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 +366,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 +377,10 @@ impl Tts {
|
|||
text: S,
|
||||
interrupt: bool,
|
||||
) -> Result<Option<UtteranceId>, Error> {
|
||||
self.0.speak(text.into().as_str(), interrupt)
|
||||
self.0
|
||||
.write()
|
||||
.unwrap()
|
||||
.speak(text.into().as_str(), interrupt)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -382,7 +389,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 +400,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 +423,7 @@ impl Tts {
|
|||
pub fn get_rate(&self) -> Result<f32, Error> {
|
||||
let Features { rate, .. } = self.supported_features();
|
||||
if rate {
|
||||
self.0.get_rate()
|
||||
self.0.read().unwrap().get_rate()
|
||||
} else {
|
||||
Err(Error::UnsupportedFeature)
|
||||
}
|
||||
|
@ -430,10 +437,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 +453,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 +476,7 @@ impl Tts {
|
|||
pub fn get_pitch(&self) -> Result<f32, Error> {
|
||||
let Features { pitch, .. } = self.supported_features();
|
||||
if pitch {
|
||||
self.0.get_pitch()
|
||||
self.0.read().unwrap().get_pitch()
|
||||
} else {
|
||||
Err(Error::UnsupportedFeature)
|
||||
}
|
||||
|
@ -483,10 +491,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 +507,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 +530,7 @@ impl Tts {
|
|||
pub fn get_volume(&self) -> Result<f32, Error> {
|
||||
let Features { volume, .. } = self.supported_features();
|
||||
if volume {
|
||||
self.0.get_volume()
|
||||
self.0.read().unwrap().get_volume()
|
||||
} else {
|
||||
Err(Error::UnsupportedFeature)
|
||||
}
|
||||
|
@ -536,10 +545,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 +563,7 @@ impl Tts {
|
|||
pub fn is_speaking(&self) -> Result<bool, Error> {
|
||||
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 +582,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 +604,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 +626,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,9 +656,11 @@ impl Tts {
|
|||
|
||||
impl Drop for Tts {
|
||||
fn drop(&mut self) {
|
||||
if let Some(id) = self.0.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue