mirror of https://github.com/ndarilek/tts-rs.git
Merge pull request #2 from francois-caddet/feature/voices
Add voices feature
This commit is contained in:
commit
6057d9c968
13
Cargo.toml
13
Cargo.toml
|
@ -8,6 +8,15 @@ license = "MIT"
|
||||||
exclude = ["*.cfg", "*.yml"]
|
exclude = ["*.cfg", "*.yml"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
[package.metadata.patch.speech-dispatcher]
|
||||||
|
version = "0.7.0"
|
||||||
|
#patches = [
|
||||||
|
# "speech-dispatcher.patch"
|
||||||
|
#]
|
||||||
|
|
||||||
|
#[patch.crates-io]
|
||||||
|
#speech-dispatcher = { path = './target/patch/speech-dispatcher-0.7.0'}
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["lib", "cdylib", "staticlib"]
|
crate-type = ["lib", "cdylib", "staticlib"]
|
||||||
|
|
||||||
|
@ -20,6 +29,7 @@ dyn-clonable = "0.9"
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
unic-langid = "0.9.0"
|
||||||
serde = { version = "1.0", optional = true, features = ["derive"] }
|
serde = { version = "1.0", optional = true, features = ["derive"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
@ -34,6 +44,7 @@ speech-dispatcher = { version = "0.13", default-features = false }
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
|
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
|
||||||
cocoa-foundation = "0.1"
|
cocoa-foundation = "0.1"
|
||||||
|
core-foundation = "0.9"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
objc = { version = "0.2", features = ["exception"] }
|
objc = { version = "0.2", features = ["exception"] }
|
||||||
|
|
||||||
|
@ -43,4 +54,4 @@ web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "Spee
|
||||||
|
|
||||||
[target.'cfg(target_os="android")'.dependencies]
|
[target.'cfg(target_os="android")'.dependencies]
|
||||||
jni = "0.19"
|
jni = "0.19"
|
||||||
ndk-glue = "0.6"
|
ndk-glue = "0.6"
|
||||||
|
|
|
@ -71,6 +71,20 @@ fn main() -> Result<(), Error> {
|
||||||
tts.speak("This is normal volume.", false)?;
|
tts.speak("This is normal volume.", false)?;
|
||||||
tts.set_volume(original_volume)?;
|
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)?;
|
tts.speak("Goodbye.", false)?;
|
||||||
let mut _input = String::new();
|
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.
|
// The below is only needed to make the example run on MacOS because there is no NSRunLoop in this context.
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -200,6 +200,18 @@ impl Backend for AppKit {
|
||||||
let is_speaking: i8 = unsafe { msg_send![self.0, isSpeaking] };
|
let is_speaking: i8 = unsafe { msg_send![self.0, isSpeaking] };
|
||||||
Ok(is_speaking != NO as i8)
|
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 {
|
impl Drop for AppKit {
|
||||||
|
|
|
@ -10,6 +10,10 @@ use objc::runtime::{Object, Sel};
|
||||||
use objc::{class, declare::ClassDecl, msg_send, sel, sel_impl};
|
use objc::{class, declare::ClassDecl, msg_send, sel, sel_impl};
|
||||||
|
|
||||||
use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS};
|
use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS};
|
||||||
|
use crate::voices::Backend as VoiceBackend;
|
||||||
|
|
||||||
|
mod voices;
|
||||||
|
use voices::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct AvFoundation {
|
pub(crate) struct AvFoundation {
|
||||||
|
@ -19,6 +23,7 @@ pub(crate) struct AvFoundation {
|
||||||
rate: f32,
|
rate: f32,
|
||||||
volume: f32,
|
volume: f32,
|
||||||
pitch: f32,
|
pitch: f32,
|
||||||
|
voice: AVSpeechSynthesisVoice,
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
@ -142,6 +147,7 @@ impl AvFoundation {
|
||||||
rate: 0.5,
|
rate: 0.5,
|
||||||
volume: 1.,
|
volume: 1.,
|
||||||
pitch: 1.,
|
pitch: 1.,
|
||||||
|
voice: AVSpeechSynthesisVoice::new(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
*backend_id += 1;
|
*backend_id += 1;
|
||||||
|
@ -161,6 +167,7 @@ impl Backend for AvFoundation {
|
||||||
pitch: true,
|
pitch: true,
|
||||||
volume: true,
|
volume: true,
|
||||||
is_speaking: true,
|
is_speaking: true,
|
||||||
|
voices: true,
|
||||||
utterance_callbacks: true,
|
utterance_callbacks: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,6 +192,7 @@ impl Backend for AvFoundation {
|
||||||
let _: () = msg_send![utterance, setVolume: self.volume];
|
let _: () = msg_send![utterance, setVolume: self.volume];
|
||||||
trace!("Setting pitch to {}", self.pitch);
|
trace!("Setting pitch to {}", self.pitch);
|
||||||
let _: () = msg_send![utterance, setPitchMultiplier: self.pitch];
|
let _: () = msg_send![utterance, setPitchMultiplier: self.pitch];
|
||||||
|
let _: () = msg_send![utterance, setVoice: self.voice];
|
||||||
trace!("Enqueuing");
|
trace!("Enqueuing");
|
||||||
let _: () = msg_send![self.synth, speakUtterance: utterance];
|
let _: () = msg_send![self.synth, speakUtterance: utterance];
|
||||||
trace!("Done queuing");
|
trace!("Done queuing");
|
||||||
|
@ -271,6 +279,19 @@ impl Backend for AvFoundation {
|
||||||
let is_speaking: i8 = unsafe { msg_send![self.synth, isSpeaking] };
|
let is_speaking: i8 = unsafe { msg_send![self.synth, isSpeaking] };
|
||||||
Ok(is_speaking != NO as i8)
|
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 {
|
impl Drop for AvFoundation {
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
|
||||||
|
use objc::runtime::*;
|
||||||
|
use objc::*;
|
||||||
|
use core_foundation::array::CFArray;
|
||||||
|
use cocoa_foundation::foundation::NSString;
|
||||||
|
use cocoa_foundation::base::{nil,id};
|
||||||
|
use core_foundation::string::CFString;
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -81,6 +81,7 @@ impl Backend for SpeechDispatcher {
|
||||||
pitch: true,
|
pitch: true,
|
||||||
volume: true,
|
volume: true,
|
||||||
is_speaking: true,
|
is_speaking: true,
|
||||||
|
voices: false,
|
||||||
utterance_callbacks: true,
|
utterance_callbacks: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,6 +180,18 @@ impl Backend for SpeechDispatcher {
|
||||||
let is_speaking = speaking.get(&self.0.client_id()).unwrap();
|
let is_speaking = speaking.get(&self.0.client_id()).unwrap();
|
||||||
Ok(*is_speaking)
|
Ok(*is_speaking)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 SpeechDispatcher {
|
impl Drop for SpeechDispatcher {
|
||||||
|
|
|
@ -108,4 +108,16 @@ impl Backend for Tolk {
|
||||||
fn is_speaking(&self) -> Result<bool, Error> {
|
fn is_speaking(&self) -> Result<bool, Error> {
|
||||||
unimplemented!()
|
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!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ impl Backend for Web {
|
||||||
pitch: true,
|
pitch: true,
|
||||||
volume: true,
|
volume: true,
|
||||||
is_speaking: true,
|
is_speaking: true,
|
||||||
|
voices: true,
|
||||||
utterance_callbacks: true,
|
utterance_callbacks: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,6 +197,18 @@ impl Backend for Web {
|
||||||
Err(Error::NoneError)
|
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 {
|
impl Drop for Web {
|
||||||
|
|
|
@ -145,6 +145,7 @@ impl Backend for WinRt {
|
||||||
pitch: true,
|
pitch: true,
|
||||||
volume: true,
|
volume: true,
|
||||||
is_speaking: true,
|
is_speaking: true,
|
||||||
|
voices: true,
|
||||||
utterance_callbacks: true,
|
utterance_callbacks: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -289,6 +290,18 @@ impl Backend for WinRt {
|
||||||
let utterances = utterances.get(&self.id).unwrap();
|
let utterances = utterances.get(&self.id).unwrap();
|
||||||
Ok(!utterances.is_empty())
|
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 {
|
impl Drop for WinRt {
|
||||||
|
|
41
src/lib.rs
41
src/lib.rs
|
@ -34,6 +34,7 @@ use thiserror::Error;
|
||||||
use tolk::Tolk;
|
use tolk::Tolk;
|
||||||
|
|
||||||
mod backends;
|
mod backends;
|
||||||
|
mod voices;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
@ -165,6 +166,7 @@ pub struct Features {
|
||||||
pub rate: bool,
|
pub rate: bool,
|
||||||
pub stop: bool,
|
pub stop: bool,
|
||||||
pub utterance_callbacks: bool,
|
pub utterance_callbacks: bool,
|
||||||
|
pub voices: bool,
|
||||||
pub volume: bool,
|
pub volume: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,7 +209,7 @@ pub enum Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[clonable]
|
#[clonable]
|
||||||
trait Backend: Clone {
|
pub trait Backend: Clone {
|
||||||
fn id(&self) -> Option<BackendId>;
|
fn id(&self) -> Option<BackendId>;
|
||||||
fn supported_features(&self) -> Features;
|
fn supported_features(&self) -> Features;
|
||||||
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error>;
|
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error>;
|
||||||
|
@ -228,6 +230,9 @@ trait Backend: Clone {
|
||||||
fn get_volume(&self) -> Result<f32, Error>;
|
fn get_volume(&self) -> Result<f32, Error>;
|
||||||
fn set_volume(&mut self, volume: f32) -> Result<(), Error>;
|
fn set_volume(&mut self, volume: f32) -> Result<(), Error>;
|
||||||
fn is_speaking(&self) -> Result<bool, Error>;
|
fn is_speaking(&self) -> Result<bool, Error>;
|
||||||
|
fn voice(&self) -> Result<String, Error>;
|
||||||
|
fn list_voices(&self) -> Vec<String>;
|
||||||
|
fn set_voice(&mut self, voice: &str) -> Result<(),Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -556,6 +561,40 @@ impl Tts {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of available voices.
|
||||||
|
*/
|
||||||
|
pub fn list_voices(&self) -> Vec<String> {
|
||||||
|
self.0.read().unwrap().list_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.
|
* Called when this speech synthesizer begins speaking an utterance.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
|
||||||
|
pub use unic_langid::LanguageIdentifier;
|
||||||
|
|
||||||
|
pub enum Gender {
|
||||||
|
Other,
|
||||||
|
Male,
|
||||||
|
Female,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Backend: Sized {
|
||||||
|
type Backend: crate::Backend;
|
||||||
|
fn from_id(id: String) -> Self;
|
||||||
|
fn from_language(lang: LanguageIdentifier) -> Self;
|
||||||
|
fn list() -> Vec<Self>;
|
||||||
|
fn name(self) -> String;
|
||||||
|
fn gender(self) -> Gender;
|
||||||
|
fn id(self) -> String;
|
||||||
|
fn language(self) -> LanguageIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Voice<T: Backend + Sized>(Box<T>);
|
Loading…
Reference in New Issue