Compare commits

...

20 Commits

Author SHA1 Message Date
Bear_03 eede49b0a6
Merge 4f4ab53252 into 3c8ae0ae42 2024-03-18 00:29:55 +00: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
Bear-03 4f4ab53252
Add Tts::synthesize method 2022-07-23 14:04:15 +02:00
Bear-03 91a0f03f1a
Remove InMemoryRandomAccessStream in WinRt::speak 2022-07-23 14:03:45 +02:00
Bear-03 87cf05f78e
Fix errors on stable toolchain 2022-07-22 18:03:01 +02:00
Bear_03 9cd66c0358
Merge branch 'master' into synthesize 2022-07-22 17:42:04 +02:00
Bear-03 b85ffc80d4
Add synthesis support to WinRT 2022-07-22 17:31:14 +02:00
Bear-03 bf522b42a7
Prepare Backend and Features for synthesis 2022-07-22 17:29:48 +02:00
11 changed files with 113 additions and 40 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -250,6 +250,7 @@ impl Backend for Android {
utterance_callbacks: true,
voice: false,
get_voice: false,
synthesize: false,
}
}
@ -284,6 +285,10 @@ impl Backend for Android {
}
}
fn synthesize(&mut self, text: &str) -> Result<Vec<u8>, Error> {
unimplemented!();
}
fn stop(&mut self) -> Result<(), Error> {
let vm = Self::vm()?;
let mut env = vm.get_env()?;

View File

@ -125,6 +125,10 @@ impl Backend for AppKit {
Ok(None)
}
fn synthesize(&mut self, text: &str) -> Result<Vec<u8>, Error> {
unimplemented!();
}
fn stop(&mut self) -> Result<(), Error> {
trace!("stop()");
unsafe {

View File

@ -170,6 +170,7 @@ impl Backend for AvFoundation {
voice: true,
get_voice: false,
utterance_callbacks: true,
synthesize: false,
}
}
@ -206,6 +207,10 @@ impl Backend for AvFoundation {
Ok(Some(UtteranceId::AvFoundation(utterance)))
}
fn synthesize(&mut self, text: &str) -> Result<Vec<u8>, Error> {
unimplemented!();
}
fn stop(&mut self) -> Result<(), Error> {
trace!("stop()");
unsafe {

View File

@ -85,6 +85,7 @@ impl Backend for SpeechDispatcher {
voice: true,
get_voice: false,
utterance_callbacks: true,
synthesize: false,
}
}
@ -108,6 +109,10 @@ impl Backend for SpeechDispatcher {
}
}
fn synthesize(&mut self, text: &str) -> Result<Vec<u8>, Error> {
unimplemented!();
}
fn stop(&mut self) -> Result<(), Error> {
trace!("stop()");
self.0.cancel()?;
@ -188,6 +193,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(),

View File

@ -39,6 +39,10 @@ impl Backend for Tolk {
Ok(None)
}
fn synthesize(&mut self, text: &str) -> Result<Vec<u8>, Error> {
unimplemented!();
}
fn stop(&mut self) -> Result<(), Error> {
trace!("stop()");
self.0.silence();

View File

@ -59,6 +59,7 @@ impl Backend for Web {
voice: true,
get_voice: true,
utterance_callbacks: true,
synthesize: false,
}
}
@ -121,6 +122,10 @@ impl Backend for Web {
}
}
fn synthesize(&mut self, text: &str) -> Result<Vec<u8>, Error> {
unimplemented!();
}
fn stop(&mut self) -> Result<(), Error> {
trace!("stop()");
if let Some(window) = web_sys::window() {

View File

@ -12,8 +12,11 @@ use windows::{
Media::{
Core::MediaSource,
Playback::{MediaPlayer, MediaPlayerAudioCategory},
SpeechSynthesis::{SpeechSynthesizer, VoiceGender, VoiceInformation},
SpeechSynthesis::{
SpeechSynthesisStream, SpeechSynthesizer, VoiceGender, VoiceInformation,
},
},
Storage::Streams::DataReader,
};
use crate::{Backend, BackendId, Error, Features, Gender, UtteranceId, Voice, CALLBACKS};
@ -138,6 +141,20 @@ impl WinRt {
voice: SpeechSynthesizer::DefaultVoice()?,
})
}
fn create_synthesis_stream(&mut self, text: &str) -> Result<SpeechSynthesisStream, Error> {
self.synth.Options()?.SetSpeakingRate(self.rate.into())?;
self.synth.Options()?.SetAudioPitch(self.pitch.into())?;
self.synth.Options()?.SetAudioVolume(self.volume.into())?;
self.synth.SetVoice(&self.voice)?;
let stream = self
.synth
.SynthesizeTextToStreamAsync(&text.into())?
.get()?;
Ok(stream)
}
}
impl Backend for WinRt {
@ -155,6 +172,7 @@ impl Backend for WinRt {
voice: true,
get_voice: true,
utterance_callbacks: true,
synthesize: true,
}
}
@ -189,18 +207,12 @@ impl Backend for WinRt {
}
}
if no_utterances {
self.synth.Options()?.SetSpeakingRate(self.rate.into())?;
self.synth.Options()?.SetAudioPitch(self.pitch.into())?;
self.synth.Options()?.SetAudioVolume(self.volume.into())?;
self.synth.SetVoice(&self.voice)?;
let stream = self
.synth
.SynthesizeTextToStreamAsync(&text.into())?
.get()?;
let content_type = stream.ContentType()?;
let source = MediaSource::CreateFromStream(&stream, &content_type)?;
self.player.SetSource(&source)?;
let stream = self.create_synthesis_stream(text)?;
let media_source = MediaSource::CreateFromStream(&stream, &stream.ContentType()?)?;
self.player.SetSource(&media_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() {
@ -210,6 +222,18 @@ impl Backend for WinRt {
Ok(Some(utterance_id))
}
fn synthesize(&mut self, text: &str) -> Result<Vec<u8>, Error> {
let stream = self.create_synthesis_stream(text)?;
let size = stream.Size()?;
let data_reader = DataReader::CreateDataReader(&stream.GetInputStreamAt(0)?)?;
let mut bytes = vec![0; size as usize];
data_reader.LoadAsync(size as u32)?;
data_reader.ReadBytes(&mut bytes)?;
Ok(bytes)
}
fn stop(&mut self) -> std::result::Result<(), Error> {
trace!("stop()");
if !self.is_speaking()? {

View File

@ -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"))]
@ -162,6 +163,7 @@ unsafe impl Sync for UtteranceId {}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Features {
pub is_speaking: bool,
pub synthesize: bool,
pub pitch: bool,
pub rate: bool,
pub stop: bool,
@ -217,6 +219,7 @@ pub trait Backend: Clone {
fn id(&self) -> Option<BackendId>;
fn supported_features(&self) -> Features;
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error>;
fn synthesize(&mut self, text: &str) -> Result<Vec<u8>, Error>;
fn stop(&mut self) -> Result<(), Error>;
fn min_rate(&self) -> f32;
fn max_rate(&self) -> f32;
@ -258,7 +261,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 +274,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 +293,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 {
@ -374,6 +377,15 @@ impl Tts {
.speak(text.into().as_str(), interrupt)
}
pub fn synthesize<S: Into<String>>(&mut self, text: S) -> Result<Vec<u8>, Error> {
let Features { synthesize, .. } = self.supported_features();
if synthesize {
self.0.write().unwrap().synthesize(text.into().as_str())
} else {
Err(Error::UnsupportedFeature)
}
}
/// Stops current speech.
pub fn stop(&mut self) -> Result<&Self, Error> {
let Features { stop, .. } = self.supported_features();
@ -640,7 +652,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);