import TimedEvent from './timedEvent'; /** * TimedSpeech uses the Web Speech API (SpeechSynthesis) to speak text and * map speech progress to TimedEvent progress. Marks in the utterance are * used to approximate elapsed duration and progress notifications. * * constructor(text, gain, speed) * - text: string to speak (basic splitting into marks is applied) * - gain: volume (0.0 - 1.0) * - speed: speaking rate */ export default class TimedSpeech extends TimedEvent { markCount: number; currentMark: number; gain: number; utterance: SpeechSynthesisUtterance; constructor(text: string = '', gain: number = 1.0, speed: number = 1) { super(0); const words = text.split(/\s+(?![^<]*>)/); this.markCount = words.length; this.currentMark = 0; const ssml = words.join(' '); this.utterance = new SpeechSynthesisUtterance(ssml); this.gain = gain; this.utterance.volume = gain; this.utterance.rate = speed; this.utterance.addEventListener('mark', (e: any) => { if (e.name != '#progress#') return; this.forceTick(); this.currentMark++; if (this.markCount) { this.duration = this.elapsedTime * this.markCount / this.currentMark; } else { this.duration = 1; } this.dispatchEvent(this.makeEvent('PROGRESS')); }); this.utterance.addEventListener('end', (e) => { this.dispatchEvent(this.makeEvent('COMPLETE')); this.cancelNextTick(); }); } start() { if (this.started) return; window.speechSynthesis.speak(this.utterance); super.start(); } pause() { if (this.paused) return; window.speechSynthesis.pause(); super.pause(); } resume() { if (!this.paused) return; window.speechSynthesis.resume(); super.resume(); } cancel() { window.speechSynthesis.cancel(); super.cancel(); } setVolume(gain: number = 1.0) { this.gain = gain; // SpeechSynthesisUtterance.volume is read when spoken; no element to set this.utterance.volume = gain; } protected tick() { if (!this.started || this.paused || this.cancelled) return; if (this.duration === -1) return; const now = performance.now(); this.elapsedTime += now - this.lastTick; this.lastTick = now; if (typeof window !== 'undefined' && typeof window.requestAnimationFrame !== 'undefined') { this.nextTick = requestAnimationFrame(() => { this.tick(); }); } else { this.nextTick = setTimeout(() => { this.tick(); }, 1000 / 60); } } }