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