1
0
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:
mcb2003 2021-09-28 09:33:58 +01:00
commit 060082947c
24 changed files with 350 additions and 133 deletions

View File

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

View File

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

@ -1,2 +1,3 @@
Cargo.lock Cargo.lock
target target
*.dll

View File

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

View File

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

View File

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

View File

@ -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)?;

View File

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

View File

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

View File

@ -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();

View File

@ -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)?;

View File

@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-unknown"

1
examples/web/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist

11
examples/web/Cargo.toml Normal file
View 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
View 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
View 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);
}

View File

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

View File

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

View File

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

View File

@ -1 +1 @@
::windows::include_bindings!(); ::windows::include_bindings!();

View File

@ -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();

View File

@ -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();

View File

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

View File

@ -1,7 +0,0 @@
fn main() {
windows::build!(
windows::media::core::MediaSource
windows::media::playback::{MediaPlaybackState, MediaPlayer}
windows::media::speech_synthesis::SpeechSynthesizer
);
}