mirror of
https://github.com/ndarilek/tts-rs.git
synced 2024-11-25 07:19:38 +00:00
Merge branch 'master' into c-ffi
This commit is contained in:
commit
060082947c
26
.github/workflows/release.yml
vendored
26
.github/workflows/release.yml
vendored
|
@ -61,27 +61,13 @@ jobs:
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
args: --all-features --target wasm32-unknown-unknown
|
args: --all-features --target wasm32-unknown-unknown
|
||||||
|
- uses: actions-rs/install@v0.1
|
||||||
publish_winrt_bindings:
|
|
||||||
name: Publish winrt_bindings
|
|
||||||
runs-on: windows-latest
|
|
||||||
needs: [check]
|
|
||||||
env:
|
|
||||||
CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: Swatinem/rust-cache@v1
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
with:
|
||||||
target: wasm32-unknown-unknown
|
crate: cargo-make
|
||||||
profile: minimal
|
- uses: actions-rs/cargo@v1
|
||||||
toolchain: stable
|
with:
|
||||||
components: rustfmt, clippy
|
command: make
|
||||||
override: true
|
args: build-web-example
|
||||||
- run: |
|
|
||||||
cargo login $CARGO_TOKEN
|
|
||||||
cd winrt_bindings
|
|
||||||
cargo publish || true
|
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Publish
|
name: Publish
|
||||||
|
|
21
.github/workflows/test.yml
vendored
21
.github/workflows/test.yml
vendored
|
@ -82,3 +82,24 @@ jobs:
|
||||||
with:
|
with:
|
||||||
command: apk
|
command: apk
|
||||||
args: build
|
args: build
|
||||||
|
|
||||||
|
check_web_example:
|
||||||
|
name: Check Web Example
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: Swatinem/rust-cache@v1
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
target: wasm32-unknown-unknown
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
components: rustfmt, clippy
|
||||||
|
override: true
|
||||||
|
- uses: actions-rs/install@v0.1
|
||||||
|
with:
|
||||||
|
crate: cargo-make
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: make
|
||||||
|
args: build-web-example
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
target
|
target
|
||||||
|
*.dll
|
16
Cargo.toml
16
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tts"
|
name = "tts"
|
||||||
version = "0.14.0"
|
version = "0.17.3"
|
||||||
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"
|
||||||
|
@ -29,9 +29,11 @@ env_logger = "0.8"
|
||||||
cbindgen = {version = "0.18.0", optional = true}
|
cbindgen = {version = "0.18.0", optional = true}
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
tolk = { version = "0.3", optional = true }
|
tolk = { version = "0.5", optional = true }
|
||||||
windows = "0.2"
|
windows = "0.9"
|
||||||
tts_winrt_bindings = { version = "0.3", path="winrt_bindings" }
|
|
||||||
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
|
windows = "0.9"
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
speech-dispatcher = "0.7"
|
speech-dispatcher = "0.7"
|
||||||
|
@ -39,12 +41,12 @@ speech-dispatcher = "0.7"
|
||||||
[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"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
objc = "0.2"
|
objc = { version = "0.2", features = ["exception"] }
|
||||||
|
|
||||||
[target.wasm32-unknown-unknown.dependencies]
|
[target.wasm32-unknown-unknown.dependencies]
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "SpeechSynthesisErrorCode", "SpeechSynthesisErrorEvent", "SpeechSynthesisEvent", "SpeechSynthesisUtterance", "Window", ] }
|
web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "SpeechSynthesisErrorCode", "SpeechSynthesisErrorEvent", "SpeechSynthesisEvent", "SpeechSynthesisUtterance", "Window", ] }
|
||||||
|
|
||||||
[target.'cfg(target_os="android")'.dependencies]
|
[target.'cfg(target_os="android")'.dependencies]
|
||||||
jni = "0.18"
|
jni = "0.19"
|
||||||
ndk-glue = "0.2"
|
ndk-glue = "0.3"
|
||||||
|
|
|
@ -18,3 +18,21 @@ script = [
|
||||||
[tasks.log-android]
|
[tasks.log-android]
|
||||||
command = "adb"
|
command = "adb"
|
||||||
args = ["logcat", "RustStdoutStderr:D", "*:S"]
|
args = ["logcat", "RustStdoutStderr:D", "*:S"]
|
||||||
|
|
||||||
|
[tasks.install-trunk]
|
||||||
|
install_crate = { crate_name = "trunk", binary = "trunk", test_arg = "--help" }
|
||||||
|
|
||||||
|
[tasks.install-wasm-bindgen-cli]
|
||||||
|
install_crate = { crate_name = "wasm-bindgen-cli", binary = "wasm-bindgen", test_arg = "--help" }
|
||||||
|
|
||||||
|
[tasks.build-web-example]
|
||||||
|
dependencies = ["install-trunk", "install-wasm-bindgen-cli"]
|
||||||
|
cwd = "examples/web"
|
||||||
|
command = "trunk"
|
||||||
|
args = ["build"]
|
||||||
|
|
||||||
|
[tasks.run-web-example]
|
||||||
|
dependencies = ["install-trunk", "install-wasm-bindgen-cli"]
|
||||||
|
cwd = "examples/web"
|
||||||
|
command = "trunk"
|
||||||
|
args = ["serve"]
|
||||||
|
|
10
build.rs
10
build.rs
|
@ -1,4 +1,14 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
|
#[cfg(windows)]
|
||||||
|
if std::env::var("TARGET").unwrap().contains("windows") {
|
||||||
|
windows::build!(
|
||||||
|
Windows::Foundation::{EventRegistrationToken, IAsyncOperation, TypedEventHandler},
|
||||||
|
Windows::Media::Core::MediaSource,
|
||||||
|
Windows::Media::Playback::{MediaPlaybackSession, MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory},
|
||||||
|
Windows::Media::SpeechSynthesis::{SpeechSynthesisStream, SpeechSynthesizer, SpeechSynthesizerOptions},
|
||||||
|
Windows::Storage::Streams::IRandomAccessStream,
|
||||||
|
);
|
||||||
|
}
|
||||||
if std::env::var("TARGET").unwrap().contains("-apple") {
|
if std::env::var("TARGET").unwrap().contains("-apple") {
|
||||||
println!("cargo:rustc-link-lib=framework=AVFoundation");
|
println!("cargo:rustc-link-lib=framework=AVFoundation");
|
||||||
if !std::env::var("CARGO_CFG_TARGET_OS")
|
if !std::env::var("CARGO_CFG_TARGET_OS")
|
||||||
|
|
|
@ -12,7 +12,7 @@ use tts::*;
|
||||||
|
|
||||||
fn main() -> Result<(), Error> {
|
fn main() -> Result<(), Error> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let mut tts = TTS::default()?;
|
let mut tts = Tts::default()?;
|
||||||
let mut bottles = 99;
|
let mut bottles = 99;
|
||||||
while bottles > 0 {
|
while bottles > 0 {
|
||||||
tts.speak(format!("{} bottles of beer on the wall,", bottles), false)?;
|
tts.speak(format!("{} bottles of beer on the wall,", bottles), false)?;
|
||||||
|
|
|
@ -4,7 +4,7 @@ use tts::*;
|
||||||
// Without it, the `TTS` instance gets dropped before callbacks can run.
|
// Without it, the `TTS` instance gets dropped before callbacks can run.
|
||||||
#[allow(unreachable_code)]
|
#[allow(unreachable_code)]
|
||||||
fn run() -> Result<(), Error> {
|
fn run() -> Result<(), Error> {
|
||||||
let mut tts = TTS::default()?;
|
let mut tts = Tts::default()?;
|
||||||
let Features {
|
let Features {
|
||||||
utterance_callbacks,
|
utterance_callbacks,
|
||||||
..
|
..
|
||||||
|
|
|
@ -11,7 +11,12 @@ use tts::*;
|
||||||
|
|
||||||
fn main() -> Result<(), Error> {
|
fn main() -> Result<(), Error> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let mut tts = TTS::default()?;
|
let mut tts = Tts::default()?;
|
||||||
|
if Tts::screen_reader_available() {
|
||||||
|
println!("A screen reader is available on this platform.");
|
||||||
|
} else {
|
||||||
|
println!("No screen reader is available on this platform.");
|
||||||
|
}
|
||||||
let Features {
|
let Features {
|
||||||
utterance_callbacks,
|
utterance_callbacks,
|
||||||
..
|
..
|
||||||
|
|
|
@ -4,7 +4,7 @@ use tts::*;
|
||||||
|
|
||||||
fn main() -> Result<(), Error> {
|
fn main() -> Result<(), Error> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let mut tts = TTS::default()?;
|
let mut tts = Tts::default()?;
|
||||||
println!("Press Enter and wait for speech.");
|
println!("Press Enter and wait for speech.");
|
||||||
loop {
|
loop {
|
||||||
let mut _input = String::new();
|
let mut _input = String::new();
|
||||||
|
|
|
@ -4,7 +4,7 @@ use tts::*;
|
||||||
|
|
||||||
fn main() -> Result<(), Error> {
|
fn main() -> Result<(), Error> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let mut tts = TTS::default()?;
|
let mut tts = Tts::default()?;
|
||||||
let mut phrase = 1;
|
let mut phrase = 1;
|
||||||
loop {
|
loop {
|
||||||
tts.speak(format!("Phrase {}", phrase), false)?;
|
tts.speak(format!("Phrase {}", phrase), false)?;
|
||||||
|
|
2
examples/web/.cargo/config
Normal file
2
examples/web/.cargo/config
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[build]
|
||||||
|
target = "wasm32-unknown-unknown"
|
1
examples/web/.gitignore
vendored
Normal file
1
examples/web/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
dist
|
11
examples/web/Cargo.toml
Normal file
11
examples/web/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "web"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Nolan Darilek <nolan@thewordnerd.info>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
seed = "0.8"
|
||||||
|
tts = { path = "../.." }
|
12
examples/web/index.html
Normal file
12
examples/web/index.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Example</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
111
examples/web/src/main.rs
Normal file
111
examples/web/src/main.rs
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
#![allow(clippy::wildcard_imports)]
|
||||||
|
use seed::{prelude::*, *};
|
||||||
|
|
||||||
|
use tts::Tts;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Model {
|
||||||
|
text: String,
|
||||||
|
tts: Tts,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum Msg {
|
||||||
|
TextChanged(String),
|
||||||
|
RateChanged(String),
|
||||||
|
PitchChanged(String),
|
||||||
|
VolumeChanged(String),
|
||||||
|
Speak,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(_: Url, _: &mut impl Orders<Msg>) -> Model {
|
||||||
|
let tts = Tts::default().unwrap();
|
||||||
|
Model {
|
||||||
|
text: Default::default(),
|
||||||
|
tts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(msg: Msg, model: &mut Model, _: &mut impl Orders<Msg>) {
|
||||||
|
use Msg::*;
|
||||||
|
match msg {
|
||||||
|
TextChanged(text) => model.text = text,
|
||||||
|
RateChanged(rate) => {
|
||||||
|
let rate = rate.parse::<f32>().unwrap();
|
||||||
|
model.tts.set_rate(rate).unwrap();
|
||||||
|
}
|
||||||
|
PitchChanged(pitch) => {
|
||||||
|
let pitch = pitch.parse::<f32>().unwrap();
|
||||||
|
model.tts.set_pitch(pitch).unwrap();
|
||||||
|
}
|
||||||
|
VolumeChanged(volume) => {
|
||||||
|
let volume = volume.parse::<f32>().unwrap();
|
||||||
|
model.tts.set_volume(volume).unwrap();
|
||||||
|
}
|
||||||
|
Speak => {
|
||||||
|
model.tts.speak(&model.text, false).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(model: &Model) -> Node<Msg> {
|
||||||
|
form![
|
||||||
|
div![label![
|
||||||
|
"Text to speak",
|
||||||
|
input![
|
||||||
|
attrs! {
|
||||||
|
At::Value => model.text,
|
||||||
|
At::AutoFocus => AtValue::None,
|
||||||
|
},
|
||||||
|
input_ev(Ev::Input, Msg::TextChanged)
|
||||||
|
],
|
||||||
|
],],
|
||||||
|
div![label![
|
||||||
|
"Rate",
|
||||||
|
input![
|
||||||
|
attrs! {
|
||||||
|
At::Type => "number",
|
||||||
|
At::Value => model.tts.get_rate().unwrap(),
|
||||||
|
At::Min => model.tts.min_rate(),
|
||||||
|
At::Max => model.tts.max_rate()
|
||||||
|
},
|
||||||
|
input_ev(Ev::Input, Msg::RateChanged)
|
||||||
|
],
|
||||||
|
],],
|
||||||
|
div![label![
|
||||||
|
"Pitch",
|
||||||
|
input![
|
||||||
|
attrs! {
|
||||||
|
At::Type => "number",
|
||||||
|
At::Value => model.tts.get_pitch().unwrap(),
|
||||||
|
At::Min => model.tts.min_pitch(),
|
||||||
|
At::Max => model.tts.max_pitch()
|
||||||
|
},
|
||||||
|
input_ev(Ev::Input, Msg::PitchChanged)
|
||||||
|
],
|
||||||
|
],],
|
||||||
|
div![label![
|
||||||
|
"Volume",
|
||||||
|
input![
|
||||||
|
attrs! {
|
||||||
|
At::Type => "number",
|
||||||
|
At::Value => model.tts.get_volume().unwrap(),
|
||||||
|
At::Min => model.tts.min_volume(),
|
||||||
|
At::Max => model.tts.max_volume()
|
||||||
|
},
|
||||||
|
input_ev(Ev::Input, Msg::VolumeChanged)
|
||||||
|
],
|
||||||
|
],],
|
||||||
|
button![
|
||||||
|
"Speak",
|
||||||
|
ev(Ev::Click, |e| {
|
||||||
|
e.prevent_default();
|
||||||
|
Msg::Speak
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::start("app", init, update, view);
|
||||||
|
}
|
|
@ -198,7 +198,7 @@ impl Backend for AppKit {
|
||||||
|
|
||||||
fn is_speaking(&self) -> Result<bool, Error> {
|
fn is_speaking(&self) -> Result<bool, Error> {
|
||||||
let is_speaking: i8 = unsafe { msg_send![self.0, isSpeaking] };
|
let is_speaking: i8 = unsafe { msg_send![self.0, isSpeaking] };
|
||||||
Ok(is_speaking == YES)
|
Ok(is_speaking != NO as i8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
#[link(name = "AVFoundation", kind = "framework")]
|
#[link(name = "AVFoundation", kind = "framework")]
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use cocoa_foundation::base::{id, nil};
|
use cocoa_foundation::base::{id, nil, NO};
|
||||||
use cocoa_foundation::foundation::NSString;
|
use cocoa_foundation::foundation::NSString;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use log::{info, trace};
|
use log::{info, trace};
|
||||||
|
@ -37,16 +37,22 @@ impl AvFoundation {
|
||||||
_synth: *const Object,
|
_synth: *const Object,
|
||||||
utterance: id,
|
utterance: id,
|
||||||
) {
|
) {
|
||||||
|
trace!("speech_synthesizer_did_start_speech_utterance");
|
||||||
unsafe {
|
unsafe {
|
||||||
let backend_id: u64 = *this.get_ivar("backend_id");
|
let backend_id: u64 = *this.get_ivar("backend_id");
|
||||||
let backend_id = BackendId::AvFoundation(backend_id);
|
let backend_id = BackendId::AvFoundation(backend_id);
|
||||||
|
trace!("Locking callbacks");
|
||||||
let mut callbacks = CALLBACKS.lock().unwrap();
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
trace!("Locked");
|
||||||
let callbacks = callbacks.get_mut(&backend_id).unwrap();
|
let callbacks = callbacks.get_mut(&backend_id).unwrap();
|
||||||
if let Some(callback) = callbacks.utterance_begin.as_mut() {
|
if let Some(callback) = callbacks.utterance_begin.as_mut() {
|
||||||
|
trace!("Calling utterance_begin");
|
||||||
let utterance_id = UtteranceId::AvFoundation(utterance);
|
let utterance_id = UtteranceId::AvFoundation(utterance);
|
||||||
callback(utterance_id);
|
callback(utterance_id);
|
||||||
|
trace!("Called");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
trace!("Done speech_synthesizer_did_start_speech_utterance");
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn speech_synthesizer_did_finish_speech_utterance(
|
extern "C" fn speech_synthesizer_did_finish_speech_utterance(
|
||||||
|
@ -55,16 +61,22 @@ impl AvFoundation {
|
||||||
_synth: *const Object,
|
_synth: *const Object,
|
||||||
utterance: id,
|
utterance: id,
|
||||||
) {
|
) {
|
||||||
|
trace!("speech_synthesizer_did_finish_speech_utterance");
|
||||||
unsafe {
|
unsafe {
|
||||||
let backend_id: u64 = *this.get_ivar("backend_id");
|
let backend_id: u64 = *this.get_ivar("backend_id");
|
||||||
let backend_id = BackendId::AvFoundation(backend_id);
|
let backend_id = BackendId::AvFoundation(backend_id);
|
||||||
|
trace!("Locking callbacks");
|
||||||
let mut callbacks = CALLBACKS.lock().unwrap();
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
trace!("Locked");
|
||||||
let callbacks = callbacks.get_mut(&backend_id).unwrap();
|
let callbacks = callbacks.get_mut(&backend_id).unwrap();
|
||||||
if let Some(callback) = callbacks.utterance_end.as_mut() {
|
if let Some(callback) = callbacks.utterance_end.as_mut() {
|
||||||
|
trace!("Calling utterance_end");
|
||||||
let utterance_id = UtteranceId::AvFoundation(utterance);
|
let utterance_id = UtteranceId::AvFoundation(utterance);
|
||||||
callback(utterance_id);
|
callback(utterance_id);
|
||||||
|
trace!("Called");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
trace!("Done speech_synthesizer_did_finish_speech_utterance");
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn speech_synthesizer_did_cancel_speech_utterance(
|
extern "C" fn speech_synthesizer_did_cancel_speech_utterance(
|
||||||
|
@ -73,16 +85,22 @@ impl AvFoundation {
|
||||||
_synth: *const Object,
|
_synth: *const Object,
|
||||||
utterance: id,
|
utterance: id,
|
||||||
) {
|
) {
|
||||||
|
trace!("speech_synthesizer_did_cancel_speech_utterance");
|
||||||
unsafe {
|
unsafe {
|
||||||
let backend_id: u64 = *this.get_ivar("backend_id");
|
let backend_id: u64 = *this.get_ivar("backend_id");
|
||||||
let backend_id = BackendId::AvFoundation(backend_id);
|
let backend_id = BackendId::AvFoundation(backend_id);
|
||||||
|
trace!("Locking callbacks");
|
||||||
let mut callbacks = CALLBACKS.lock().unwrap();
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
trace!("Locked");
|
||||||
let callbacks = callbacks.get_mut(&backend_id).unwrap();
|
let callbacks = callbacks.get_mut(&backend_id).unwrap();
|
||||||
if let Some(callback) = callbacks.utterance_stop.as_mut() {
|
if let Some(callback) = callbacks.utterance_stop.as_mut() {
|
||||||
|
trace!("Calling utterance_stop");
|
||||||
let utterance_id = UtteranceId::AvFoundation(utterance);
|
let utterance_id = UtteranceId::AvFoundation(utterance);
|
||||||
callback(utterance_id);
|
callback(utterance_id);
|
||||||
|
trace!("Called");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
trace!("Done speech_synthesizer_did_cancel_speech_utterance");
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
@ -107,16 +125,20 @@ impl AvFoundation {
|
||||||
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 mut backend_id = NEXT_BACKEND_ID.lock().unwrap();
|
||||||
let rv = unsafe {
|
let rv = unsafe {
|
||||||
|
trace!("Creating synth");
|
||||||
let synth: *mut Object = msg_send![class!(AVSpeechSynthesizer), new];
|
let synth: *mut Object = msg_send![class!(AVSpeechSynthesizer), new];
|
||||||
|
trace!("Allocated {:?}", synth);
|
||||||
delegate_obj
|
delegate_obj
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_ivar("backend_id", *backend_id);
|
.set_ivar("backend_id", *backend_id);
|
||||||
|
trace!("Set backend ID in delegate");
|
||||||
let _: () = msg_send![synth, setDelegate: delegate_obj];
|
let _: () = msg_send![synth, setDelegate: delegate_obj];
|
||||||
|
trace!("Assigned delegate: {:?}", delegate_obj);
|
||||||
AvFoundation {
|
AvFoundation {
|
||||||
id: BackendId::AvFoundation(*backend_id),
|
id: BackendId::AvFoundation(*backend_id),
|
||||||
delegate: delegate_obj,
|
delegate: delegate_obj,
|
||||||
synth: synth,
|
synth,
|
||||||
rate: 0.5,
|
rate: 0.5,
|
||||||
volume: 1.,
|
volume: 1.,
|
||||||
pitch: 1.,
|
pitch: 1.,
|
||||||
|
@ -145,18 +167,27 @@ impl Backend for AvFoundation {
|
||||||
|
|
||||||
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error> {
|
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error> {
|
||||||
trace!("speak({}, {})", text, interrupt);
|
trace!("speak({}, {})", text, interrupt);
|
||||||
if interrupt {
|
if interrupt && self.is_speaking()? {
|
||||||
self.stop()?;
|
self.stop()?;
|
||||||
}
|
}
|
||||||
let utterance: id;
|
let mut utterance: id;
|
||||||
unsafe {
|
unsafe {
|
||||||
let str = NSString::alloc(nil).init_str(text);
|
trace!("Allocating utterance string");
|
||||||
|
let mut str = NSString::alloc(nil);
|
||||||
|
str = str.init_str(text);
|
||||||
|
trace!("Allocating utterance");
|
||||||
utterance = msg_send![class!(AVSpeechUtterance), alloc];
|
utterance = msg_send![class!(AVSpeechUtterance), alloc];
|
||||||
let _: () = msg_send![utterance, initWithString: str];
|
trace!("Initializing utterance");
|
||||||
|
utterance = msg_send![utterance, initWithString: str];
|
||||||
|
trace!("Setting rate to {}", self.rate);
|
||||||
let _: () = msg_send![utterance, setRate: self.rate];
|
let _: () = msg_send![utterance, setRate: self.rate];
|
||||||
|
trace!("Setting volume to {}", self.volume);
|
||||||
let _: () = msg_send![utterance, setVolume: self.volume];
|
let _: () = msg_send![utterance, setVolume: self.volume];
|
||||||
|
trace!("Setting pitch to {}", self.pitch);
|
||||||
let _: () = msg_send![utterance, setPitchMultiplier: self.pitch];
|
let _: () = msg_send![utterance, setPitchMultiplier: self.pitch];
|
||||||
|
trace!("Enqueuing");
|
||||||
let _: () = msg_send![self.synth, speakUtterance: utterance];
|
let _: () = msg_send![self.synth, speakUtterance: utterance];
|
||||||
|
trace!("Done queuing");
|
||||||
}
|
}
|
||||||
Ok(Some(UtteranceId::AvFoundation(utterance)))
|
Ok(Some(UtteranceId::AvFoundation(utterance)))
|
||||||
}
|
}
|
||||||
|
@ -208,6 +239,7 @@ impl Backend for AvFoundation {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> {
|
fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> {
|
||||||
|
trace!("set_pitch({})", pitch);
|
||||||
self.pitch = pitch;
|
self.pitch = pitch;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -229,13 +261,15 @@ impl Backend for AvFoundation {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_volume(&mut self, volume: f32) -> Result<(), Error> {
|
fn set_volume(&mut self, volume: f32) -> Result<(), Error> {
|
||||||
|
trace!("set_volume({})", volume);
|
||||||
self.volume = volume;
|
self.volume = volume;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_speaking(&self) -> Result<bool, Error> {
|
fn is_speaking(&self) -> Result<bool, Error> {
|
||||||
|
trace!("is_speaking()");
|
||||||
let is_speaking: i8 = unsafe { msg_send![self.synth, isSpeaking] };
|
let is_speaking: i8 = unsafe { msg_send![self.synth, isSpeaking] };
|
||||||
Ok(is_speaking == 1)
|
Ok(is_speaking != NO as i8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
#[cfg(all(windows, feature = "tolk"))]
|
#[cfg(all(windows, feature = "tolk"))]
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use log::{info, trace};
|
use log::{info, trace};
|
||||||
use tolk::Tolk as TolkPtr;
|
use tolk::Tolk as TolkPtr;
|
||||||
|
|
||||||
use crate::{Backend, BackendId, Error, Features, UtteranceId};
|
use crate::{Backend, BackendId, Error, Features, UtteranceId};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct Tolk(TolkPtr);
|
pub(crate) struct Tolk(Arc<TolkPtr>);
|
||||||
|
|
||||||
impl Tolk {
|
impl Tolk {
|
||||||
pub(crate) fn new() -> Option<Self> {
|
pub(crate) fn new() -> Option<Self> {
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
::windows::include_bindings!();
|
::windows::include_bindings!();
|
|
@ -5,22 +5,27 @@ use std::sync::Mutex;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use log::{info, trace};
|
use log::{info, trace};
|
||||||
|
|
||||||
use tts_winrt_bindings::windows::media::playback::{
|
mod bindings;
|
||||||
MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory,
|
|
||||||
|
use bindings::Windows::{
|
||||||
|
Foundation::TypedEventHandler,
|
||||||
|
Media::{
|
||||||
|
Core::MediaSource,
|
||||||
|
Playback::{MediaPlayer, MediaPlayerAudioCategory},
|
||||||
|
SpeechSynthesis::SpeechSynthesizer,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use tts_winrt_bindings::windows::media::speech_synthesis::SpeechSynthesizer;
|
|
||||||
use tts_winrt_bindings::windows::{foundation::TypedEventHandler, media::core::MediaSource};
|
|
||||||
|
|
||||||
use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS};
|
use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS};
|
||||||
|
|
||||||
impl From<windows::Error> for Error {
|
impl From<windows::Error> for Error {
|
||||||
fn from(e: windows::Error) -> Self {
|
fn from(e: windows::Error) -> Self {
|
||||||
Error::WinRT(e)
|
Error::WinRt(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct WinRT {
|
pub struct WinRt {
|
||||||
id: BackendId,
|
id: BackendId,
|
||||||
synth: SpeechSynthesizer,
|
synth: SpeechSynthesizer,
|
||||||
player: MediaPlayer,
|
player: MediaPlayer,
|
||||||
|
@ -54,15 +59,15 @@ lazy_static! {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WinRT {
|
impl WinRt {
|
||||||
pub fn new() -> std::result::Result<Self, Error> {
|
pub fn new() -> std::result::Result<Self, Error> {
|
||||||
info!("Initializing WinRT backend");
|
info!("Initializing WinRT backend");
|
||||||
let synth = SpeechSynthesizer::new()?;
|
let synth = SpeechSynthesizer::new()?;
|
||||||
let player = MediaPlayer::new()?;
|
let player = MediaPlayer::new()?;
|
||||||
player.set_real_time_playback(true)?;
|
player.SetRealTimePlayback(true)?;
|
||||||
player.set_audio_category(MediaPlayerAudioCategory::Speech)?;
|
player.SetAudioCategory(MediaPlayerAudioCategory::Speech)?;
|
||||||
let mut backend_id = NEXT_BACKEND_ID.lock().unwrap();
|
let mut backend_id = NEXT_BACKEND_ID.lock().unwrap();
|
||||||
let bid = BackendId::WinRT(*backend_id);
|
let bid = BackendId::WinRt(*backend_id);
|
||||||
*backend_id += 1;
|
*backend_id += 1;
|
||||||
drop(backend_id);
|
drop(backend_id);
|
||||||
{
|
{
|
||||||
|
@ -76,7 +81,7 @@ impl WinRT {
|
||||||
backend_to_speech_synthesizer.insert(bid, synth.clone());
|
backend_to_speech_synthesizer.insert(bid, synth.clone());
|
||||||
drop(backend_to_speech_synthesizer);
|
drop(backend_to_speech_synthesizer);
|
||||||
let bid_clone = bid;
|
let bid_clone = bid;
|
||||||
player.media_ended(TypedEventHandler::new(
|
player.MediaEnded(TypedEventHandler::new(
|
||||||
move |sender: &Option<MediaPlayer>, _args| {
|
move |sender: &Option<MediaPlayer>, _args| {
|
||||||
if let Some(sender) = sender {
|
if let Some(sender) = sender {
|
||||||
let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap();
|
let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap();
|
||||||
|
@ -97,19 +102,17 @@ impl WinRT {
|
||||||
.iter()
|
.iter()
|
||||||
.find(|v| *v.0 == bid_clone);
|
.find(|v| *v.0 == bid_clone);
|
||||||
if let Some((_, tts)) = id {
|
if let Some((_, tts)) = id {
|
||||||
tts.options()?.set_speaking_rate(utterance.rate.into())?;
|
tts.Options()?.SetSpeakingRate(utterance.rate.into())?;
|
||||||
tts.options()?.set_audio_pitch(utterance.pitch.into())?;
|
tts.Options()?.SetAudioPitch(utterance.pitch.into())?;
|
||||||
tts.options()?.set_audio_volume(utterance.volume.into())?;
|
tts.Options()?.SetAudioVolume(utterance.volume.into())?;
|
||||||
let stream = tts
|
let stream = tts
|
||||||
.synthesize_text_to_stream_async(
|
.SynthesizeTextToStreamAsync(utterance.text.as_str())?
|
||||||
utterance.text.as_str(),
|
|
||||||
)?
|
|
||||||
.get()?;
|
.get()?;
|
||||||
let content_type = stream.content_type()?;
|
let content_type = stream.ContentType()?;
|
||||||
let source =
|
let source =
|
||||||
MediaSource::create_from_stream(stream, content_type)?;
|
MediaSource::CreateFromStream(stream, content_type)?;
|
||||||
sender.set_source(source)?;
|
sender.SetSource(source)?;
|
||||||
sender.play()?;
|
sender.Play()?;
|
||||||
if let Some(callback) = callbacks.utterance_begin.as_mut() {
|
if let Some(callback) = callbacks.utterance_begin.as_mut() {
|
||||||
callback(utterance.id);
|
callback(utterance.id);
|
||||||
}
|
}
|
||||||
|
@ -133,7 +136,7 @@ impl WinRT {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backend for WinRT {
|
impl Backend for WinRt {
|
||||||
fn id(&self) -> Option<BackendId> {
|
fn id(&self) -> Option<BackendId> {
|
||||||
Some(self.id)
|
Some(self.id)
|
||||||
}
|
}
|
||||||
|
@ -159,7 +162,7 @@ impl Backend for WinRT {
|
||||||
}
|
}
|
||||||
let utterance_id = {
|
let utterance_id = {
|
||||||
let mut uid = NEXT_UTTERANCE_ID.lock().unwrap();
|
let mut uid = NEXT_UTTERANCE_ID.lock().unwrap();
|
||||||
let utterance_id = UtteranceId::WinRT(*uid);
|
let utterance_id = UtteranceId::WinRt(*uid);
|
||||||
*uid += 1;
|
*uid += 1;
|
||||||
utterance_id
|
utterance_id
|
||||||
};
|
};
|
||||||
|
@ -178,17 +181,15 @@ impl Backend for WinRT {
|
||||||
utterances.push_back(utterance);
|
utterances.push_back(utterance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if no_utterances
|
if no_utterances {
|
||||||
&& self.player.playback_session()?.playback_state()? != MediaPlaybackState::Playing
|
self.synth.Options()?.SetSpeakingRate(self.rate.into())?;
|
||||||
{
|
self.synth.Options()?.SetAudioPitch(self.pitch.into())?;
|
||||||
self.synth.options()?.set_speaking_rate(self.rate.into())?;
|
self.synth.Options()?.SetAudioVolume(self.volume.into())?;
|
||||||
self.synth.options()?.set_audio_pitch(self.pitch.into())?;
|
let stream = self.synth.SynthesizeTextToStreamAsync(text)?.get()?;
|
||||||
self.synth.options()?.set_audio_volume(self.volume.into())?;
|
let content_type = stream.ContentType()?;
|
||||||
let stream = self.synth.synthesize_text_to_stream_async(text)?.get()?;
|
let source = MediaSource::CreateFromStream(stream, content_type)?;
|
||||||
let content_type = stream.content_type()?;
|
self.player.SetSource(source)?;
|
||||||
let source = MediaSource::create_from_stream(stream, content_type)?;
|
self.player.Play()?;
|
||||||
self.player.set_source(source)?;
|
|
||||||
self.player.play()?;
|
|
||||||
let mut callbacks = CALLBACKS.lock().unwrap();
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
let callbacks = callbacks.get_mut(&self.id).unwrap();
|
let callbacks = callbacks.get_mut(&self.id).unwrap();
|
||||||
if let Some(callback) = callbacks.utterance_begin.as_mut() {
|
if let Some(callback) = callbacks.utterance_begin.as_mut() {
|
||||||
|
@ -216,7 +217,7 @@ impl Backend for WinRT {
|
||||||
if let Some(utterances) = utterances.get_mut(&self.id) {
|
if let Some(utterances) = utterances.get_mut(&self.id) {
|
||||||
utterances.clear();
|
utterances.clear();
|
||||||
}
|
}
|
||||||
self.player.pause()?;
|
self.player.Pause()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,7 +234,7 @@ impl Backend for WinRT {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_rate(&self) -> std::result::Result<f32, Error> {
|
fn get_rate(&self) -> std::result::Result<f32, Error> {
|
||||||
let rate = self.synth.options()?.speaking_rate()?;
|
let rate = self.synth.Options()?.SpeakingRate()?;
|
||||||
Ok(rate as f32)
|
Ok(rate as f32)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,7 +256,7 @@ impl Backend for WinRT {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_pitch(&self) -> std::result::Result<f32, Error> {
|
fn get_pitch(&self) -> std::result::Result<f32, Error> {
|
||||||
let pitch = self.synth.options()?.audio_pitch()?;
|
let pitch = self.synth.Options()?.AudioPitch()?;
|
||||||
Ok(pitch as f32)
|
Ok(pitch as f32)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,7 +278,7 @@ impl Backend for WinRT {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_volume(&self) -> std::result::Result<f32, Error> {
|
fn get_volume(&self) -> std::result::Result<f32, Error> {
|
||||||
let volume = self.synth.options()?.audio_volume()?;
|
let volume = self.synth.Options()?.AudioVolume()?;
|
||||||
Ok(volume as f32)
|
Ok(volume as f32)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,7 +294,7 @@ impl Backend for WinRT {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for WinRT {
|
impl Drop for WinRt {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let mut backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap();
|
let mut backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap();
|
84
src/lib.rs
84
src/lib.rs
|
@ -27,6 +27,8 @@ use libc::c_char;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use objc::{class, msg_send, sel, sel_impl};
|
use objc::{class, msg_send, sel, sel_impl};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
#[cfg(all(windows, feature = "tolk"))]
|
||||||
|
use tolk::Tolk;
|
||||||
|
|
||||||
mod backends;
|
mod backends;
|
||||||
#[cfg(feature = "ffi")]
|
#[cfg(feature = "ffi")]
|
||||||
|
@ -42,7 +44,7 @@ pub enum Backends {
|
||||||
#[cfg(all(windows, feature = "tolk"))]
|
#[cfg(all(windows, feature = "tolk"))]
|
||||||
Tolk,
|
Tolk,
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
WinRT,
|
WinRt,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
AppKit,
|
AppKit,
|
||||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
|
@ -58,7 +60,7 @@ pub enum BackendId {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
Web(u64),
|
Web(u64),
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
WinRT(u64),
|
WinRt(u64),
|
||||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
AvFoundation(u64),
|
AvFoundation(u64),
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
|
@ -72,7 +74,7 @@ pub enum UtteranceId {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
Web(u64),
|
Web(u64),
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
WinRT(u64),
|
WinRt(u64),
|
||||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||||
AvFoundation(id),
|
AvFoundation(id),
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
|
@ -109,7 +111,7 @@ impl Default for Features {
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("IO error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
IO(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
#[error("Value not received")]
|
#[error("Value not received")]
|
||||||
NoneError,
|
NoneError,
|
||||||
#[error("Operation failed")]
|
#[error("Operation failed")]
|
||||||
|
@ -119,7 +121,7 @@ pub enum Error {
|
||||||
JavaScriptError(wasm_bindgen::JsValue),
|
JavaScriptError(wasm_bindgen::JsValue),
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
#[error("WinRT error")]
|
#[error("WinRT error")]
|
||||||
WinRT(windows::Error),
|
WinRt(windows::Error),
|
||||||
#[error("Unsupported feature")]
|
#[error("Unsupported feature")]
|
||||||
UnsupportedFeature,
|
UnsupportedFeature,
|
||||||
#[error("Out of range")]
|
#[error("Out of range")]
|
||||||
|
@ -172,47 +174,47 @@ lazy_static! {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TTS(Box<dyn Backend>);
|
pub struct Tts(Box<dyn Backend>);
|
||||||
|
|
||||||
unsafe impl Send for TTS {}
|
unsafe impl Send for Tts {}
|
||||||
|
|
||||||
unsafe impl Sync for TTS {}
|
unsafe impl Sync for Tts {}
|
||||||
|
|
||||||
impl TTS {
|
impl Tts {
|
||||||
/**
|
/**
|
||||||
* Create a new `TTS` instance with the specified backend.
|
* Create a new `TTS` instance with the specified backend.
|
||||||
*/
|
*/
|
||||||
pub fn new(backend: Backends) -> Result<TTS, Error> {
|
pub fn new(backend: Backends) -> Result<Tts, Error> {
|
||||||
let backend = match backend {
|
let backend = match backend {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
Backends::SpeechDispatcher => Ok(TTS(Box::new(backends::SpeechDispatcher::new()))),
|
Backends::SpeechDispatcher => Ok(Tts(Box::new(backends::SpeechDispatcher::new()))),
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
Backends::Web => {
|
Backends::Web => {
|
||||||
let tts = backends::Web::new()?;
|
let tts = backends::Web::new()?;
|
||||||
Ok(TTS(Box::new(tts)))
|
Ok(Tts(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(Box::new(tts)))
|
Ok(Tts(Box::new(tts)))
|
||||||
} else {
|
} else {
|
||||||
Err(Error::NoneError)
|
Err(Error::NoneError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
Backends::WinRT => {
|
Backends::WinRt => {
|
||||||
let tts = backends::WinRT::new()?;
|
let tts = backends::WinRt::new()?;
|
||||||
Ok(TTS(Box::new(tts)))
|
Ok(Tts(Box::new(tts)))
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
Backends::AppKit => Ok(TTS(Box::new(backends::AppKit::new()))),
|
Backends::AppKit => Ok(Tts(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(Box::new(backends::AvFoundation::new()))),
|
Backends::AvFoundation => Ok(Tts(Box::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(Box::new(tts)))
|
Ok(Tts(Box::new(tts)))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Ok(backend) = backend {
|
if let Ok(backend) = backend {
|
||||||
|
@ -226,19 +228,19 @@ impl TTS {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default() -> Result<TTS, Error> {
|
pub fn default() -> Result<Tts, Error> {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let tts = TTS::new(Backends::SpeechDispatcher);
|
let tts = Tts::new(Backends::SpeechDispatcher);
|
||||||
#[cfg(all(windows, feature = "tolk"))]
|
#[cfg(all(windows, feature = "tolk"))]
|
||||||
let tts = if let Ok(tts) = TTS::new(Backends::Tolk) {
|
let tts = if let Ok(tts) = Tts::new(Backends::Tolk) {
|
||||||
Ok(tts)
|
Ok(tts)
|
||||||
} else {
|
} else {
|
||||||
TTS::new(Backends::WinRT)
|
Tts::new(Backends::WinRt)
|
||||||
};
|
};
|
||||||
#[cfg(all(windows, not(feature = "tolk")))]
|
#[cfg(all(windows, not(feature = "tolk")))]
|
||||||
let tts = TTS::new(Backends::WinRT);
|
let tts = Tts::new(Backends::WinRt);
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
let tts = TTS::new(Backends::Web);
|
let tts = Tts::new(Backends::Web);
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let tts = unsafe {
|
let tts = unsafe {
|
||||||
// Needed because the Rust NSProcessInfo structs report bogus values, and I don't want to pull in a full bindgen stack.
|
// Needed because the Rust NSProcessInfo structs report bogus values, and I don't want to pull in a full bindgen stack.
|
||||||
|
@ -247,21 +249,21 @@ impl TTS {
|
||||||
let str: *const c_char = msg_send![version, UTF8String];
|
let str: *const c_char = msg_send![version, UTF8String];
|
||||||
let str = CStr::from_ptr(str);
|
let str = CStr::from_ptr(str);
|
||||||
let str = str.to_string_lossy();
|
let str = str.to_string_lossy();
|
||||||
let version: Vec<&str> = str.split(" ").collect();
|
let version: Vec<&str> = str.split(' ').collect();
|
||||||
let version = version[1];
|
let version = version[1];
|
||||||
let version_parts: Vec<&str> = version.split(".").collect();
|
let version_parts: Vec<&str> = version.split('.').collect();
|
||||||
let major_version: i8 = version_parts[0].parse().unwrap();
|
let major_version: i8 = version_parts[0].parse().unwrap();
|
||||||
let minor_version: i8 = version_parts[1].parse().unwrap();
|
let minor_version: i8 = version_parts[1].parse().unwrap();
|
||||||
if major_version >= 11 || minor_version >= 14 {
|
if major_version >= 11 || minor_version >= 14 {
|
||||||
TTS::new(Backends::AvFoundation)
|
Tts::new(Backends::AvFoundation)
|
||||||
} else {
|
} else {
|
||||||
TTS::new(Backends::AppKit)
|
Tts::new(Backends::AppKit)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
#[cfg(target_os = "ios")]
|
#[cfg(target_os = "ios")]
|
||||||
let tts = TTS::new(Backends::AvFoundation);
|
let tts = Tts::new(Backends::AvFoundation);
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
let tts = TTS::new(Backends::Android);
|
let tts = Tts::new(Backends::Android);
|
||||||
tts
|
tts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -531,9 +533,27 @@ impl TTS {
|
||||||
Err(Error::UnsupportedFeature)
|
Err(Error::UnsupportedFeature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns `true` if a screen reader is available to provide speech.
|
||||||
|
*/
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
pub fn screen_reader_available() -> bool {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
#[cfg(feature = "tolk")]
|
||||||
|
{
|
||||||
|
let tolk = Tolk::new();
|
||||||
|
return tolk.detect_screen_reader().is_some();
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "tolk"))]
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for TTS {
|
impl Drop for Tts {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(id) = self.0.id() {
|
if let Some(id) = self.0.id() {
|
||||||
let mut callbacks = CALLBACKS.lock().unwrap();
|
let mut callbacks = CALLBACKS.lock().unwrap();
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "tts_winrt_bindings"
|
|
||||||
version = "0.3.0"
|
|
||||||
authors = ["Nolan Darilek <nolan@thewordnerd.info>"]
|
|
||||||
description = "Internal crate used by `tts`"
|
|
||||||
license = "MIT"
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
windows = "0.2"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
windows = "0.2"
|
|
|
@ -1,7 +0,0 @@
|
||||||
fn main() {
|
|
||||||
windows::build!(
|
|
||||||
windows::media::core::MediaSource
|
|
||||||
windows::media::playback::{MediaPlaybackState, MediaPlayer}
|
|
||||||
windows::media::speech_synthesis::SpeechSynthesizer
|
|
||||||
);
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user