Compare commits

...

15 Commits

Author SHA1 Message Date
Lee C. Baker a2e8e201cf
Merge a55463dac8 into 3c8ae0ae42 2024-02-19 15:26:58 +08:00
Nolan Darilek 3c8ae0ae42 Bump version. 2024-02-09 11:54:31 -06:00
Nolan Darilek 07edc20861 Switch `Arc` to `Rc` to appease Clippy. 2024-02-09 11:34:01 -06:00
Nolan Darilek 96a5209a9f Attempt to simplify CI. 2024-02-09 11:29:00 -06:00
Nolan Darilek 20b18949e2 Just build for target since we don't need an APK. 2024-02-09 11:07:11 -06:00
Nolan Darilek f29de0aede Switch to actions/checkout@v4. 2024-02-09 11:03:41 -06:00
Nolan Darilek 9e1476fd36
Merge pull request #49 from subalterngames:speech_dispatcher_voices_panic
Fixed a panic in SpeechDispatcher.voices()
2024-02-09 10:59:04 -06:00
Nolan Darilek 3032fe0fb3
Merge pull request #51 from Enyium:patch-1
Added docs.rs link to `Cargo.toml`
2024-02-09 10:54:59 -06:00
Nolan Darilek edd09c24e7 Switch from cargo-apk to xbuild. 2024-02-09 10:49:08 -06:00
Nolan Darilek b7b4e7dc85 Bump dependencies. 2024-02-09 10:37:55 -06:00
Nolan Darilek f593340051
Merge pull request #50 from MarijnS95:drop-ndk-glue
Drop `ndk-glue` dependency from the main crate
2024-02-09 10:31:54 -06:00
Enyium 12d8e1f532
Added docs.rs link to `Cargo.toml` 2023-12-05 12:28:47 +01:00
Marijn Suijten 2a81dc9b70 Drop `ndk-glue` dependency from the main crate
Commit d42d201 ("Update Android dependencies and example.") correctly
replaces `ndk-glue` with `ndk-context` as a more generic crate to hold on
to a global `JavaVM` and Android `jobject` `Context`, but didn't drop the
unused `ndk-glue` crate from the list of Android dependencies.  This
crate is only used in the example crate, and [shouldn't clobber
downstream crates].

Besides, `ndk-glue` has been deprecated for some time and should be
replaced by `android-activity` in the example in a followup PR.

