Compare commits

...

21 Commits

Author SHA1 Message Date
Nolan Darilek 1e55c43153 WIP: Use correct type in backend implementation. 2022-03-30 15:13:28 -05:00
Nolan Darilek e56a0da2e5 WIP: Reorganize, and try to get working with Speech Dispatcher. 2022-03-30 12:07:59 -05:00
Nolan Darilek 55f841d887 Merge extra module into main module. 2022-03-30 10:54:30 -05:00
Nolan Darilek c222c087b2 cargo fmt 2022-03-30 10:18:22 -05:00
Nolan Darilek 6057d9c968
Merge pull request #2 from francois-caddet/feature/voices
Add voices feature
2022-03-30 10:17:32 -05:00
francois-caddet 88f4598ec6 Merge branch 'master' into feature/voices 2022-03-20 13:02:37 +01:00
François Caddet d2c42d97f5 the voices::Backend trait is almost stable 2020-09-28 11:18:54 +02:00
François Caddet 3294a82485 some fixes
now build on macOS
2020-09-27 20:35:40 +02:00
François Caddet e19eb56169 first implementation of a voice trait for macOS
WARN: not tested
2020-09-27 20:04:12 +02:00
François Caddet f7297e18fd add condition for macOS 11 and greater for default backend 2020-09-26 23:39:30 +02:00
François Caddet f78aed211f fix conflicts 2020-09-26 23:36:15 +02:00
Francois Caddet 008662c940 temporary fix to a build issue with the crate speech-dispatcher 2020-09-26 23:16:10 +02:00
Francois Caddet 8c8dc0ae9f add voices value returned by the backends 2020-09-26 23:03:56 +02:00
Francois Caddet 47cbb80595 Merge branch 'develop' into feature/voices 2020-09-26 18:20:10 +02:00
François Caddet 97f1de5724 Merge branch 'develop' into feature/voices 2020-09-05 12:27:19 +02:00
François Caddet 335ac710a6 add unimplemented functions forvoices feature on every backends 2020-09-05 12:07:51 +02:00
François Caddet b238c8c938 fix return type of AVSpeechSynthesisVoice:new 2020-09-05 11:30:11 +02:00
François Caddet 1b8809aaeb remove the example changing voice.
the default() voice working properly for av_foundation
2020-09-05 10:55:23 +02:00
François Caddet 0fb6c62d83 fix some parameters types and implement set_voice
We have an ilegal hardware instruction in
backend::av_foundation::voices::AVSpeechSynthesisVoice::new(identifier)
when sending voiceWithIdentifier. Is it because the runLoop is not
runing when it's called?
2020-09-04 15:48:56 +02:00
François Caddet 6ed94686f3 implement set_voice for AVFoundation backend
- TODO: test the implementation
- fixed: set_voice mutability of self parameter
2020-09-03 18:40:32 +02:00
François Caddet 5b0d1b6621 Add voices feature
Implemented for AVFoundation backend but set_voice has no effect for now
Warning: does not build on Linux or windows for now
2020-09-03 16:50:11 +02:00
11 changed files with 292 additions and 14 deletions

View File

@ -13,13 +13,13 @@ 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"
log = "0.4"
thiserror = "1"
unic-langid = "0.9.0"
serde = { version = "1.0", optional = true, features = ["derive"] }
[dev-dependencies]
@ -34,6 +34,7 @@ speech-dispatcher = { version = "0.13", default-features = false }
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
cocoa-foundation = "0.1"
core-foundation = "0.9"
libc = "0.2"
objc = { version = "0.2", features = ["exception"] }
@ -43,4 +44,4 @@ web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "Spee
[target.'cfg(target_os="android")'.dependencies]
jni = "0.19"
ndk-glue = "0.6"
ndk-glue = "0.6"

View File

@ -71,6 +71,20 @@ fn main() -> Result<(), Error> {
tts.speak("This is normal volume.", false)?;
tts.set_volume(original_volume)?;
}
let Features { voices, .. } = tts.supported_features();
if voices {
let original_voice = tts.voice()?;
let voices_list = tts.list_voices();
println!("Available voices:\n===");
for v in voices_list.iter() {
println!("{}", v);
tts.set_voice(v)?;
println!("voice set");
println!("{}", tts.voice()?);
tts.speak(v, false)?;
}
tts.set_voice(original_voice)?;
}
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.

22
speech-dispatcher.patch Normal file
View File

