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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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> {
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")]
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)
}
}

View File

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

View File

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

View File

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

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