[shouldn't clobber downstream crates]: https://github.com/emilk/egui/pull/3606/files#r1401313794
2023-11-22 00:12:48 +01:00
Esther Alter a0c6cbaf6a Fixed a panic in SpeechDispatcher.voices() 2023-10-25 11:59:13 -04:00
Lee Baker a55463dac8 Don't define the AVFoundation delegate more than once.
This prevents a crash when multiple instances of the TTS engine are instantiated.
2022-08-06 06:08:28 -05:00
6 changed files with 139 additions and 124 deletions

View File

@ -12,7 +12,7 @@ jobs:
env: env:
CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- run: | - run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libspeechd-dev sudo apt-get install -y libspeechd-dev

View File

@ -5,6 +5,17 @@ on:
pull_request: pull_request:
jobs: 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: check:
name: Check name: Check
strategy: strategy:
@ -12,41 +23,38 @@ jobs:
os: [windows-latest, ubuntu-22.04, macos-latest] os: [windows-latest, ubuntu-22.04, macos-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- run: sudo apt-get update; sudo apt-get install -y libspeechd-dev - run: sudo apt-get update; sudo apt-get install -y libspeechd-dev
if: ${{ runner.os == 'Linux' }} if: ${{ runner.os == 'Linux' }}
- run: | - run: |
rustup toolchain install stable rustup toolchain install stable
cargo fmt --check cargo clippy --all-targets
cargo clippy
check_web: check_web:
name: Check Web name: Check Web
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- run: | - run: |
rustup target add wasm32-unknown-unknown rustup target add wasm32-unknown-unknown
rustup toolchain install stable rustup toolchain install stable
cargo fmt --all --check cargo clippy --all-targets --target wasm32-unknown-unknown
cargo clippy --target wasm32-unknown-unknown
check_android: check_android:
name: Check Android name: Check Android
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- run: | - run: |
rustup target add aarch64-linux-android
rustup toolchain install stable rustup toolchain install stable
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android cargo clippy --all-targets --target aarch64-linux-android
cargo install -f cargo-apk
cargo apk build
check_web_example: check_web_example:
name: Check Web Example name: Check Web Example
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- run: | - run: |
rustup target add wasm32-unknown-unknown rustup target add wasm32-unknown-unknown
rustup toolchain install stable rustup toolchain install stable

View File

@ -1,9 +1,10 @@
[package] [package]
name = "tts" name = "tts"
version = "0.25.6" version = "0.26.0"
authors = ["Nolan Darilek <nolan@thewordnerd.info>"] authors = ["Nolan Darilek <nolan@thewordnerd.info>"]
repository = "https://github.com/ndarilek/tts-rs" repository = "https://github.com/ndarilek/tts-rs"
description = "High-level Text-To-Speech (TTS) interface" description = "High-level Text-To-Speech (TTS) interface"
documentation = "https://docs.rs/tts"
license = "MIT" license = "MIT"
exclude = ["*.cfg", "*.yml"] exclude = ["*.cfg", "*.yml"]
edition = "2021" edition = "2021"
@ -26,11 +27,11 @@ serde = { version = "1", optional = true, features = ["derive"] }
thiserror = "1" thiserror = "1"
[dev-dependencies] [dev-dependencies]
env_logger = "0.10" env_logger = "0.11"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
tolk = { version = "0.5", optional = true } tolk = { version = "0.5", optional = true }
windows = { version = "0.51", features = [ windows = { version = "0.52", features = [
"Foundation", "Foundation",
"Foundation_Collections", "Foundation_Collections",
"Media_Core", "Media_Core",
@ -64,7 +65,6 @@ web-sys = { version = "0.3", features = [
[target.'cfg(target_os="android")'.dependencies] [target.'cfg(target_os="android")'.dependencies]
jni = "0.21" jni = "0.21"
ndk-context = "0.1" ndk-context = "0.1"
ndk-glue = "0.7"
[package.metadata.docs.rs] [package.metadata.docs.rs]
no-default-features = true no-default-features = true

View File

@ -8,7 +8,7 @@ use core_foundation::base::TCFType;
use core_foundation::string::CFString; use core_foundation::string::CFString;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use log::{info, trace}; 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 objc::{class, declare::ClassDecl, msg_send, sel, sel_impl};
use oxilangtag::LanguageTag; use oxilangtag::LanguageTag;
@ -25,109 +25,114 @@ pub(crate) struct AvFoundation {
voice: Option<Voice>, 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! { lazy_static! {
static ref NEXT_BACKEND_ID: Mutex<u64> = Mutex::new(0); static ref NEXT_BACKEND_ID: Mutex<u64> = Mutex::new(0);
static ref DELEGATE_CLASS: &'static Class = make_speech_sythesizer_delegate().unwrap();
} }
impl AvFoundation { impl AvFoundation {
pub(crate) fn new() -> Result<Self, Error> { pub(crate) fn new() -> Result<Self, Error> {
info!("Initializing AVFoundation backend"); 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( let delegate_obj: *mut Object = unsafe { msg_send![*DELEGATE_CLASS, new] };
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 mut backend_id = NEXT_BACKEND_ID.lock().unwrap(); let mut backend_id = NEXT_BACKEND_ID.lock().unwrap();
let rv = unsafe { let rv = unsafe {
trace!("Creating synth"); trace!("Creating synth");

View File

@ -188,6 +188,7 @@ impl Backend for SpeechDispatcher {
.0 .0
.list_synthesis_voices()? .list_synthesis_voices()?
.iter() .iter()
.filter(|v| LanguageTag::parse(v.language.clone()).is_ok())
.map(|v| Voice { .map(|v| Voice {
id: v.name.clone(), id: v.name.clone(),
name: v.name.clone(), name: v.name.clone(),

View File

@ -14,9 +14,10 @@ use std::collections::HashMap;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use std::ffi::CStr; use std::ffi::CStr;
use std::fmt; use std::fmt;
use std::rc::Rc;
#[cfg(windows)] #[cfg(windows)]
use std::string::FromUtf16Error; use std::string::FromUtf16Error;
use std::sync::{Arc, Mutex}; use std::sync::Mutex;
use std::{boxed::Box, sync::RwLock}; use std::{boxed::Box, sync::RwLock};
#[cfg(any(target_os = "macos", target_os = "ios"))] #[cfg(any(target_os = "macos", target_os = "ios"))]
@ -258,7 +259,7 @@ lazy_static! {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct Tts(Arc<RwLock<Box<dyn Backend>>>); pub struct Tts(Rc<RwLock<Box<dyn Backend>>>);
unsafe impl Send for Tts {} unsafe impl Send for Tts {}
@ -271,18 +272,18 @@ impl Tts {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
Backends::SpeechDispatcher => { Backends::SpeechDispatcher => {
let tts = backends::SpeechDispatcher::new()?; 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")] #[cfg(target_arch = "wasm32")]
Backends::Web => { Backends::Web => {
let tts = backends::Web::new()?; 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"))] #[cfg(all(windows, feature = "tolk"))]
Backends::Tolk => { Backends::Tolk => {
let tts = backends::Tolk::new(); let tts = backends::Tolk::new();
if let Some(tts) = tts { if let Some(tts) = tts {
Ok(Tts(Arc::new(RwLock::new(Box::new(tts))))) Ok(Tts(Rc::new(RwLock::new(Box::new(tts)))))
} else { } else {
Err(Error::NoneError) Err(Error::NoneError)
} }
@ -290,20 +291,20 @@ impl Tts {
#[cfg(windows)] #[cfg(windows)]
Backends::WinRt => { Backends::WinRt => {
let tts = backends::WinRt::new()?; 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")] #[cfg(target_os = "macos")]
Backends::AppKit => Ok(Tts(Arc::new(RwLock::new(Box::new( Backends::AppKit => Ok(Tts(Rc::new(RwLock::new(
backends::AppKit::new()? Box::new(backends::AppKit::new()?),
))))), )))),
#[cfg(any(target_os = "macos", target_os = "ios"))] #[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()?, backends::AvFoundation::new()?,
))))), ))))),
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
Backends::Android => { Backends::Android => {
let tts = backends::Android::new()?; 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 { if let Ok(backend) = backend {
@ -640,7 +641,7 @@ impl Tts {
impl Drop for Tts { impl Drop for Tts {
fn drop(&mut self) { 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() { if let Some(id) = self.0.read().unwrap().id() {
let mut callbacks = CALLBACKS.lock().unwrap(); let mut callbacks = CALLBACKS.lock().unwrap();
callbacks.remove(&id); callbacks.remove(&id);