1import { SyntheticPlatformEmitter, CodedError } from 'expo-modules-core'; 2 3import { SpeechOptions, WebVoice, VoiceQuality } from './Speech.types'; 4 5//https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance/text 6const MAX_SPEECH_INPUT_LENGTH = 32767; 7 8async function getVoices(): Promise<SpeechSynthesisVoice[]> { 9 return new Promise<SpeechSynthesisVoice[]>((resolve) => { 10 const voices = window.speechSynthesis.getVoices(); 11 12 if (voices.length > 0) { 13 resolve(voices); 14 return; 15 } 16 17 // when a page loads it takes some amount of time to populate the voices list 18 // see https://stackoverflow.com/a/52005323/4337317 19 window.speechSynthesis.onvoiceschanged = function () { 20 const voices = window.speechSynthesis.getVoices(); 21 resolve(voices); 22 }; 23 }); 24} 25 26export default { 27 get name(): string { 28 return 'ExponentSpeech'; 29 }, 30 async speak(id: string, text: string, options: SpeechOptions): Promise<SpeechSynthesisUtterance> { 31 if (text.length > MAX_SPEECH_INPUT_LENGTH) { 32 throw new CodedError( 33 'ERR_SPEECH_INPUT_LENGTH', 34 'Speech input text is too long! Limit of input length is: ' + MAX_SPEECH_INPUT_LENGTH 35 ); 36 } 37 38 const message = new SpeechSynthesisUtterance(); 39 40 if (typeof options.rate === 'number') { 41 message.rate = options.rate; 42 } 43 if (typeof options.pitch === 'number') { 44 message.pitch = options.pitch; 45 } 46 if (typeof options.language === 'string') { 47 message.lang = options.language; 48 } 49 if (typeof options.volume === 'number') { 50 message.volume = options.volume; 51 } 52 if ('_voiceIndex' in options && options._voiceIndex != null) { 53 const voices = await getVoices(); 54 message.voice = voices[Math.min(voices.length - 1, Math.max(0, options._voiceIndex))]; 55 } 56 if (typeof options.voice === 'string') { 57 const voices = await getVoices(); 58 message.voice = 59 voices[ 60 Math.max( 61 0, 62 voices.findIndex((voice) => voice.voiceURI === options.voice) 63 ) 64 ]; 65 } 66 if (typeof options.onResume === 'function') { 67 message.onresume = options.onResume; 68 } 69 if (typeof options.onMark === 'function') { 70 message.onmark = options.onMark; 71 } 72 if (typeof options.onBoundary === 'function') { 73 message.onboundary = options.onBoundary; 74 } 75 76 message.onstart = (nativeEvent: SpeechSynthesisEvent) => { 77 SyntheticPlatformEmitter.emit('Exponent.speakingStarted', { id, nativeEvent }); 78 }; 79 message.onend = (nativeEvent: SpeechSynthesisEvent) => { 80 SyntheticPlatformEmitter.emit('Exponent.speakingDone', { id, nativeEvent }); 81 }; 82 message.onpause = (nativeEvent: SpeechSynthesisEvent) => { 83 SyntheticPlatformEmitter.emit('Exponent.speakingStopped', { id, nativeEvent }); 84 }; 85 message.onerror = (nativeEvent: SpeechSynthesisErrorEvent) => { 86 SyntheticPlatformEmitter.emit('Exponent.speakingError', { id, nativeEvent }); 87 }; 88 89 message.text = text; 90 91 window.speechSynthesis.speak(message); 92 93 return message; 94 }, 95 async getVoices(): Promise<WebVoice[]> { 96 const voices = await getVoices(); 97 return voices.map((voice) => ({ 98 identifier: voice.voiceURI, 99 quality: VoiceQuality.Default, 100 isDefault: voice.default, 101 language: voice.lang, 102 localService: voice.localService, 103 name: voice.name, 104 voiceURI: voice.voiceURI, 105 })); 106 }, 107 async isSpeaking(): Promise<boolean> { 108 return window.speechSynthesis.speaking; 109 }, 110 async stop(): Promise<void> { 111 return window.speechSynthesis.cancel(); 112 }, 113 async pause(): Promise<void> { 114 return window.speechSynthesis.pause(); 115 }, 116 async resume(): Promise<void> { 117 return window.speechSynthesis.resume(); 118 }, 119 maxSpeechInputLength: MAX_SPEECH_INPUT_LENGTH, 120}; 121