use std::cell::RefCell;
use std::sync::Arc;
use std::sync::mpsc::Receiver;
use dispatch2::DispatchQueue;
use objc2::rc::Retained;
use objc2::runtime::{AnyClass, AnyObject, NSObjectProtocol};
use objc2::{MainThreadMarker, sel};
use objc2_app_kit::{
NSAccessibilityAnnouncementKey, NSAccessibilityAnnouncementRequestedNotification,
NSAccessibilityPostNotificationWithUserInfo, NSAccessibilityPriorityKey, NSAccessibilityPriorityLevel,
NSApplication, NSWorkspace,
};
use objc2_avf_audio::{AVSpeechBoundary, AVSpeechSynthesisVoice, AVSpeechSynthesizer, AVSpeechUtterance};
use objc2_foundation::{NSDictionary, NSNumber, NSString};
use super::{Command, Shared, Voice, sort_voices};
const VOICEOVER: &str = "VoiceOver";
const SYSTEM_VOICE: &str = "AVSpeechSynthesizer";
thread_local! {
static OUTPUT: RefCell<Option<Output>> = const { RefCell::new(None) };
}
struct Output {
synth: Retained<AVSpeechSynthesizer>,
voice: Option<Retained<AVSpeechSynthesisVoice>>,
rate: Option<f32>,
volume: Option<f32>,
backend: &'static str,
}
impl Output {
fn new() -> Self {
let synth = unsafe { AVSpeechSynthesizer::new() };
Self { synth, voice: None, rate: None, volume: None, backend: "" }
}
fn handle(&mut self, mtm: MainThreadMarker, cmd: Command, shared: &Shared) {
match cmd {
Command::Speak { text, interrupt } => {
if voiceover_running() {
self.stop();
announce(mtm, &text, interrupt);
self.set_backend(VOICEOVER, shared);
} else {
self.speak(&text, interrupt);
self.set_backend(SYSTEM_VOICE, shared);
}
}
Command::Stop => self.stop(),
Command::Rate(rate) => self.rate = Some(rate),
Command::Volume(volume) => self.volume = Some(volume),
Command::Voice(id) => {
self.voice = id.and_then(|id| unsafe { AVSpeechSynthesisVoice::voiceWithIdentifier(&NSString::from_str(&id)) });
}
Command::Braille(_) => {}
}
}
fn set_backend(&mut self, name: &'static str, shared: &Shared) {
if self.backend != name {
self.backend = name;
shared.set_backend(name.into());
}
}
fn speak(&self, text: &str, interrupt: bool) {
unsafe {
if interrupt {
self.synth.stopSpeakingAtBoundary(AVSpeechBoundary::Immediate);
}
let utterance = AVSpeechUtterance::speechUtteranceWithString(&NSString::from_str(text));
if let Some(voice) = &self.voice {
utterance.setVoice(Some(voice));
}
if let Some(rate) = self.rate {
utterance.setRate(rate);
}
if self.voice.is_none()
&& self.rate.is_none()
&& utterance.respondsToSelector(sel!(setPrefersAssistiveTechnologySettings:))
{
utterance.setPrefersAssistiveTechnologySettings(true);
}
if let Some(volume) = self.volume {
utterance.setVolume(volume);
}
self.synth.speakUtterance(&utterance);
}
}
fn stop(&self) {
unsafe {
self.synth.stopSpeakingAtBoundary(AVSpeechBoundary::Immediate);
}
}
}
fn voiceover_running() -> bool {
NSWorkspace::sharedWorkspace().isVoiceOverEnabled()
}
fn announce(mtm: MainThreadMarker, text: &str, interrupt: bool) {
let priority = if interrupt { NSAccessibilityPriorityLevel::High } else { NSAccessibilityPriorityLevel::Medium };
let text = NSString::from_str(text);
let priority = NSNumber::new_isize(priority.0);
let (announcement_key, priority_key, notification) =
unsafe { (NSAccessibilityAnnouncementKey, NSAccessibilityPriorityKey, NSAccessibilityAnnouncementRequestedNotification) };
let objects: [&AnyObject; 2] = [text.as_ref(), priority.as_ref()];
let info = NSDictionary::from_slices(&[announcement_key, priority_key], &objects);
let app = NSApplication::sharedApplication(mtm);
let window = app.keyWindow();
let element: &AnyObject = match &window {
Some(window) => window.as_ref(),
None => app.as_ref(),
};
unsafe { NSAccessibilityPostNotificationWithUserInfo(element, notification, Some(&info)) };
}
fn system_voices() -> Vec<Voice> {
let list = unsafe { AVSpeechSynthesisVoice::speechVoices() };
let mut voices: Vec<Voice> = list
.to_vec()
.into_iter()
.map(|v| unsafe {
Voice { id: v.identifier().to_string(), name: v.name().to_string(), language: Some(v.language().to_string()) }
})
.collect();
sort_voices(&mut voices);
voices
}
fn on_main(cmd: Command, shared: &Arc<Shared>) {
let shared = shared.clone();
DispatchQueue::main().exec_async(move || {
let Some(mtm) = MainThreadMarker::new() else { return };
OUTPUT.with_borrow_mut(|output| output.get_or_insert_with(Output::new).handle(mtm, cmd, &shared));
});
}
pub(super) fn run(rx: &Receiver<Command>, shared: &Arc<Shared>) -> Result<(), String> {
if AnyClass::get(c"AVSpeechSynthesizer").is_none() {
return Err("AVSpeechSynthesizer is not available (needs macOS 10.14 or newer)".into());
}
shared.set_voices(system_voices());
on_main(Command::Stop, shared);
for cmd in rx {
on_main(cmd, shared);
}
on_main(Command::Stop, shared);
Ok(())
}