From e699f7e5e56e33943211727bb0559320091fe031 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Thu, 31 Mar 2022 10:39:39 -0500 Subject: [PATCH] Add voices support to web platform. --- Cargo.toml | 2 +- examples/web/Cargo.toml | 2 ++ examples/web/index.html | 2 +- examples/web/src/main.rs | 50 ++++++++++++++++++++++++-- src/backends/web.rs | 76 +++++++++++++++++++++++++++++++++++----- src/lib.rs | 32 +++++++++++++---- 6 files changed, 144 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 68c4a1d..eccc258 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ 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", ] } +web-sys = { version = "0.3", features = ["EventTarget", "SpeechSynthesis", "SpeechSynthesisErrorCode", "SpeechSynthesisErrorEvent", "SpeechSynthesisEvent", "SpeechSynthesisUtterance", "SpeechSynthesisVoice", "Window", ] } [target.'cfg(target_os="android")'.dependencies] jni = "0.19" diff --git a/examples/web/Cargo.toml b/examples/web/Cargo.toml index e93d316..745c74e 100644 --- a/examples/web/Cargo.toml +++ b/examples/web/Cargo.toml @@ -7,5 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +console_log = "0.2" +log = "0.4" seed = "0.8" tts = { path = "../.." } \ No newline at end of file diff --git a/examples/web/index.html b/examples/web/index.html index 15e486f..1ec233a 100644 --- a/examples/web/index.html +++ b/examples/web/index.html @@ -1,5 +1,5 @@ - + Example diff --git a/examples/web/src/main.rs b/examples/web/src/main.rs index fb03c38..a2c258e 100644 --- a/examples/web/src/main.rs +++ b/examples/web/src/main.rs @@ -15,13 +15,20 @@ enum Msg { RateChanged(String), PitchChanged(String), VolumeChanged(String), + VoiceChanged(String), Speak, } fn init(_: Url, _: &mut impl Orders) -> Model { - let tts = Tts::default().unwrap(); + let mut tts = Tts::default().unwrap(); + if tts.voices().unwrap().iter().len() > 0 { + if tts.voice().unwrap().is_none() { + tts.set_voice(tts.voices().unwrap().first().unwrap()) + .expect("Failed to set voice"); + } + } Model { - text: Default::default(), + text: "Hello, world. This is a test of the current text-to-speech values.".into(), tts, } } @@ -42,6 +49,13 @@ fn update(msg: Msg, model: &mut Model, _: &mut impl Orders) { let volume = volume.parse::().unwrap(); model.tts.set_volume(volume).unwrap(); } + VoiceChanged(voice) => { + for v in model.tts.voices().unwrap() { + if v.id() == voice { + model.tts.set_voice(&v).unwrap(); + } + } + } Speak => { model.tts.speak(&model.text, false).unwrap(); } @@ -49,6 +63,7 @@ fn update(msg: Msg, model: &mut Model, _: &mut impl Orders) { } fn view(model: &Model) -> Node { + let should_show_voices = model.tts.voices().unwrap().iter().len() > 0; form![ div![label![ "Text to speak", @@ -96,6 +111,36 @@ fn view(model: &Model) -> Node { input_ev(Ev::Input, Msg::VolumeChanged) ], ],], + if should_show_voices { + div![ + label!["Voice"], + select![ + model.tts.voices().unwrap().iter().map(|v| { + let selected = if let Some(voice) = model.tts.voice().unwrap() { + voice.id() == v.id() + } else { + false + }; + option![ + attrs! { + At::Value => v.id() + }, + if selected { + attrs! { + At::Selected => selected + } + } else { + attrs! {} + }, + v.name() + ] + }), + input_ev(Ev::Change, Msg::VoiceChanged) + ] + ] + } else { + div!["Your browser does not seem to support selecting voices."] + }, button![ "Speak", ev(Ev::Click, |e| { @@ -107,5 +152,6 @@ fn view(model: &Model) -> Node { } fn main() { + console_log::init().expect("Error initializing logger"); App::start("app", init, update, view); } diff --git a/src/backends/web.rs b/src/backends/web.rs index 57a8af3..a39dff2 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -1,15 +1,18 @@ #[cfg(target_arch = "wasm32")] -use std::sync::Mutex; +use std::{str::FromStr, sync::Mutex}; use lazy_static::lazy_static; use log::{info, trace}; +use unic_langid::LanguageIdentifier; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use web_sys::{ SpeechSynthesisErrorCode, SpeechSynthesisErrorEvent, SpeechSynthesisEvent, - SpeechSynthesisUtterance, + SpeechSynthesisUtterance, SpeechSynthesisVoice, }; +use crate::Gender; +use crate::Voice; use crate::{Backend, BackendId, Error, Features, UtteranceId, CALLBACKS}; #[derive(Clone, Debug)] @@ -18,6 +21,7 @@ pub struct Web { rate: f32, pitch: f32, volume: f32, + voice: Option, } lazy_static! { @@ -35,6 +39,7 @@ impl Web { rate: 1., pitch: 1., volume: 1., + voice: None, }; *backend_id += 1; Ok(rv) @@ -53,7 +58,8 @@ impl Backend for Web { pitch: true, volume: true, is_speaking: true, - voices: true, + voice: true, + get_voice: true, utterance_callbacks: true, } } @@ -64,6 +70,9 @@ impl Backend for Web { utterance.set_rate(self.rate); utterance.set_pitch(self.pitch); utterance.set_volume(self.volume); + if self.voice.is_some() { + utterance.set_voice(self.voice.as_ref()); + } let id = self.id().unwrap(); let mut uid = NEXT_UTTERANCE_ID.lock().unwrap(); let utterance_id = UtteranceId::Web(*uid); @@ -198,16 +207,53 @@ impl Backend for Web { } } - fn voice(&self) -> Result { - unimplemented!() + fn voice(&self) -> Result, Error> { + if let Some(voice) = &self.voice { + Ok(Some(voice.clone().into())) + } else { + if let Some(window) = web_sys::window() { + let speech_synthesis = window.speech_synthesis().unwrap(); + for voice in speech_synthesis.get_voices().iter() { + let voice: SpeechSynthesisVoice = voice.into(); + if voice.default() { + return Ok(Some(voice.into())); + } + } + } else { + return Err(Error::NoneError); + } + Ok(None) + } } - fn list_voices(&self) -> Vec { - unimplemented!() + fn voices(&self) -> Result, Error> { + if let Some(window) = web_sys::window() { + let speech_synthesis = window.speech_synthesis().unwrap(); + let mut rv: Vec = vec![]; + for v in speech_synthesis.get_voices().iter() { + let v: SpeechSynthesisVoice = v.into(); + rv.push(v.into()); + } + Ok(rv) + } else { + Err(Error::NoneError) + } } - fn set_voice(&mut self, voice: &str) -> Result<(), Error> { - unimplemented!() + fn set_voice(&mut self, voice: &Voice) -> Result<(), Error> { + if let Some(window) = web_sys::window() { + let speech_synthesis = window.speech_synthesis().unwrap(); + for v in speech_synthesis.get_voices().iter() { + let v: SpeechSynthesisVoice = v.into(); + if v.voice_uri() == voice.id { + self.voice = Some(v); + return Ok(()); + } + } + return Err(Error::OperationFailed); + } else { + Err(Error::NoneError) + } } } @@ -217,3 +263,15 @@ impl Drop for Web { mappings.retain(|v| v.0 != self.id); } } + +impl From for Voice { + fn from(other: SpeechSynthesisVoice) -> Self { + let language = LanguageIdentifier::from_str(&other.lang()).unwrap(); + Voice { + id: other.voice_uri(), + name: other.name(), + gender: Gender::Unspecified, + language, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index b3249d4..1285569 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -237,7 +237,7 @@ pub trait Backend: Clone { fn set_volume(&mut self, volume: f32) -> Result<(), Error>; fn is_speaking(&self) -> Result; fn voices(&self) -> Result, Error>; - fn voice(&self) -> Result; + fn voice(&self) -> Result, Error>; fn set_voice(&mut self, voice: &Voice) -> Result<(), Error>; } @@ -582,7 +582,7 @@ impl Tts { /** * Return the current speaking voice. */ - pub fn voice(&self) -> Result { + pub fn voice(&self) -> Result, Error> { let Features { get_voice, .. } = self.supported_features(); if get_voice { self.0.read().unwrap().voice() @@ -702,7 +702,7 @@ impl Drop for Tts { } } -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub enum Gender { Unspecified, Male, @@ -711,8 +711,26 @@ pub enum Gender { #[derive(Debug)] pub struct Voice { - pub id: String, - pub name: String, - pub gender: Gender, - pub language: LanguageIdentifier, + pub(crate) id: String, + pub(crate) name: String, + pub(crate) gender: Gender, + pub(crate) language: LanguageIdentifier, +} + +impl Voice { + pub fn id(&self) -> String { + self.id.clone() + } + + pub fn name(&self) -> String { + self.name.clone() + } + + pub fn gender(&self) -> Gender { + self.gender + } + + pub fn language(&self) -> LanguageIdentifier { + self.language.clone() + } }