UNPKG

expo-speech

Version:

Provides text-to-speech functionality.

127 lines (112 loc) 4.24 kB
import { CodedError, NativeModule, registerWebModule } from 'expo-modules-core'; import { SpeechOptions, WebVoice, VoiceQuality } from './Speech.types'; //https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance/text const MAX_SPEECH_INPUT_LENGTH = 32767; async function getVoices(): Promise<SpeechSynthesisVoice[]> { return new Promise<SpeechSynthesisVoice[]>((resolve) => { const voices = window.speechSynthesis.getVoices(); if (voices.length > 0) { resolve(voices); return; } // when a page loads it takes some amount of time to populate the voices list // see https://stackoverflow.com/a/52005323/4337317 window.speechSynthesis.onvoiceschanged = function () { const voices = window.speechSynthesis.getVoices(); resolve(voices); }; }); } type ExpoSpeechEvents = { 'Exponent.speakingStarted': (params: { id: string; nativeEvent: SpeechSynthesisEvent }) => void; 'Exponent.speakingDone': (params: { id: string; nativeEvent: SpeechSynthesisEvent }) => void; 'Exponent.speakingStopped': (params: { id: string; nativeEvent: SpeechSynthesisEvent }) => void; 'Exponent.speakingError': (params: { id: string; nativeEvent: SpeechSynthesisEvent }) => void; }; class ExpoSpeech extends NativeModule<ExpoSpeechEvents> { async speak(id: string, text: string, options: SpeechOptions): Promise<SpeechSynthesisUtterance> { if (text.length > MAX_SPEECH_INPUT_LENGTH) { throw new CodedError( 'ERR_SPEECH_INPUT_LENGTH', 'Speech input text is too long! Limit of input length is: ' + MAX_SPEECH_INPUT_LENGTH ); } const message = new SpeechSynthesisUtterance(); if (typeof options.rate === 'number') { message.rate = options.rate; } if (typeof options.pitch === 'number') { message.pitch = options.pitch; } if (typeof options.language === 'string') { message.lang = options.language; } if (typeof options.volume === 'number') { message.volume = options.volume; } if ('_voiceIndex' in options && options._voiceIndex != null) { const voices = await getVoices(); message.voice = voices[Math.min(voices.length - 1, Math.max(0, options._voiceIndex))]; } if (typeof options.voice === 'string') { const voices = await getVoices(); message.voice = voices[ Math.max( 0, voices.findIndex((voice) => voice.voiceURI === options.voice) ) ]; } if (typeof options.onResume === 'function') { message.onresume = options.onResume; } if (typeof options.onMark === 'function') { message.onmark = options.onMark; } if (typeof options.onBoundary === 'function') { message.onboundary = options.onBoundary; } message.onstart = (nativeEvent: SpeechSynthesisEvent) => { this.emit('Exponent.speakingStarted', { id, nativeEvent }); }; message.onend = (nativeEvent: SpeechSynthesisEvent) => { this.emit('Exponent.speakingDone', { id, nativeEvent }); }; message.onpause = (nativeEvent: SpeechSynthesisEvent) => { this.emit('Exponent.speakingStopped', { id, nativeEvent }); }; message.onerror = (nativeEvent: SpeechSynthesisErrorEvent) => { this.emit('Exponent.speakingError', { id, nativeEvent }); }; message.text = text; window.speechSynthesis.speak(message); return message; } async getVoices(): Promise<WebVoice[]> { const voices = await getVoices(); return voices.map((voice) => ({ identifier: voice.voiceURI, quality: VoiceQuality.Default, isDefault: voice.default, language: voice.lang, localService: voice.localService, name: voice.name, voiceURI: voice.voiceURI, })); } async isSpeaking(): Promise<boolean> { return window.speechSynthesis.speaking; } async stop(): Promise<void> { return window.speechSynthesis.cancel(); } async pause(): Promise<void> { return window.speechSynthesis.pause(); } async resume(): Promise<void> { return window.speechSynthesis.resume(); } maxSpeechInputLength = MAX_SPEECH_INPUT_LENGTH; } export default registerWebModule(ExpoSpeech, 'ExpoSpeech');