From 753f6c5ecd490399bcdee30d9f07cdce81ac98d2 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 11 Aug 2020 12:11:19 -0500 Subject: [PATCH] WIP: Initial support for MacOS/`NSSpeechSynthesizer`. * Add necessary dependencies, build script, and `NSSpeechSynthesizer` backend. * Get very basic speech working. Needs a delegate to handle queued speech, and currently segfaults if one is set. --- Cargo.toml | 6 +- build.rs | 14 +++ src/backends/mod.rs | 6 ++ src/backends/ns_speech_synthesizer.rs | 128 ++++++++++++++++++++++++++ src/lib.rs | 8 ++ 5 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 build.rs create mode 100644 src/backends/ns_speech_synthesizer.rs diff --git a/Cargo.toml b/Cargo.toml index 00718a0..cb8d5c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tts" -version = "0.3.9" +version = "0.4.0" authors = ["Nolan Darilek "] repository = "https://github.com/ndarilek/tts-rs" description = "High-level Text-To-Speech (TTS) interface" @@ -23,6 +23,10 @@ tts_winrt_bindings = { version = "0.1", path="winrt_bindings" } [target.'cfg(target_os = "linux")'.dependencies] speech-dispatcher = "0.4" +[target.'cfg(target_os = "macos")'.dependencies] +cocoa-foundation = "0.1" +objc = "0.2" + [target.wasm32-unknown-unknown.dependencies] wasm-bindgen = "0.2" web-sys = { version = "0.3", features = ["SpeechSynthesis", "SpeechSynthesisUtterance", "Window", ] } diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..d32fa79 --- /dev/null +++ b/build.rs @@ -0,0 +1,14 @@ +// Copyright 2013-2015 The Servo Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +fn main() { + if std::env::var("TARGET").unwrap().contains("-apple") { + println!("cargo:rustc-link-lib=framework=AppKit"); + } +} diff --git a/src/backends/mod.rs b/src/backends/mod.rs index c8b065a..15ffedf 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -10,6 +10,9 @@ pub(crate) mod winrt; #[cfg(target_arch = "wasm32")] mod web; +#[cfg(target_os = "macos")] +mod ns_speech_synthesizer; + #[cfg(target_os = "linux")] pub use self::speech_dispatcher::*; @@ -18,3 +21,6 @@ pub use self::tolk::*; #[cfg(target_arch = "wasm32")] pub use self::web::*; + +#[cfg(target_os = "macos")] +pub use self::ns_speech_synthesizer::*; diff --git a/src/backends/ns_speech_synthesizer.rs b/src/backends/ns_speech_synthesizer.rs new file mode 100644 index 0000000..551db6a --- /dev/null +++ b/src/backends/ns_speech_synthesizer.rs @@ -0,0 +1,128 @@ +#[cfg(target_os = "macos")] +#[link(name = "AppKit", kind = "framework")] +use cocoa_foundation::base::nil; +use cocoa_foundation::foundation::NSString; +use log::{info, trace}; +use objc::declare::ClassDecl; +use objc::runtime::*; +use objc::*; + +use crate::{Backend, Error, Features}; + +pub struct NSSpeechSynthesizerBackend(*mut Object); + +impl NSSpeechSynthesizerBackend { + pub fn new() -> Self { + info!("Initializing NSSpeechSynthesizer backend"); + let mut obj: *mut Object = unsafe { msg_send![class!(NSSpeechSynthesizer), alloc] }; + obj = unsafe { msg_send![obj, init] }; + let mut decl = ClassDecl::new("MyNSSpeechSynthesizerDelegate", class!(NSObject)).unwrap(); + extern "C" fn speech_synthesizer_did_finish_speaking(_: &Object, _: Sel, _: BOOL) { + println!("Got it"); + } + unsafe { + decl.add_method( + sel!(didFinishSpeaking:), + speech_synthesizer_did_finish_speaking as extern "C" fn(&Object, Sel, BOOL) -> (), + ) + }; + let delegate_class = decl.register(); + let delegate_object: Object = unsafe { msg_send![delegate_class, alloc] }; + let _: () = unsafe { msg_send![obj, setDelegate: delegate_object] }; + NSSpeechSynthesizerBackend(obj) + } +} + +impl Backend for NSSpeechSynthesizerBackend { + fn supported_features(&self) -> Features { + Features { + stop: false, + rate: false, + pitch: false, + volume: false, + is_speaking: false, + } + } + + fn speak(&mut self, text: &str, interrupt: bool) -> Result<(), Error> { + println!("speak({}, {})", text, interrupt); + let str = unsafe { NSString::alloc(nil).init_str(text) }; + let success: BOOL = unsafe { msg_send![self.0, startSpeakingString: str] }; + println!("Comparing"); + if success == NO { + println!("Failed"); + Ok(()) + } else { + Ok(()) + } + } + + fn stop(&mut self) -> Result<(), Error> { + trace!("stop()"); + unimplemented!() + } + + fn min_rate(&self) -> f32 { + -100. + } + + fn max_rate(&self) -> f32 { + 100. + } + + fn normal_rate(&self) -> f32 { + 0. + } + + fn get_rate(&self) -> Result { + unimplemented!() + } + + fn set_rate(&mut self, rate: f32) -> Result<(), Error> { + unimplemented!() + } + + fn min_pitch(&self) -> f32 { + -100. + } + + fn max_pitch(&self) -> f32 { + 100. + } + + fn normal_pitch(&self) -> f32 { + 0. + } + + fn get_pitch(&self) -> Result { + unimplemented!() + } + + fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> { + unimplemented!() + } + + fn min_volume(&self) -> f32 { + -100. + } + + fn max_volume(&self) -> f32 { + 100. + } + + fn normal_volume(&self) -> f32 { + 0. + } + + fn get_volume(&self) -> Result { + unimplemented!() + } + + fn set_volume(&mut self, volume: f32) -> Result<(), Error> { + unimplemented!() + } + + fn is_speaking(&self) -> Result { + unimplemented!() + } +} diff --git a/src/lib.rs b/src/lib.rs index 2fa5590..cd0dcac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,8 @@ pub enum Backends { Tolk, #[cfg(windows)] WinRT, + #[cfg(target_os = "macos")] + NSSpeechSynthesizer, } pub struct Features { @@ -104,6 +106,10 @@ impl TTS { let tts = backends::winrt::WinRT::new()?; Ok(TTS(Box::new(tts))) } + #[cfg(target_os = "macos")] + Backends::NSSpeechSynthesizer => { + Ok(TTS(Box::new(backends::NSSpeechSynthesizerBackend::new()))) + } } } @@ -118,6 +124,8 @@ impl TTS { }; #[cfg(target_arch = "wasm32")] let tts = TTS::new(Backends::Web); + #[cfg(target_os = "macos")] + let tts = TTS::new(Backends::NSSpeechSynthesizer); tts }