@ -0,0 +1,22 @@
diff --git src/lib.rs src/lib.rs
index 26ba271..180513e 100644
--- src/lib.rs
+++ src/lib.rs
@@ -127,7 +127,7 @@ unsafe extern "C" fn cb(msg_id: u64, client_id: u64, state: u32) {
}
}
-unsafe extern "C" fn cb_im(msg_id: u64, client_id: u64, state: u32, index_mark: *mut i8) {
+unsafe extern "C" fn cb_im(msg_id: u64, client_id: u64, state: u32, index_mark: *mut u8) {
let index_mark = CStr::from_ptr(index_mark);
let index_mark = index_mark.to_string_lossy().to_string();
let state = match state {
@@ -325,7 +325,7 @@ impl Connection {
i32_to_bool(v)
}
- pub fn wchar(&self, priority: Priority, wchar: i32) -> bool {
+ pub fn wchar(&self, priority: Priority, wchar: u32) -> bool {
let v = unsafe { spd_wchar(self.0, priority as u32, wchar) };
i32_to_bool(v)
}

View File

@ -200,6 +200,18 @@ impl Backend for AppKit {
let is_speaking: i8 = unsafe { msg_send![self.0, isSpeaking] };
Ok(is_speaking != NO as i8)
}
fn voice(&self) -> Result<String, Error> {
unimplemented!()
}
fn list_voices(&self) -> Vec<String> {
unimplemented!()
}
fn set_voice(&mut self, voice: &str) -> Result<(), Error> {
unimplemented!()
}
}
impl Drop for AppKit {

View File

@ -9,8 +9,12 @@ use log::{info, trace};
use objc::runtime::{Object, Sel};
use objc::{class, declare::ClassDecl, msg_send, sel, sel_impl};
use crate::voices::Backend as VoiceBackend;
use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS};
mod voices;
use voices::*;
#[derive(Clone, Debug)]
pub(crate) struct AvFoundation {
id: BackendId,
@ -19,6 +23,7 @@ pub(crate) struct AvFoundation {
rate: f32,
volume: f32,
pitch: f32,
voice: AVSpeechSynthesisVoice,
}
lazy_static! {
@ -142,6 +147,7 @@ impl AvFoundation {
rate: 0.5,
volume: 1.,
pitch: 1.,
voice: AVSpeechSynthesisVoice::new(),
}
};
*backend_id += 1;
@ -161,6 +167,7 @@ impl Backend for AvFoundation {
pitch: true,
volume: true,
is_speaking: true,
voices: true,
utterance_callbacks: true,
}
}
@ -185,6 +192,7 @@ impl Backend for AvFoundation {
let _: () = msg_send![utterance, setVolume: self.volume];
trace!("Setting pitch to {}", self.pitch);
let _: () = msg_send![utterance, setPitchMultiplier: self.pitch];
let _: () = msg_send![utterance, setVoice: self.voice];
trace!("Enqueuing");
let _: () = msg_send![self.synth, speakUtterance: utterance];
trace!("Done queuing");
@ -271,6 +279,22 @@ impl Backend for AvFoundation {
let is_speaking: i8 = unsafe { msg_send![self.synth, isSpeaking] };
Ok(is_speaking != NO as i8)
}
fn voice(&self) -> Result<String, Error> {
Ok(self.voice.id())
}
fn list_voices(&self) -> Vec<String> {
AVSpeechSynthesisVoice::list()
.iter()
.map(|v| v.id())
.collect()
}
fn set_voice(&mut self, voice: &str) -> Result<(), Error> {
self.voice = AVSpeechSynthesisVoice::new();
Ok(())
}
}
impl Drop for AvFoundation {

View File

@ -0,0 +1,69 @@
use cocoa_foundation::base::{id, nil};
use cocoa_foundation::foundation::NSString;
use core_foundation::array::CFArray;
use core_foundation::string::CFString;
use objc::runtime::*;
use objc::*;
use crate::backends::AvFoundation;
use crate::voices;
use crate::voices::Gender;
#[derive(Copy, Clone, Debug)]
pub(crate) struct AVSpeechSynthesisVoice(*const Object);
impl AVSpeechSynthesisVoice {
pub fn new() -> Self {
let voice: *const Object;
unsafe {
voice = msg_send![class!(AVSpeechSynthesisVoice), new];
};
AVSpeechSynthesisVoice { 0: voice }
}
}
impl voices::Backend for AVSpeechSynthesisVoice {
type Backend = AvFoundation;
fn from_id(id: String) -> Self {
unimplemented!()
}
fn from_language(lang: voices::LanguageIdentifier) -> Self {
unimplemented!()
}
fn list() -> Vec<Self> {
let voices: CFArray = unsafe { msg_send![class!(AVSpeechSynthesisVoice), speechVoices] };
voices
.iter()
.map(|v| AVSpeechSynthesisVoice {
0: *v as *const Object,
})
.collect()
}
fn name(self) -> String {
let name: CFString = unsafe { msg_send![self.0, name] };
name.to_string()
}
fn gender(self) -> Gender {
let gender: i64 = unsafe { msg_send![self.0, gender] };
match gender {
1 => Gender::Male,
2 => Gender::Female,
_ => Gender::Other,
}
}
fn id(self) -> String {
let identifier: CFString = unsafe { msg_send![self.0, identifier] };
identifier.to_string()
}
fn language(self) -> voices::LanguageIdentifier {
let lang: CFString = unsafe { msg_send![self.0, language] };
lang.to_string().parse().unwrap()
}
}

View File

@ -1,11 +1,15 @@
use std::str::FromStr;
#[cfg(target_os = "linux")]
use std::{collections::HashMap, sync::Mutex};
use lazy_static::*;
use log::{info, trace};
use speech_dispatcher::*;
use speech_dispatcher::{Voice as SpdVoice, *};
use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS};
use crate::{
Backend, BackendId, Error, Features, Gender, UtteranceId, Voice, VoiceImpl, CALLBACKS,
};
#[derive(Clone, Debug)]
pub(crate) struct SpeechDispatcher(Connection);
@ -17,6 +21,24 @@ lazy_static! {
};
}
impl VoiceImpl for SpdVoice {
fn id(self) -> String {
self.name
}
fn name(self) -> String {
self.name
}
fn gender(self) -> Gender {
Gender::Other
}
fn language(self) -> Result<LanguageIdentifier, LanguageIdentifierError> {
LanguageIdentifier::from_str(&self.language)
}
}
impl SpeechDispatcher {
pub(crate) fn new() -> std::result::Result<Self, Error> {
info!("Initializing SpeechDispatcher backend");
@ -69,7 +91,7 @@ impl SpeechDispatcher {
}
}
impl Backend for SpeechDispatcher {
impl Backend<SpdVoice> for SpeechDispatcher {
fn id(&self) -> Option<BackendId> {
Some(BackendId::SpeechDispatcher(self.0.client_id()))
}
@ -81,6 +103,7 @@ impl Backend for SpeechDispatcher {
pitch: true,
volume: true,
is_speaking: true,
voices: false,
utterance_callbacks: true,
}
}
@ -179,6 +202,25 @@ impl Backend for SpeechDispatcher {
let is_speaking = speaking.get(&self.0.client_id()).unwrap();
Ok(*is_speaking)
}
fn voices(&self) -> Result<Vec<Voice<SpdVoice>>, Error> {
let rv = self
.0
.list_synthesis_voices()?
.iter()
.cloned()
.map(|v| Voice(Box::new(v)))
.collect::<Vec<Voice<SpdVoice>>>();
Ok(rv)
}
fn voice(&self) -> Result<String, Error> {
unimplemented!()
}
fn set_voice(&mut self, voice: &str) -> Result<(), Error> {
unimplemented!()
}
}
impl Drop for SpeechDispatcher {

View File

@ -108,4 +108,16 @@ impl Backend for Tolk {
fn is_speaking(&self) -> Result<bool, Error> {
unimplemented!()
}
fn voice(&self) -> Result<String, Error> {
unimplemented!()
}
fn list_voices(&self) -> Vec<String> {
unimplemented!()
}
fn set_voice(&mut self, voice: &str) -> Result<(), Error> {
unimplemented!()
}
}

View File

@ -53,6 +53,7 @@ impl Backend for Web {
pitch: true,
volume: true,
is_speaking: true,
voices: true,
utterance_callbacks: true,
}
}
@ -196,6 +197,18 @@ impl Backend for Web {
Err(Error::NoneError)
}
}
fn voice(&self) -> Result<String, Error> {
unimplemented!()
}
fn list_voices(&self) -> Vec<String> {
unimplemented!()
}
fn set_voice(&mut self, voice: &str) -> Result<(), Error> {
unimplemented!()
}
}
impl Drop for Web {

View File

@ -145,6 +145,7 @@ impl Backend for WinRt {
pitch: true,
volume: true,
is_speaking: true,
voices: true,
utterance_callbacks: true,
}
}
@ -289,6 +290,18 @@ impl Backend for WinRt {
let utterances = utterances.get(&self.id).unwrap();
Ok(!utterances.is_empty())
}
fn voice(&self) -> Result<String, Error> {
unimplemented!()
}
fn list_voices(&self) -> Vec<String> {
unimplemented!()
}
fn set_voice(&mut self, voice: &str) -> Result<(), Error> {
unimplemented!()
}
}
impl Drop for WinRt {

View File

@ -16,6 +16,7 @@ use std::collections::HashMap;
#[cfg(target_os = "macos")]
use std::ffi::CStr;
use std::fmt;
use std::marker::PhantomData;
use std::sync::{Arc, Mutex};
use std::{boxed::Box, sync::RwLock};
@ -32,6 +33,8 @@ use speech_dispatcher::Error as SpeechDispatcherError;
use thiserror::Error;
#[cfg(all(windows, feature = "tolk"))]
use tolk::Tolk;
pub use unic_langid::LanguageIdentifier;
use unic_langid::LanguageIdentifierError;
mod backends;
@ -165,6 +168,7 @@ pub struct Features {
pub rate: bool,
pub stop: bool,
pub utterance_callbacks: bool,
pub voices: bool,
pub volume: bool,
}
@ -207,7 +211,7 @@ pub enum Error {
}
#[clonable]
trait Backend: Clone {
pub trait Backend<T: VoiceImpl>: Clone {
fn id(&self) -> Option<BackendId>;
fn supported_features(&self) -> Features;
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error>;
@ -228,6 +232,9 @@ trait Backend: Clone {
fn get_volume(&self) -> Result<f32, Error>;
fn set_volume(&mut self, volume: f32) -> Result<(), Error>;
fn is_speaking(&self) -> Result<bool, Error>;
fn voices(&self) -> Result<Vec<Voice<T>>, Error>;
fn voice(&self) -> Result<String, Error>;
fn set_voice(&mut self, voice: &str) -> Result<(), Error>;
}
#[derive(Default)]
@ -249,22 +256,22 @@ lazy_static! {
}
#[derive(Clone)]
pub struct Tts(Arc<RwLock<Box<dyn Backend>>>);
pub struct Tts<T: VoiceImpl>(Arc<RwLock<Box<dyn Backend<T>>>>, PhantomData<T>);
unsafe impl Send for Tts {}
unsafe impl<T: VoiceImpl> Send for Tts<T> {}
unsafe impl Sync for Tts {}
unsafe impl<T: VoiceImpl> Sync for Tts<T> {}
impl Tts {
impl<T: VoiceImpl> Tts<T> {
/**
* Create a new `TTS` instance with the specified backend.
*/
pub fn new(backend: Backends) -> Result<Tts, Error> {
pub fn new(backend: Backends) -> Result<Tts<T>, Error> {
let backend = match backend {
#[cfg(target_os = "linux")]
Backends::SpeechDispatcher => {
let tts = backends::SpeechDispatcher::new()?;
Ok(Tts(Arc::new(RwLock::new(Box::new(tts)))))
Ok(Tts(Arc::new(RwLock::new(Box::new(tts))), PhantomData))
}
#[cfg(target_arch = "wasm32")]
Backends::Web => {
@ -310,7 +317,7 @@ impl Tts {
}
}
pub fn default() -> Result<Tts, Error> {
pub fn default() -> Result<Tts<T>, Error> {
#[cfg(target_os = "linux")]
let tts = Tts::new(Backends::SpeechDispatcher);
#[cfg(all(windows, feature = "tolk"))]
@ -556,6 +563,40 @@ impl Tts {
}
}
/**
* Returns list of available voices.
*/
pub fn voices(&self) -> Result<Vec<Voice<T>>, Error> {
self.0.read().unwrap().voices()
}
/**
* Return the current speaking voice.
*/
pub fn voice(&self) -> Result<String, Error> {
let Features { voices, .. } = self.supported_features();
if voices {
self.0.read().unwrap().voice()
} else {
Err(Error::UnsupportedFeature)
}
}
/**
* Set speaking voice.
*/
pub fn set_voice<S: Into<String>>(&mut self, voice: S) -> Result<(), Error> {
let Features {
voices: voices_feature,
..
} = self.supported_features();
if voices_feature {
self.0.write().unwrap().set_voice(voice.into().as_str())
} else {
Err(Error::UnsupportedFeature)
}
}
/**
* Called when this speech synthesizer begins speaking an utterance.
*/
@ -641,7 +682,7 @@ impl Tts {
}
}
impl Drop for Tts {
impl<T: VoiceImpl> Drop for Tts<T> {
fn drop(&mut self) {
if Arc::strong_count(&self.0) <= 1 {
if let Some(id) = self.0.read().unwrap().id() {
@ -651,3 +692,18 @@ impl Drop for Tts {
}
}
}
pub enum Gender {
Other,
Male,
Female,
}
pub trait VoiceImpl: Sized {
fn id(self) -> String;
fn name(self) -> String;
fn gender(self) -> Gender;
fn language(self) -> Result<LanguageIdentifier, LanguageIdentifierError>;
}
pub struct Voice<T: VoiceImpl + Sized>(Box<T>);