This repository has been archived on 2021-09-15. You can view files and clone it, but cannot push or open issues or pull requests.
ModuleWeb/SRC/public/js/responsivevoice.js

492 lines
12 KiB
JavaScript
Executable File

var ResponsiveVoice = function(){
var self = this;
// Ourn own collection of voices
var responsivevoices = [
{name: 'UK English Female', voiceIDs: [3,5,1,6,7,8] },
{name: 'UK English Male', voiceIDs: [0,4,2,6,7,8] },
{name: 'US English Female', voiceIDs: [39,40,41,42,43,44] },
{name: 'Spanish Female', voiceIDs: [19,16,17,18,20,15] },
{name: 'French Female', voiceIDs: [21,22,23,26] },
{name: 'Deutsch Female', voiceIDs: [27,28,29,30,31,32] },
{name: 'Italian Female', voiceIDs: [33,34,35,36,37,38] },
{name: 'Hungarian Female', voiceIDs: [9,10,11] },
{name: 'Serbian Male', voiceIDs: [12] },
{name: 'Croatian Male', voiceIDs: [13] },
{name: 'Bosnian Male', voiceIDs: [14] },
{name: 'Fallback UK Female', voiceIDs: [8] }
];
//All voices available on every system and device
var voicecollection = [
{name: 'Google UK English Male'}, //0 male uk android/chrome
{name: 'Agnes'}, //1 female us safari mac
{name: 'Daniel Compact'}, //2 male us safari mac
{name: 'Google UK English Female'}, //3 female uk android/chrome
{name: 'en-GB', rate: 0.5, pitch: 1}, //4 male uk IOS
{name: 'en-AU', rate: 0.5, pitch: 1}, //5 female english IOS
{name: 'inglés Reino Unido'}, //6 spanish english android
{name: 'English United Kingdom'}, //7 english english android
{name: 'Fallback en-GB Female', lang: 'en-GB', fallbackvoice: true}, //8 fallback english female
{name: 'Eszter Compact'}, //9 Hungarian mac
{name: 'hu-HU', rate: 0.4}, //10 Hungarian iOS
{name: 'Fallback Hungarian', lang: 'hu', fallbackvoice:true}, //11 Hungarian fallback
{name: 'Fallback Serbian', lang: 'sr', fallbackvoice:true}, //12 Serbian fallback
{name: 'Fallback Croatian', lang: 'hr', fallbackvoice:true}, //13 Croatian fallback
{name: 'Fallback Bosnian', lang: 'bs', fallbackvoice:true}, //14 Bosnian fallback
{name: 'Fallback Spanish', lang: 'es', fallbackvoice:true}, //15 Spanish fallback
{name: 'Spanish Spain'}, //16 female es android/chrome
{name: 'español España'}, //17 female es android/chrome
{name: 'Diego Compact', rate: 0.3}, //18 male es mac
{name: 'Google Español'}, //19 male es chrome
{name: 'es-ES', rate: 0.3}, //20 male es iOS
{name: 'Google Français'}, //21 FR chrome
{name: 'French France'}, //22 android/chrome
{name: 'francés Francia'}, //23 android/chrome
{name: 'Virginie Compact', rate: 0.5}, //24 mac
{name: 'fr-FR', rate: 0.5}, //25 iOS
{name: 'Fallback French', lang: 'fr', fallbackvoice:true}, //26 fallback
{name: 'Google Deutsch'}, //27 DE chrome
{name: 'German Germany'}, //28 android/chrome
{name: 'alemán Alemania'}, //29 android/chrome
{name: 'Yannick Compact', rate: 0.5}, //30 mac
{name: 'de-DE', rate: 0.5}, //31 iOS
{name: 'Fallback Deutsch', lang: 'de', fallbackvoice:true}, //32 fallback
{name: 'Google Italiano'}, //33 DE chrome
{name: 'Italian Italy'}, //34 android/chrome
{name: 'italiano Italia'}, //35 android/chrome
{name: 'Paolo Compact', rate: 0.5}, //36 mac
{name: 'it-IT', rate: 0.5}, //37 iOS
{name: 'Fallback Italian', lang: 'it', fallbackvoice:true}, //38 fallback
{name: 'Google US English'}, //39 DE chrome
{name: 'English United States'}, //40 android/chrome
{name: 'inglés Estados Unidos'}, //41 android/chrome
{name: 'Vicki'}, //42 mac
{name: 'en-US', rate: 0.5}, //43 iOS
{name: 'Fallback English', lang: 'en-US', fallbackvoice:true}, //44 fallback
];
var systemvoices;
var CHARACTER_LIMIT = 100;
var VOICESUPPORT_ATTEMPTLIMIT = 5;
var voicesupport_attempts = 0;
var fallbackMode = false;
this.fallback_playing = false;
this.fallback_parts = null;
this.fallback_part_index = 0;
this.fallback_audio = null;
//Wait until system voices are ready and trigger the event OnVoiceReady
if (typeof speechSynthesis != 'undefined') {
speechSynthesis.onvoiceschanged = function() {
systemvoices = window.speechSynthesis.getVoices();
if (self.OnVoiceReady!=null) {
self.OnVoiceReady.call();
}
};
}
this.default_rv = responsivevoices[0];
this.OnVoiceReady = null;
//We should use jQuery if it's available
if (typeof $ === 'undefined') {
document.addEventListener('DOMContentLoaded',function(){
init();
});
}else{
$(document).ready(function() {
init();
});
}
function init() {
if (typeof speechSynthesis === 'undefined') {
enableFallbackMode();
} else {
//Waiting a few ms before calling getVoices() fixes some issues with safari on IOS as well as Chrome
setTimeout(function(){
var gsvinterval = setInterval(function() {
var v = window.speechSynthesis.getVoices();
if (v.length==0 && (systemvoices==null || systemvoices.length==0)) {
console.log('Voice support NOT ready');
voicesupport_attempts++;
if (voicesupport_attempts > VOICESUPPORT_ATTEMPTLIMIT) {
//We don't support voices. Using fallback
clearInterval(gsvinterval);
enableFallbackMode();
}
}else{
console.log('Voice support ready');
clearInterval(gsvinterval);
systemvoices = v;
mapRVs();
if (self.OnVoiceReady!=null)
self.OnVoiceReady.call();
}
},100);
},100);
}
}
function enableFallbackMode() {
fallbackMode = true;
console.log('Voice not supported. Using fallback mode');
mapRVs();
if (self.OnVoiceReady!=null)
self.OnVoiceReady.call();
}
this.getVoices = function() {
//Create voices array
var v = [];
for (var i=0; i<responsivevoices.length; i++) {
v.push( { name: responsivevoices[i].name });
}
return v;
}
this.speak = function(text, voicename) {
//Support for multipart text (there is a limit on characters)
var multipartText = [];
if (text.length>CHARACTER_LIMIT) {
var tmptxt = text;
while(tmptxt.length>CHARACTER_LIMIT) {
//Split by common phrase delimiters
var p = tmptxt.search(/[:!?.;]+/);
var part = '';
//Coludn't split by priority characters, try commas
if (p==-1 || p>=CHARACTER_LIMIT ) {
p = tmptxt.search(/[,]+/);
}
//Couldn't split by normal characters, then we use spaces
if (p==-1 || p>=CHARACTER_LIMIT) {
var words = tmptxt.split(' ');
for (var i=0; i<words.length; i++) {
if (part.length + words[i].length +1 >CHARACTER_LIMIT)
break;
part += (i!=0?' ':'') + words[i];
}
} else {
part = tmptxt.substr(0, p+1);
}
tmptxt = tmptxt.substr(part.length, tmptxt.length-part.length);
multipartText.push(part);
//console.log(part.length + " - " + part);
}
//Add the remaining text
if (tmptxt.length>0) {
multipartText.push(tmptxt);
}
}else{
//Small text
multipartText.push(text);
}
//Find system voice that matches voice name
var rv;
if (voicename==null) {
rv = self.default_rv;
}else{
rv = getResponsiveVoice(voicename);
}
var profile = {};
//Map was done so no need to look for the mapped voice
if (rv.mappedProfile!=null) {
profile = rv.mappedProfile;
}else{
profile.systemvoice = getMatchedVoice(rv);
profile.collectionvoice = {};
if (profile.systemvoice==null) {
console.log('ERROR: No voice found for: ' + voicename);
return;
}
}
if (profile.collectionvoice.fallbackvoice==true) {
fallbackMode = true;
self.fallback_parts = [];
}else{
fallbackMode = false;
}
//Play multipart text
for (var i=0; i<multipartText.length; i++) {
if (!fallbackMode) {
//Use SpeechSynthesis
//Create msg object
var msg = new SpeechSynthesisUtterance();
msg.voice = profile.systemvoice;
msg.voiceURI = profile.systemvoice.voiceURI;
msg.volume = profile.collectionvoice.volume || profile.systemvoice.volume || 1; // 0 to 1
msg.rate = profile.collectionvoice.rate || profile.systemvoice.rate || 1; // 0.1 to 10
msg.pitch = profile.collectionvoice.pitch || profile.systemvoice.pitch || 1; //0 to 2*/
msg.text = multipartText[i];
msg.lang = profile.collectionvoice.lang || profile.systemvoice.lang;
msg.onend = self.OnFinishedPlaying;
msg.onerror = function(e){
console.log('Error');
console.log(e);
};
//console.log(msg);
speechSynthesis.speak(msg);
}else{
var url = 'http://www.corsproxy.com/translate.google.com/translate_tts?ie=UTF-8&q=' + multipartText[i] + '&tl=' + profile.collectionvoice.lang || profile.systemvoice.lang || 'en-US';
var audio = new Audio(url);
audio.playbackRate = 1;
audio.preload = 'auto';
audio.volume = profile.collectionvoice.volume || profile.systemvoice.volume || 1; // 0 to 1;
self.fallback_parts.push(audio);
}
}
if (fallbackMode)
self.fallback_startPlaying();
}
this.fallback_startPlaying = function() {
//console.log('start playing');
self.fallback_part_index = 0;
//console.log(self.fallback_parts);
self.fallback_finishedplaying();
}
this.fallback_finishedplaying = function(e) {
//console.log('chunk ended');
self.fallback_audio = self.fallback_parts[self.fallback_part_index];
//console.log(self.fallback_audio);
//self.fallback_audio.addEventListener('error', function(e){ console.log('error'); console.log(e)});
//self.fallback_audio.addEventListener('progress', function(e){ console.log('progress'); this.play();});
//self.fallback_audio.addEventListener('loadstart', function(e){ console.log('loadstart'); console.log(e)});
//self.fallback_audio.load();
self.fallback_audio.play();
//audio.addEventListener('play', utterance.onstart);
self.fallback_part_index ++;
if (self.fallback_part_index < self.fallback_parts.length) {
self.fallback_audio.addEventListener('ended', self.fallback_finishedplaying);
}
}
this.cancel = function() {
if (fallbackMode)
self.fallback_audio.pause();
else
speechSynthesis.cancel();
}
this.voiceSupport = function() {
return ('speechSynthesis' in window);
}
this.OnFinishedPlaying = function(event) {
}
//Set default voice to use when no voice name is supplied to speak()
this.setDefaultVoice = function(voicename) {
var vr = getResponsiveVoice(voicename);
if (vr!=null) {
self.default_vr = vr;
}
}
//Map responsivevoices to system voices
function mapRVs() {
for (var i=0; i<responsivevoices.length; i++) {
var rv = responsivevoices[i];
for (var j=0; j<rv.voiceIDs.length; j++) {
var vcoll = voicecollection[rv.voiceIDs[j]];
if (vcoll.fallbackvoice != true) { // vcoll.fallbackvoice would be null instead of false
// Look on system voices
var v = getSystemVoice(vcoll.name);
if (v!=null) {
rv.mappedProfile = {
systemvoice: v,
collectionvoice: vcoll
};
console.log("Mapped " + rv.name + " to " + v.name);
break;
}
}else {
//Pick the fallback voice
rv.mappedProfile = {
systemvoice: {},
collectionvoice: vcoll
};
console.log("Mapped " + rv.name + " to " + vcoll.lang + " fallback voice");
break;
}
}
}
}
//Look for the voice in the system that matches the one in our collection
function getMatchedVoice(rv) {
for (var i=0; i<rv.voiceIDs.length; i++) {
var v = getSystemVoice(voicecollection[rv.voiceIDs[i]].name);
if (v!=null)
return v;
}
return null;
}
function getSystemVoice(name) {
if (typeof systemvoices === 'undefined') return null;
for (var i=0; i<systemvoices.length; i++) {
if (systemvoices[i].name == name)
return systemvoices[i];
}
return null;
}
function getResponsiveVoice(name) {
for (var i=0; i<responsivevoices.length; i++) {
if (responsivevoices[i].name == name) {
return responsivevoices[i];
}
}
return null;
}
}
var responsiveVoice = new ResponsiveVoice();