mirror of https://github.com/ndarilek/tts-rs.git
Compare commits
15 Commits
bde29226a0
...
a2e8e201cf
Author | SHA1 | Date |
---|---|---|
Lee C. Baker | a2e8e201cf | |
Nolan Darilek | 3c8ae0ae42 | |
Nolan Darilek | 07edc20861 | |
Nolan Darilek | 96a5209a9f | |
Nolan Darilek | 20b18949e2 | |
Nolan Darilek | f29de0aede | |
Nolan Darilek | 9e1476fd36 | |
Nolan Darilek | 3032fe0fb3 | |
Nolan Darilek | edd09c24e7 | |
Nolan Darilek | b7b4e7dc85 | |
Nolan Darilek | f593340051 | |
Enyium | 12d8e1f532 | |
Marijn Suijten | 2a81dc9b70 | |
Esther Alter | a0c6cbaf6a | |
Lee Baker | a55463dac8 |
|
@ -12,7 +12,7 @@ jobs:
|
|||
env:
|
||||
CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libspeechd-dev
|
||||
|
|
|
@ -5,6 +5,17 @@ on:
|
|||
pull_request:
|
||||
|
||||
jobs:
|
||||
check_formatting:
|
||||
name: Check Formatting
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
rustup toolchain install stable
|
||||
cargo fmt --all --check
|
||||
cd examples/web
|
||||
cargo fmt --all --check
|
||||
|
||||
check:
|
||||
name: Check
|
||||
strategy:
|
||||
|
@ -12,41 +23,38 @@ jobs:
|
|||
os: [windows-latest, ubuntu-22.04, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- run: sudo apt-get update; sudo apt-get install -y libspeechd-dev
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
- run: |
|
||||
rustup toolchain install stable
|
||||
cargo fmt --check
|
||||
cargo clippy
|
||||
cargo clippy --all-targets
|
||||
|
||||
check_web:
|
||||
name: Check Web
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
rustup target add wasm32-unknown-unknown
|
||||
rustup toolchain install stable
|
||||
cargo fmt --all --check
|
||||
cargo clippy --target wasm32-unknown-unknown
|
||||
cargo clippy --all-targets --target wasm32-unknown-unknown
|
||||
|
||||
check_android:
|
||||
name: Check Android
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
rustup target add aarch64-linux-android
|
||||
rustup toolchain install stable
|
||||
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
|
||||
cargo install -f cargo-apk
|
||||
cargo apk build
|
||||
cargo clippy --all-targets --target aarch64-linux-android
|
||||
|
||||
check_web_example:
|
||||
name: Check Web Example
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
rustup target add wasm32-unknown-unknown
|
||||
rustup toolchain install stable
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
[package]
|
||||
name = "tts"
|
||||
version = "0.25.6"
|
||||
version = "0.26.0"
|
||||
authors = ["Nolan Darilek <nolan@thewordnerd.info>"]
|
||||
repository = "https://github.com/ndarilek/tts-rs"
|
||||
description = "High-level Text-To-Speech (TTS) interface"
|
||||
documentation = "https://docs.rs/tts"
|
||||
license = "MIT"
|
||||
exclude = ["*.cfg", "*.yml"]
|
||||
edition = "2021"
|
||||
|
@ -26,11 +27,11 @@ serde = { version = "1", optional = true, features = ["derive"] }
|
|||
thiserror = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.10"
|
||||
env_logger = "0.11"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
tolk = { version = "0.5", optional = true }
|
||||
windows = { version = "0.51", features = [
|
||||
windows = { version = "0.52", features = [
|
||||
"Foundation",
|
||||
"Foundation_Collections",
|
||||
"Media_Core",
|
||||
|
@ -64,7 +65,6 @@ web-sys = { version = "0.3", features = [
|
|||
[target.'cfg(target_os="android")'.dependencies]
|
||||
jni = "0.21"
|
||||
ndk-context = "0.1"
|
||||
ndk-glue = "0.7"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
no-default-features = true
|
||||
|
|
|
@ -8,7 +8,7 @@ use core_foundation::base::TCFType;
|
|||
use core_foundation::string::CFString;
|
||||
use lazy_static::lazy_static;
|
||||
use log::{info, trace};
|
||||
use objc::runtime::{Object, Sel};
|
||||
use objc::runtime::{Class, Object, Sel};
|
||||
use objc::{class, declare::ClassDecl, msg_send, sel, sel_impl};
|
||||
use oxilangtag::LanguageTag;
|
||||
|
||||
|
@ -25,109 +25,114 @@ pub(crate) struct AvFoundation {
|
|||
voice: Option<Voice>,
|
||||
}
|
||||
|
||||
fn make_speech_sythesizer_delegate() -> Result<&'static Class, Error> {
|
||||
let mut decl = ClassDecl::new("MyNSSpeechSynthesizerDelegate", class!(NSObject))
|
||||
.ok_or(Error::OperationFailed)?;
|
||||
decl.add_ivar::<u64>("backend_id");
|
||||
|
||||
extern "C" fn speech_synthesizer_did_start_speech_utterance(
|
||||
this: &Object,
|
||||
_: Sel,
|
||||
_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(
|
||||
this: &Object,
|
||||
_: Sel,
|
||||
_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(
|
||||
this: &Object,
|
||||
_: Sel,
|
||||
_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 {
|
||||
decl.add_method(
|
||||
sel!(speechSynthesizer:didStartSpeechUtterance:),
|
||||
speech_synthesizer_did_start_speech_utterance
|
||||
as extern "C" fn(&Object, Sel, *const Object, id) -> (),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(speechSynthesizer:didFinishSpeechUtterance:),
|
||||
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) -> (),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(decl.register())
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref NEXT_BACKEND_ID: Mutex<u64> = Mutex::new(0);
|
||||
static ref DELEGATE_CLASS: &'static Class = make_speech_sythesizer_delegate().unwrap();
|
||||
}
|
||||
|
||||
impl AvFoundation {
|
||||
pub(crate) fn new() -> Result<Self, Error> {
|
||||
info!("Initializing AVFoundation backend");
|
||||
let mut decl = ClassDecl::new("MyNSSpeechSynthesizerDelegate", class!(NSObject))
|
||||
.ok_or(Error::OperationFailed)?;
|
||||
decl.add_ivar::<u64>("backend_id");
|
||||
|
||||
extern "C" fn speech_synthesizer_did_start_speech_utterance(
|
||||
this: &Object,
|
||||
_: Sel,
|
||||
_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(
|
||||
this: &Object,
|
||||
_: Sel,
|
||||
_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(
|
||||
this: &Object,
|
||||
_: Sel,
|
||||
_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 {
|
||||
decl.add_method(
|
||||
sel!(speechSynthesizer:didStartSpeechUtterance:),
|
||||
speech_synthesizer_did_start_speech_utterance
|
||||
as extern "C" fn(&Object, Sel, *const Object, id) -> (),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(speechSynthesizer:didFinishSpeechUtterance:),
|
||||
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();
|
||||
let delegate_obj: *mut Object = unsafe { msg_send![delegate_class, new] };
|
||||
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");
|
||||
|
|
|
@ -188,6 +188,7 @@ impl Backend for SpeechDispatcher {
|
|||
.0
|
||||
.list_synthesis_voices()?
|
||||
.iter()
|
||||
.filter(|v| LanguageTag::parse(v.language.clone()).is_ok())
|
||||
.map(|v| Voice {
|
||||
id: v.name.clone(),
|
||||
name: v.name.clone(),
|
||||
|
|
25
src/lib.rs
25
src/lib.rs
|
@ -14,9 +14,10 @@ use std::collections::HashMap;
|
|||
#[cfg(target_os = "macos")]
|
||||
use std::ffi::CStr;
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
#[cfg(windows)]
|
||||
use std::string::FromUtf16Error;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Mutex;
|
||||
use std::{boxed::Box, sync::RwLock};
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
|
@ -258,7 +259,7 @@ lazy_static! {
|
|||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Tts(Arc<RwLock<Box<dyn Backend>>>);
|
||||
pub struct Tts(Rc<RwLock<Box<dyn Backend>>>);
|
||||
|
||||
unsafe impl Send for Tts {}
|
||||
|
||||
|
@ -271,18 +272,18 @@ impl Tts {
|
|||
#[cfg(target_os = "linux")]
|
||||
Backends::SpeechDispatcher => {
|
||||
let tts = backends::SpeechDispatcher::new()?;
|
||||
Ok(Tts(Arc::new(RwLock::new(Box::new(tts)))))
|
||||
Ok(Tts(Rc::new(RwLock::new(Box::new(tts)))))
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Backends::Web => {
|
||||
let tts = backends::Web::new()?;
|
||||
Ok(Tts(Arc::new(RwLock::new(Box::new(tts)))))
|
||||
Ok(Tts(Rc::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(Arc::new(RwLock::new(Box::new(tts)))))
|
||||
Ok(Tts(Rc::new(RwLock::new(Box::new(tts)))))
|
||||
} else {
|
||||
Err(Error::NoneError)
|
||||
}
|
||||
|
@ -290,20 +291,20 @@ impl Tts {
|
|||
#[cfg(windows)]
|
||||
Backends::WinRt => {
|
||||
let tts = backends::WinRt::new()?;
|
||||
Ok(Tts(Arc::new(RwLock::new(Box::new(tts)))))
|
||||
Ok(Tts(Rc::new(RwLock::new(Box::new(tts)))))
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
Backends::AppKit => Ok(Tts(Arc::new(RwLock::new(Box::new(
|
||||
backends::AppKit::new()?
|
||||
))))),
|
||||
Backends::AppKit => Ok(Tts(Rc::new(RwLock::new(
|
||||
Box::new(backends::AppKit::new()?),
|
||||
)))),
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
Backends::AvFoundation => Ok(Tts(Arc::new(RwLock::new(Box::new(
|
||||
Backends::AvFoundation => Ok(Tts(Rc::new(RwLock::new(Box::new(
|
||||
backends::AvFoundation::new()?,
|
||||
))))),
|
||||
#[cfg(target_os = "android")]
|
||||
Backends::Android => {
|
||||
let tts = backends::Android::new()?;
|
||||
Ok(Tts(Arc::new(RwLock::new(Box::new(tts)))))
|
||||
Ok(Tts(Rc::new(RwLock::new(Box::new(tts)))))
|
||||
}
|
||||
};
|
||||
if let Ok(backend) = backend {
|
||||
|
@ -640,7 +641,7 @@ impl Tts {
|
|||
|
||||
impl Drop for Tts {
|
||||
fn drop(&mut self) {
|
||||
if Arc::strong_count(&self.0) <= 1 {
|
||||
if Rc::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