Add AVFoundation backend, used automatically on MacOS 10.14 and above.

This commit is contained in:
Nolan Darilek 2020-08-13 11:08:00 -05:00
parent 2d0ab8889a
commit c5b1ff1944
6 changed files with 182 additions and 3 deletions

View File

@ -25,6 +25,7 @@ speech-dispatcher = "0.4"
[target.'cfg(target_os = "macos")'.dependencies]
cocoa-foundation = "0.1"
libc = "0.2"
objc = "0.2"
[target.wasm32-unknown-unknown.dependencies]

View File

@ -10,5 +10,6 @@
fn main() {
if std::env::var("TARGET").unwrap().contains("-apple") {
println!("cargo:rustc-link-lib=framework=AppKit");
println!("cargo:rustc-link-lib=framework=AVFoundation");
}
}

View File

@ -13,7 +13,7 @@ pub struct AppKit(*mut Object, *mut Object);
impl AppKit {
pub fn new() -> Self {
info!("Initializing NSSpeechSynthesizer backend");
info!("Initializing AppKit backend");
unsafe {
let obj: *mut Object = msg_send![class!(NSSpeechSynthesizer), new];
let mut decl =

View File

@ -0,0 +1,145 @@
#[cfg(target_os = "macos")]
#[link(name = "AVFoundation", kind = "framework")]
use cocoa_foundation::base::{id, nil};
use cocoa_foundation::foundation::NSString;
use log::{info, trace};
use objc::runtime::*;
use objc::*;
use crate::{Backend, Error, Features};
pub struct AvFoundation {
synth: *mut Object,
rate: f32,
volume: f32,
pitch: f32,
}
impl AvFoundation {
pub fn new() -> Self {
info!("Initializing AVFoundation backend");
unsafe {
let synth: *mut Object = msg_send![class!(AVSpeechSynthesizer), new];
AvFoundation {
synth: synth,
rate: 0.5,
volume: 1.,
pitch: 1.,
}
}
}
}
impl Backend for AvFoundation {
fn supported_features(&self) -> Features {
Features {
stop: true,
rate: true,
pitch: true,
volume: true,
is_speaking: true,
}
}
fn speak(&mut self, text: &str, interrupt: bool) -> Result<(), Error> {
trace!("speak({}, {})", text, interrupt);
if interrupt {
self.stop()?;
}
unsafe {
let str = NSString::alloc(nil).init_str(text);
let utterance: id = msg_send![class!(AVSpeechUtterance), alloc];
let _: () = msg_send![utterance, initWithString: str];
let _: () = msg_send![utterance, setRate: self.rate];
let _: () = msg_send![utterance, setVolume: self.volume];
let _: () = msg_send![utterance, setPitchMultiplier: self.pitch];
let _: () = msg_send![self.synth, speakUtterance: utterance];
}
Ok(())
}
fn stop(&mut self) -> Result<(), Error> {
trace!("stop()");
unsafe {
let _: () = msg_send![self.synth, stopSpeakingAtBoundary: 0];
}
Ok(())
}
fn min_rate(&self) -> f32 {
0.1
}
fn max_rate(&self) -> f32 {
2.
}
fn normal_rate(&self) -> f32 {
0.5
}
fn get_rate(&self) -> Result<f32, Error> {
Ok(self.rate)
}
fn set_rate(&mut self, rate: f32) -> Result<(), Error> {
trace!("set_rate({})", rate);
self.rate = rate;
Ok(())
}
fn min_pitch(&self) -> f32 {
0.5
}
fn max_pitch(&self) -> f32 {
2.0
}
fn normal_pitch(&self) -> f32 {
1.0
}
fn get_pitch(&self) -> Result<f32, Error> {
Ok(self.pitch)
}
fn set_pitch(&mut self, pitch: f32) -> Result<(), Error> {
self.pitch = pitch;
Ok(())
}
fn min_volume(&self) -> f32 {
0.
}
fn max_volume(&self) -> f32 {
1.
}
fn normal_volume(&self) -> f32 {
1.
}
fn get_volume(&self) -> Result<f32, Error> {
Ok(self.volume)
}
fn set_volume(&mut self, volume: f32) -> Result<(), Error> {
self.volume = volume;
Ok(())
}
fn is_speaking(&self) -> Result<bool, Error> {
let is_speaking: i8 = unsafe { msg_send![self.synth, isSpeaking] };
Ok(is_speaking == YES)
}
}
impl Drop for AvFoundation {
fn drop(&mut self) {
unsafe {
let _: Object = msg_send![self.synth, release];
}
}
}

View File

@ -13,6 +13,9 @@ mod web;
#[cfg(target_os = "macos")]
mod appkit;
#[cfg(target_os = "macos")]
mod av_foundation;
#[cfg(target_os = "linux")]
pub use self::speech_dispatcher::*;
@ -24,3 +27,6 @@ pub use self::web::*;
#[cfg(target_os = "macos")]
pub use self::appkit::*;
#[cfg(target_os = "macos")]
pub use self::av_foundation::*;

View File

@ -8,8 +8,14 @@
* * WebAssembly
*/
use std::boxed::Box;
use std::{boxed::Box, ffi::CStr};
#[cfg(target_os = "macos")]
use cocoa_foundation::base::id;
#[cfg(target_os = "macos")]
use libc::c_char;
#[cfg(target_os = "macos")]
use objc::{class, msg_send, sel, sel_impl};
use thiserror::Error;
mod backends;
@ -25,6 +31,8 @@ pub enum Backends {
WinRT,
#[cfg(target_os = "macos")]
AppKit,
#[cfg(target_os = "macos")]
AvFoundation,
}
pub struct Features {
@ -110,6 +118,8 @@ impl TTS {
}
#[cfg(target_os = "macos")]
Backends::AppKit => Ok(TTS(Box::new(backends::AppKit::new()))),
#[cfg(target_os = "macos")]
Backends::AvFoundation => Ok(TTS(Box::new(backends::AvFoundation::new()))),
}
}
@ -125,7 +135,23 @@ impl TTS {
#[cfg(target_arch = "wasm32")]
let tts = TTS::new(Backends::Web);
#[cfg(target_os = "macos")]
let tts = TTS::new(Backends::AppKit);
let tts = unsafe {
// Needed because the Rust NSProcessInfo structs report bogus values, and I don't want to pull in a full bindgen stack.
let pi: id = msg_send![class!(NSProcessInfo), new];
let version: id = msg_send![pi, operatingSystemVersionString];
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 = version[1];
let version_parts: Vec<&str> = version.split(".").collect();
let minor_version: i8 = version_parts[1].parse().unwrap();
if minor_version >= 14 {
TTS::new(Backends::AvFoundation)
} else {
TTS::new(Backends::AppKit)
}
};
tts
}