import { AvatarAnimation } from './avatar-animation.js';
import { WorldManager } from '../core/world-manager.js';
import { Avatar } from './avatar.js';
/**
* Additional processing of Bot events: voice synthesis and animations.
*/
export class BotController {
/** Synthesis can be iritating, enabled by default */
static speechSynthesisEnabled = true;
/**
* @param {Avatar} avatar
*/
constructor(avatar) {
/** Timestamp of last change */
this.lastChange = Date.now();
/** After not receiving any events for this many millis, idle animation starts */
this.idleTimeout = 200;
this.lastAnimation = null;
this.worldManager = WorldManager.instance;
this.world = this.worldManager.world;
this.scene = this.worldManager.scene;
this.avatar = avatar;
this.vrObject = avatar.VRObject;
this.animation = new AvatarAnimation(avatar);
this.animation.improvise = false;
this.setupIdleTimer();
this.vrObject.addListener((obj, changes) => this.processChanges(obj, changes));
this.animationEnd = (animation) => this.animationEnded(animation);
this.voice = null;
const voices = window.speechSynthesis.getVoices();
if (voices.length == 0) {
// chrome fires event
window.speechSynthesis.onvoiceschanged = (changed) => {
this.processVoices(window.speechSynthesis.getVoices());
}
} else {
// mozilla returns it right away
this.processVoices(voices);
}
}
processVoices(voices) {
console.log("Searching voices for " + this.vrObject.lang + " " + this.vrObject.gender, voices);
this.voice = voices[0];
let langMatch = this.vrObject.lang == null; // null means any matches
let genderMatch = this.vrObject.gender == null; // null means any matches
voices.forEach((voice, index) => {
if (this.vrObject.lang) {
// android uses underscore, other devices dash
let androidLang = this.vrObject.lang.replace("-", "_");
let webLang = this.vrObject.lang.replace("_", "-");
if (voice.lang.startsWith(androidLang) || voice.lang.startsWith(webLang)) {
langMatch = true;
// first voice matching the language is taken by default
if (!this.voice) {
this.voice = voice;
}
}
}
if (this.vrObject.gender) {
let gender = this.vrObject.gender.toLowerCase();
if (gender === "female" && voice.name.indexOf("Zira") >= 0 || voice.name.indexOf("Female") >= 0) {
genderMatch = true;
} else if (gender === "male" && voice.name.indexOf("David") >= 0 || voice.name.indexOf("Male") >= 0) {
genderMatch = true;
}
}
if (langMatch && genderMatch) {
this.voice = voice;
langMatch = false;
genderMatch = false;
}
});
if (!this.voice && voices.length > 0) {
this.voice = voices[0];
}
console.log("Voice selected for " + this.avatar.name, this.voice);
}
/**
* Create timer for idle animation, if it doesn't exist.
* CHECKME copied from AvatarController
*/
setupIdleTimer() {
if (this.idleTimerId) {
return;
}
this.idleTimerId = setInterval(() => {
if (this.worldManager.isOnline() && Date.now() - this.lastChange > this.idleTimeout) {
clearInterval(this.idleTimerId);
this.idleTimerId = null;
this.startAnimation(this.animation.idle().group, true);
}
}, this.idleTimeout);
}
startAnimation(animation, loop = false) {
if (animation && this.animation.contains(animation.name) && animation.name != this.lastAnimation) {
this.avatar.startAnimation(animation.name, loop);
this.lastAnimation = animation.name;
}
}
animationEnded(animation) {
//console.log("Animation ended", animation);
this.setupIdleTimer();
animation.onAnimationGroupEndObservable.remove(this.animationEnd);
}
processChanges(obj, changes) {
//console.log("processing changes for "+this.avatar.name,obj,changes);
if (changes['wrote']) {
//console.log("processing speech "+this.avatar.name);
let text = changes.wrote.text;
if (this.voice && BotController.speechSynthesisEnabled) {
if (window.speechSynthesis.pending || window.speechSynthesis.speaking) {
console.log("Interrupting speech");
window.speechSynthesis.cancel();
}
const utter = new SpeechSynthesisUtterance(text);
console.log("Speaking " + this.avatar.name, this.voice);
utter.voice = this.voice;
window.speechSynthesis.speak(utter);
}
let animation = this.animation.processText(text);
if (animation) {
animation.onAnimationGroupEndObservable.add(this.animationEnd);
this.startAnimation(animation);
}
}
if (changes['animation']) {
let animation = changes['animation'].name;
// animation is already playing
let group = this.avatar.getAnimation(animation);
if (group) {
this.lastAnimation = group.name;
group.onAnimationGroupEndObservable.add(this.animationEnd);
} else {
console.log(this.avatar.name + " tried to play non existing animation " + animation);
}
}
}
}