Add voices support to web platform.

This commit is contained in:
Nolan Darilek 2022-03-31 10:39:39 -05:00
parent 3f9e7c22db
commit e699f7e5e5
6 changed files with 144 additions and 20 deletions

View File

@ -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"

View File

@ -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 = "../.." }

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>Example</title>

View File

@ -15,13 +15,20 @@ enum Msg {
RateChanged(String),
PitchChanged(String),
VolumeChanged(String),
VoiceChanged(String),
Speak,
}
fn init(_: Url, _: &mut impl Orders<Msg>) -> 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<Msg>) {
let volume = volume.parse::<f32>().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<Msg>) {
}
fn view(model: &Model) -> Node<Msg> {
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<Msg> {
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<Msg> {
}
fn main() {
console_log::init().expect("Error initializing logger");
App::start("app", init, update, view);
}

View File

@ -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<SpeechSynthesisVoice>,
}
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<String, Error> {
unimplemented!()
fn voice(&self) -> Result<Option<Voice>, 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<String> {
unimplemented!()
fn voices(&self) -> Result<Vec<Voice>, Error> {
if let Some(window) = web_sys::window() {
let speech_synthesis = window.speech_synthesis().unwrap();
let mut rv: Vec<Voice> = 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<SpeechSynthesisVoice> 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,
}
}
}

View File

@ -237,7 +237,7 @@ pub trait Backend: Clone {
fn set_volume(&mut self, volume: f32) -> Result<(), Error>;
fn is_speaking(&self) -> Result<bool, Error>;
fn voices(&self) -> Result<Vec<Voice>, Error>;
fn voice(&self) -> Result<Voice, Error>;
fn voice(&self) -> Result<Option<Voice>, 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<Voice, Error> {
pub fn voice(&self) -> Result<Option<Voice>, 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()
}
}