#![cfg(all(target_arch = "wasm32", feature = "web-speech"))]
use wasm_bindgen::JsValue;
use web_sys::{SpeechSynthesis, SpeechSynthesisUtterance, SpeechSynthesisVoice};
use super::TtsSink;
#[derive(Debug, Clone, Default)]
pub struct WebSpeechTtsOptions {
pub voice_uri: Option<String>,
pub lang: Option<String>,
pub rate: Option<f32>,
pub pitch: Option<f32>,
pub volume: Option<f32>,
}
#[derive(Debug, Clone)]
pub struct VoiceInfo {
pub voice_uri: String,
pub name: String,
pub lang: String,
pub local_service: bool,
pub default: bool,
}
pub struct WebSpeechTts {
synth: SpeechSynthesis,
}
impl WebSpeechTts {
pub fn new() -> Result<Self, JsValue> {
let window =
web_sys::window().ok_or_else(|| JsValue::from_str("no global `window` available"))?;
let synth = window.speech_synthesis().map_err(|e| {
JsValue::from_str(&format!(
"speechSynthesis unavailable: {}",
e.as_string().unwrap_or_default()
))
})?;
Ok(Self { synth })
}
pub fn from_synthesis(synth: SpeechSynthesis) -> Self {
Self { synth }
}
pub fn speak(&self, text: &str, opts: WebSpeechTtsOptions) -> Result<(), JsValue> {
let utter = SpeechSynthesisUtterance::new_with_text(text)?;
if let Some(rate) = opts.rate {
utter.set_rate(rate);
}
if let Some(pitch) = opts.pitch {
utter.set_pitch(pitch);
}
if let Some(volume) = opts.volume {
utter.set_volume(volume);
}
if let Some(lang) = opts.lang.as_deref() {
utter.set_lang(lang);
}
if let Some(voice_uri) = opts.voice_uri.as_deref()
&& let Some(voice) = self.find_voice_by_uri(voice_uri)
{
utter.set_voice(Some(&voice));
}
self.synth.speak(&utter);
Ok(())
}
pub fn cancel(&self) {
self.synth.cancel();
}
pub fn pause(&self) {
self.synth.pause();
}
pub fn resume(&self) {
self.synth.resume();
}
pub fn voices(&self) -> Vec<VoiceInfo> {
let arr = self.synth.get_voices();
let len = arr.length();
let mut out = Vec::with_capacity(len as usize);
for i in 0..len {
let v = arr.get(i);
let voice: SpeechSynthesisVoice = v.unchecked_into();
out.push(VoiceInfo {
voice_uri: voice.voice_uri(),
name: voice.name(),
lang: voice.lang(),
local_service: voice.local_service(),
default: voice.default(),
});
}
out
}
pub fn is_speaking(&self) -> bool {
self.synth.speaking()
}
pub fn is_paused(&self) -> bool {
self.synth.paused()
}
fn find_voice_by_uri(&self, voice_uri: &str) -> Option<SpeechSynthesisVoice> {
let arr = self.synth.get_voices();
let len = arr.length();
for i in 0..len {
let v = arr.get(i);
let voice: SpeechSynthesisVoice = v.unchecked_into();
if voice.voice_uri() == voice_uri {
return Some(voice);
}
}
None
}
}
use wasm_bindgen::JsCast;
impl TtsSink for WebSpeechTts {
type Options = WebSpeechTtsOptions;
type Error = JsValue;
fn speak(&self, text: &str, opts: Self::Options) -> Result<(), Self::Error> {
WebSpeechTts::speak(self, text, opts)
}
fn cancel(&self) {
WebSpeechTts::cancel(self);
}
fn pause(&self) {
WebSpeechTts::pause(self);
}
fn resume(&self) {
WebSpeechTts::resume(self);
}
}