mirror of
https://github.com/ndarilek/tts-rs.git
synced 2024-11-25 07:39:37 +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:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-features --target wasm32-unknown-unknown
|
||||
|
||||
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
|
||||
- uses: actions-rs/install@v0.1
|
||||
with:
|
||||
target: wasm32-unknown-unknown
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
components: rustfmt, clippy
|
||||
override: true
|
||||
- run: |
|
||||
cargo login $CARGO_TOKEN
|
||||
cd winrt_bindings
|
||||
cargo publish || true
|
||||
crate: cargo-make
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: make
|
||||
args: build-web-example
|
||||
|
||||
publish:
|
||||
name: Publish
|
||||
|
|
21
.github/workflows/test.yml
vendored
21
.github/workflows/test.yml
vendored
|
@ -82,3 +82,24 @@ jobs:
|
|||
with:
|
||||
command: apk
|
||||
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
|
||||
target
|
||||
*.dll
|
16
Cargo.toml
16
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "tts"
|
||||
version = "0.14.0"
|
||||
version = "0.17.3"
|
||||
authors = ["Nolan Darilek <nolan@thewordnerd.info>"]
|
||||
repository = "https://github.com/ndarilek/tts-rs"
|
||||
description = "High-level Text-To-Speech (TTS) interface"
|
||||
|
@ -29,9 +29,11 @@ env_logger = "0.8"
|
|||
cbindgen = {version = "0.18.0", optional = true}
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
tolk = { version = "0.3", optional = true }
|
||||
windows = "0.2"
|
||||
tts_winrt_bindings = { version = "0.3", path="winrt_bindings" }
|
||||
tolk = { version = "0.5", optional = true }
|
||||
windows = "0.9"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
windows = "0.9"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
speech-dispatcher = "0.7"
|
||||
|
@ -39,12 +41,12 @@ speech-dispatcher = "0.7"
|
|||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
|
||||
cocoa-foundation = "0.1"
|
||||
libc = "0.2"
|
||||
objc = "0.2"
|
||||
objc = { version = "0.2", features = ["exception"] }
|
||||
|
||||
[target.wasm32-unknown-unknown.dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "SpeechSynthesisErrorCode", "SpeechSynthesisErrorEvent", "SpeechSynthesisEvent", "SpeechSynthesisUtterance", "Window", ] }
|
||||
|
||||
[target.'cfg(target_os="android")'.dependencies]
|
||||
jni = "0.18"
|
||||
ndk-glue = "0.2"
|
||||
jni = "0.19"
|
||||
ndk-glue = "0.3"
|
||||
|
|
|
@ -18,3 +18,21 @@ script = [
|
|||
[tasks.log-android]
|
||||
command = "adb"
|
||||
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() {
|
||||
#[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") {
|
||||
println!("cargo:rustc-link-lib=framework=AVFoundation");
|
||||
if !std::env::var("CARGO_CFG_TARGET_OS")
|
||||
|
|
|
@ -12,7 +12,7 @@ use tts::*;
|
|||
|
||||
fn main() -> Result<(), Error> {
|
||||
env_logger::init();
|
||||
let mut tts = TTS::default()?;
|
||||
let mut tts = Tts::default()?;
|
||||
let mut bottles = 99;
|
||||
while bottles > 0 {
|
||||
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.
|
||||
#[allow(unreachable_code)]
|
||||
fn run() -> Result<(), Error> {
|
||||
let mut tts = TTS::default()?;
|
||||
let mut tts = Tts::default()?;
|
||||
let Features {
|
||||
utterance_callbacks,
|
||||
..
|
||||
|
|
|
@ -11,7 +11,12 @@ use tts::*;
|
|||
|
||||
fn main() -> Result<(), Error> {
|
||||
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 {
|
||||
utterance_callbacks,
|
||||
..
|
||||
|
|
|
@ -4,7 +4,7 @@ use tts::*;
|
|||
|
||||
fn main() -> Result<(), Error> {
|
||||
env_logger::init();
|
||||
let mut tts = TTS::default()?;
|
||||
let mut tts = Tts::default()?;
|
||||
println!("Press Enter and wait for speech.");
|
||||
loop {
|
||||
let mut _input = String::new();
|
||||
|
|
|
@ -4,7 +4,7 @@ use tts::*;
|
|||
|
||||
fn main() -> Result<(), Error> {
|
||||
env_logger::init();
|
||||
let mut tts = TTS::default()?;
|
||||
let mut tts = Tts::default()?;
|
||||
let mut phrase = 1;
|
||||
loop {
|
||||
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> {
|
||||
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")]
|
||||
use std::sync::Mutex;
|
||||
|
||||
use cocoa_foundation::base::{id, nil};
|
||||
use cocoa_foundation::base::{id, nil, NO};
|
||||
use cocoa_foundation::foundation::NSString;
|
||||
use lazy_static::lazy_static;
|
||||
use log::{info, trace};
|
||||
|
@ -37,16 +37,22 @@ impl AvFoundation {
|
|||
_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(
|
||||
|
@ -55,16 +61,22 @@ impl AvFoundation {
|
|||
_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(
|
||||
|
@ -73,16 +85,22 @@ impl AvFoundation {
|
|||
_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 {
|
||||
|
@ -107,16 +125,20 @@ impl AvFoundation {
|
|||
let delegate_obj: *mut Object = unsafe { msg_send![delegate_class, new] };
|
||||
let mut backend_id = NEXT_BACKEND_ID.lock().unwrap();
|
||||
let rv = unsafe {
|
||||
trace!("Creating synth");
|
||||
let synth: *mut Object = msg_send![class!(AVSpeechSynthesizer), new];
|
||||
trace!("Allocated {:?}", synth);
|
||||
delegate_obj
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.set_ivar("backend_id", *backend_id);
|
||||
trace!("Set backend ID in delegate");
|
||||
let _: () = msg_send![synth, setDelegate: delegate_obj];
|
||||
trace!("Assigned delegate: {:?}", delegate_obj);
|
||||
AvFoundation {
|
||||
id: BackendId::AvFoundation(*backend_id),
|
||||
delegate: delegate_obj,
|
||||
synth: synth,
|
||||
synth,
|
||||
rate: 0.5,
|
||||
volume: 1.,
|
||||
pitch: 1.,
|
||||
|
@ -145,18 +167,27 @@ impl Backend for AvFoundation {
|
|||
|
||||
fn speak(&mut self, text: &str, interrupt: bool) -> Result<Option<UtteranceId>, Error> {
|
||||
trace!("speak({}, {})", text, interrupt);
|
||||
if interrupt {
|
||||
if interrupt && self.is_speaking()? {
|
||||
self.stop()?;
|
||||
}
|
||||
let utterance: id;
|
||||
let mut utterance: id;
|
||||
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];
|
||||
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];
|
||||
trace!("Setting volume to {}", self.volume);
|
||||
let _: () = msg_send![utterance, setVolume: self.volume];
|
||||
trace!("Setting pitch to {}", self.pitch);
|
||||
let _: () = msg_send![utterance, setPitchMultiplier: self.pitch];
|
||||
trace!("Enqueuing");
|
||||
let _: () = msg_send![self.synth, speakUtterance: utterance];
|
||||
trace!("Done queuing");
|
||||
}
|
||||
Ok(Some(UtteranceId::AvFoundation(utterance)))
|
||||
}
|
||||
|
@ -208,6 +239,7 @@ impl Backend for AvFoundation {
|
|||
}
|
||||
|
||||
fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> {
|
||||
trace!("set_pitch({})", pitch);
|
||||
self.pitch = pitch;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -229,13 +261,15 @@ impl Backend for AvFoundation {
|
|||
}
|
||||
|
||||
fn set_volume(&mut self, volume: f32) -> Result<(), Error> {
|
||||
trace!("set_volume({})", volume);
|
||||
self.volume = volume;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_speaking(&self) -> Result<bool, Error> {
|
||||
trace!("is_speaking()");
|
||||
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"))]
|
||||
use std::sync::Arc;
|
||||
|
||||
use log::{info, trace};
|
||||
use tolk::Tolk as TolkPtr;
|
||||
|
||||
use crate::{Backend, BackendId, Error, Features, UtteranceId};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Tolk(TolkPtr);
|
||||
pub(crate) struct Tolk(Arc<TolkPtr>);
|
||||
|
||||
impl Tolk {
|
||||
pub(crate) fn new() -> Option<Self> {
|
||||
|
|
|
@ -5,22 +5,27 @@ use std::sync::Mutex;
|
|||
use lazy_static::lazy_static;
|
||||
use log::{info, trace};
|
||||
|
||||
use tts_winrt_bindings::windows::media::playback::{
|
||||
MediaPlaybackState, MediaPlayer, MediaPlayerAudioCategory,
|
||||
mod bindings;
|
||||
|
||||
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};
|
||||
|
||||
impl From<windows::Error> for Error {
|
||||
fn from(e: windows::Error) -> Self {
|
||||
Error::WinRT(e)
|
||||
Error::WinRt(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WinRT {
|
||||
pub struct WinRt {
|
||||
id: BackendId,
|
||||
synth: SpeechSynthesizer,
|
||||
player: MediaPlayer,
|
||||
|
@ -54,15 +59,15 @@ lazy_static! {
|
|||
};
|
||||
}
|
||||
|
||||
impl WinRT {
|
||||
impl WinRt {
|
||||
pub fn new() -> std::result::Result<Self, Error> {
|
||||
info!("Initializing WinRT backend");
|
||||
let synth = SpeechSynthesizer::new()?;
|
||||
let player = MediaPlayer::new()?;
|
||||
player.set_real_time_playback(true)?;
|
||||
player.set_audio_category(MediaPlayerAudioCategory::Speech)?;
|
||||
player.SetRealTimePlayback(true)?;
|
||||
player.SetAudioCategory(MediaPlayerAudioCategory::Speech)?;
|
||||
let mut backend_id = NEXT_BACKEND_ID.lock().unwrap();
|
||||
let bid = BackendId::WinRT(*backend_id);
|
||||
let bid = BackendId::WinRt(*backend_id);
|
||||
*backend_id += 1;
|
||||
drop(backend_id);
|
||||
{
|
||||
|
@ -76,7 +81,7 @@ impl WinRT {
|
|||
backend_to_speech_synthesizer.insert(bid, synth.clone());
|
||||
drop(backend_to_speech_synthesizer);
|
||||
let bid_clone = bid;
|
||||
player.media_ended(TypedEventHandler::new(
|
||||
player.MediaEnded(TypedEventHandler::new(
|
||||
move |sender: &Option<MediaPlayer>, _args| {
|
||||
if let Some(sender) = sender {
|
||||
let backend_to_media_player = BACKEND_TO_MEDIA_PLAYER.lock().unwrap();
|
||||
|
@ -97,19 +102,17 @@ impl WinRT {
|
|||
.iter()
|
||||
.find(|v| *v.0 == bid_clone);
|
||||
if let Some((_, tts)) = id {
|
||||
tts.options()?.set_speaking_rate(utterance.rate.into())?;
|
||||
tts.options()?.set_audio_pitch(utterance.pitch.into())?;
|
||||
tts.options()?.set_audio_volume(utterance.volume.into())?;
|
||||
tts.Options()?.SetSpeakingRate(utterance.rate.into())?;
|
||||
tts.Options()?.SetAudioPitch(utterance.pitch.into())?;
|
||||
tts.Options()?.SetAudioVolume(utterance.volume.into())?;
|
||||
let stream = tts
|
||||
.synthesize_text_to_stream_async(
|
||||
utterance.text.as_str(),
|
||||
)?
|
||||
.SynthesizeTextToStreamAsync(utterance.text.as_str())?
|
||||
.get()?;
|
||||
let content_type = stream.content_type()?;
|
||||
let content_type = stream.ContentType()?;
|
||||
let source =
|
||||
MediaSource::create_from_stream(stream, content_type)?;
|
||||
sender.set_source(source)?;
|
||||
sender.play()?;
|
||||
MediaSource::CreateFromStream(stream, content_type)?;
|
||||
sender.SetSource(source)?;
|
||||
sender.Play()?;
|
||||
if let Some(callback) = callbacks.utterance_begin.as_mut() {
|
||||
callback(utterance.id);
|
||||
}
|
||||
|
@ -133,7 +136,7 @@ impl WinRT {
|
|||
}
|
||||
}
|
||||
|
||||
impl Backend for WinRT {
|
||||
impl Backend for WinRt {
|
||||
fn id(&self) -> Option<BackendId> {
|
||||
Some(self.id)
|
||||
}
|
||||
|
@ -159,7 +162,7 @@ impl Backend for WinRT {
|
|||
}
|
||||
let utterance_id = {
|
||||
let mut uid = NEXT_UTTERANCE_ID.lock().unwrap();
|
||||
let utterance_id = UtteranceId::WinRT(*uid);
|
||||
let utterance_id = UtteranceId::WinRt(*uid);
|
||||
*uid += 1;
|
||||
utterance_id
|
||||
};
|
||||
|
@ -178,17 +181,15 @@ impl Backend for WinRT {
|
|||
utterances.push_back(utterance);
|
||||
}
|
||||
}
|
||||
if no_utterances
|
||||
&& self.player.playback_session()?.playback_state()? != MediaPlaybackState::Playing
|
||||
{
|
||||
self.synth.options()?.set_speaking_rate(self.rate.into())?;
|
||||
self.synth.options()?.set_audio_pitch(self.pitch.into())?;
|
||||
self.synth.options()?.set_audio_volume(self.volume.into())?;
|
||||
let stream = self.synth.synthesize_text_to_stream_async(text)?.get()?;
|
||||
let content_type = stream.content_type()?;
|
||||
let source = MediaSource::create_from_stream(stream, content_type)?;
|
||||
self.player.set_source(source)?;
|
||||
self.player.play()?;
|
||||
if no_utterances {
|
||||
self.synth.Options()?.SetSpeakingRate(self.rate.into())?;
|
||||
self.synth.Options()?.SetAudioPitch(self.pitch.into())?;
|
||||
self.synth.Options()?.SetAudioVolume(self.volume.into())?;
|
||||
let stream = self.synth.SynthesizeTextToStreamAsync(text)?.get()?;
|
||||
let content_type = stream.ContentType()?;
|
||||
let source = MediaSource::CreateFromStream(stream, content_type)?;
|
||||
self.player.SetSource(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() {
|
||||
|
@ -216,7 +217,7 @@ impl Backend for WinRT {
|
|||
if let Some(utterances) = utterances.get_mut(&self.id) {
|
||||
utterances.clear();
|
||||
}
|
||||
self.player.pause()?;
|
||||
self.player.Pause()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -233,7 +234,7 @@ impl Backend for WinRT {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -255,7 +256,7 @@ impl Backend for WinRT {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -277,7 +278,7 @@ impl Backend for WinRT {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -293,7 +294,7 @@ impl Backend for WinRT {
|
|||
}
|
||||
}
|
||||
|
||||
impl Drop for WinRT {
|
||||
impl Drop for WinRt {
|
||||
fn drop(&mut self) {
|
||||
let id = self.id;
|
||||
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")]
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
use thiserror::Error;
|
||||
#[cfg(all(windows, feature = "tolk"))]
|
||||
use tolk::Tolk;
|
||||
|
||||
mod backends;
|
||||
#[cfg(feature = "ffi")]
|
||||
|
@ -42,7 +44,7 @@ pub enum Backends {
|
|||
#[cfg(all(windows, feature = "tolk"))]
|
||||
Tolk,
|
||||
#[cfg(windows)]
|
||||
WinRT,
|
||||
WinRt,
|
||||
#[cfg(target_os = "macos")]
|
||||
AppKit,
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
|
@ -58,7 +60,7 @@ pub enum BackendId {
|
|||
#[cfg(target_arch = "wasm32")]
|
||||
Web(u64),
|
||||
#[cfg(windows)]
|
||||
WinRT(u64),
|
||||
WinRt(u64),
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
AvFoundation(u64),
|
||||
#[cfg(target_os = "android")]
|
||||
|
@ -72,7 +74,7 @@ pub enum UtteranceId {
|
|||
#[cfg(target_arch = "wasm32")]
|
||||
Web(u64),
|
||||
#[cfg(windows)]
|
||||
WinRT(u64),
|
||||
WinRt(u64),
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
AvFoundation(id),
|
||||
#[cfg(target_os = "android")]
|
||||
|
@ -109,7 +111,7 @@ impl Default for Features {
|
|||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("IO error: {0}")]
|
||||
IO(#[from] std::io::Error),
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Value not received")]
|
||||
NoneError,
|
||||
#[error("Operation failed")]
|
||||
|
@ -119,7 +121,7 @@ pub enum Error {
|
|||
JavaScriptError(wasm_bindgen::JsValue),
|
||||
#[cfg(windows)]
|
||||
#[error("WinRT error")]
|
||||
WinRT(windows::Error),
|
||||
WinRt(windows::Error),
|
||||
#[error("Unsupported feature")]
|
||||
UnsupportedFeature,
|
||||
#[error("Out of range")]
|
||||
|
@ -172,47 +174,47 @@ lazy_static! {
|
|||
}
|
||||
|
||||
#[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.
|
||||
*/
|
||||
pub fn new(backend: Backends) -> Result<TTS, Error> {
|
||||
pub fn new(backend: Backends) -> Result<Tts, Error> {
|
||||
let backend = match backend {
|
||||
#[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")]
|
||||
Backends::Web => {
|
||||
let tts = backends::Web::new()?;
|
||||
Ok(TTS(Box::new(tts)))
|
||||
Ok(Tts(Box::new(tts)))
|
||||
}
|
||||
#[cfg(all(windows, feature = "tolk"))]
|
||||
Backends::Tolk => {
|
||||
let tts = backends::Tolk::new();
|
||||
if let Some(tts) = tts {
|
||||
Ok(TTS(Box::new(tts)))
|
||||
Ok(Tts(Box::new(tts)))
|
||||
} else {
|
||||
Err(Error::NoneError)
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
Backends::WinRT => {
|
||||
let tts = backends::WinRT::new()?;
|
||||
Ok(TTS(Box::new(tts)))
|
||||
Backends::WinRt => {
|
||||
let tts = backends::WinRt::new()?;
|
||||
Ok(Tts(Box::new(tts)))
|
||||
}
|
||||
#[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"))]
|
||||
Backends::AvFoundation => Ok(TTS(Box::new(backends::AvFoundation::new()))),
|
||||
Backends::AvFoundation => Ok(Tts(Box::new(backends::AvFoundation::new()))),
|
||||
#[cfg(target_os = "android")]
|
||||
Backends::Android => {
|
||||
let tts = backends::Android::new()?;
|
||||
Ok(TTS(Box::new(tts)))
|
||||
Ok(Tts(Box::new(tts)))
|
||||
}
|
||||
};
|
||||
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")]
|
||||
let tts = TTS::new(Backends::SpeechDispatcher);
|
||||
let tts = Tts::new(Backends::SpeechDispatcher);
|
||||
#[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)
|
||||
} else {
|
||||
TTS::new(Backends::WinRT)
|
||||
Tts::new(Backends::WinRt)
|
||||
};
|
||||
#[cfg(all(windows, not(feature = "tolk")))]
|
||||
let tts = TTS::new(Backends::WinRT);
|
||||
let tts = Tts::new(Backends::WinRt);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let tts = TTS::new(Backends::Web);
|
||||
let tts = Tts::new(Backends::Web);
|
||||
#[cfg(target_os = "macos")]
|
||||
let tts = unsafe {
|
||||
// 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 = CStr::from_ptr(str);
|
||||
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_parts: Vec<&str> = version.split(".").collect();
|
||||
let version_parts: Vec<&str> = version.split('.').collect();
|
||||
let major_version: i8 = version_parts[0].parse().unwrap();
|
||||
let minor_version: i8 = version_parts[1].parse().unwrap();
|
||||
if major_version >= 11 || minor_version >= 14 {
|
||||
TTS::new(Backends::AvFoundation)
|
||||
Tts::new(Backends::AvFoundation)
|
||||
} else {
|
||||
TTS::new(Backends::AppKit)
|
||||
Tts::new(Backends::AppKit)
|
||||
}
|
||||
};
|
||||
#[cfg(target_os = "ios")]
|
||||
let tts = TTS::new(Backends::AvFoundation);
|
||||
let tts = Tts::new(Backends::AvFoundation);
|
||||
#[cfg(target_os = "android")]
|
||||
let tts = TTS::new(Backends::Android);
|
||||
let tts = Tts::new(Backends::Android);
|
||||
tts
|
||||
}
|
||||
|
||||
|
@ -531,9 +533,27 @@ impl TTS {
|
|||
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) {
|
||||
if let Some(id) = self.0.id() {
|
||||
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