1import * as Speech from 'expo-speech'; 2import * as React from 'react'; 3import { 4 Button, 5 Picker, 6 Platform, 7 ScrollView, 8 StyleSheet, 9 Text, 10 TouchableOpacity, 11 View, 12} from 'react-native'; 13 14import HeadingText from '../components/HeadingText'; 15import { Colors } from '../constants'; 16 17const EXAMPLES = [ 18 { language: 'en', text: 'Hello world' }, 19 { language: 'es', text: 'Hola mundo' }, 20 { language: 'en', text: 'Charlie Cheever chased a chortling choosy child' }, 21 { language: 'en', text: 'Adam Perry ate a pear in pairs in Paris' }, 22]; 23 24const AmountControlButton: React.FunctionComponent<React.ComponentProps<typeof TouchableOpacity> & { 25 title: string; 26}> = props => ( 27 <TouchableOpacity 28 disabled={props.disabled} 29 onPress={props.onPress} 30 hitSlop={{ top: 10, bottom: 10, left: 20, right: 20 }}> 31 <Text 32 style={{ 33 color: props.disabled ? '#ccc' : Colors.tintColor, 34 fontWeight: 'bold', 35 paddingHorizontal: 5, 36 fontSize: 18, 37 }}> 38 {props.title} 39 </Text> 40 </TouchableOpacity> 41); 42 43interface State { 44 selectedExample: { language: string; text: string }; 45 inProgress: boolean; 46 paused: boolean; 47 pitch: number; 48 rate: number; 49 voiceList?: { name: string; identifier: string }[]; 50 voice?: string; 51} 52 53// See: https://github.com/expo/expo/pull/10229#discussion_r490961694 54// eslint-disable-next-line @typescript-eslint/ban-types 55export default class TextToSpeechScreen extends React.Component<{}, State> { 56 static navigationOptions = { 57 title: 'Speech', 58 }; 59 60 readonly state: State = { 61 selectedExample: EXAMPLES[0], 62 inProgress: false, 63 paused: false, 64 pitch: 1, 65 rate: 0.75, 66 }; 67 68 async componentDidMount() { 69 if (Platform.OS === 'ios') { 70 await this._loadAllVoices(); 71 } 72 } 73 74 render() { 75 return ( 76 <ScrollView style={styles.container}> 77 <HeadingText>Select a phrase</HeadingText> 78 79 <View style={styles.examplesContainer}>{EXAMPLES.map(this._renderExample)}</View> 80 81 <View style={styles.separator} /> 82 83 <View style={styles.controlRow}> 84 <Button disabled={this.state.inProgress} onPress={this._speak} title="Speak" /> 85 86 <Button disabled={!this.state.inProgress} onPress={this._stop} title="Stop" /> 87 </View> 88 89 {Platform.OS === 'ios' && ( 90 <View style={styles.controlRow}> 91 <Button 92 disabled={!this.state.inProgress || this.state.paused} 93 onPress={this._pause} 94 title="Pause" 95 /> 96 <Button disabled={!this.state.paused} onPress={this._resume} title="Resume" /> 97 </View> 98 )} 99 100 {Platform.OS === 'ios' && this.state.voiceList && ( 101 <View> 102 <Picker 103 selectedValue={this.state.voice} 104 onValueChange={voice => this.setState({ voice })}> 105 {this.state.voiceList.map(voice => ( 106 <Picker.Item key={voice.identifier} label={voice.name} value={voice.identifier} /> 107 ))} 108 </Picker> 109 </View> 110 )} 111 112 <Text style={styles.controlText}>Pitch: {this.state.pitch.toFixed(2)}</Text> 113 <View style={styles.controlRow}> 114 <AmountControlButton 115 onPress={this._increasePitch} 116 title="Increase" 117 disabled={this.state.inProgress} 118 /> 119 120 <Text>/</Text> 121 122 <AmountControlButton 123 onPress={this._decreasePitch} 124 title="Decrease" 125 disabled={this.state.inProgress} 126 /> 127 </View> 128 129 <Text style={styles.controlText}>Rate: {this.state.rate.toFixed(2)}</Text> 130 <View style={styles.controlRow}> 131 <AmountControlButton 132 onPress={this._increaseRate} 133 title="Increase" 134 disabled={this.state.inProgress} 135 /> 136 137 <Text>/</Text> 138 <AmountControlButton 139 onPress={this._decreaseRate} 140 title="Decrease" 141 disabled={this.state.inProgress} 142 /> 143 </View> 144 </ScrollView> 145 ); 146 } 147 148 _speak = () => { 149 const start = () => { 150 this.setState({ inProgress: true }); 151 }; 152 const complete = () => { 153 this.state.inProgress && this.setState({ inProgress: false, paused: false }); 154 }; 155 156 Speech.speak(this.state.selectedExample.text, { 157 voice: this.state.voice, 158 language: this.state.selectedExample.language, 159 pitch: this.state.pitch, 160 rate: this.state.rate, 161 onStart: start, 162 onDone: complete, 163 onStopped: complete, 164 onError: complete, 165 }); 166 }; 167 168 _loadAllVoices = async () => { 169 const availableVoices = await Speech.getAvailableVoicesAsync(); 170 this.setState({ 171 voiceList: availableVoices, 172 voice: availableVoices[0].identifier, 173 }); 174 }; 175 176 _stop = () => { 177 Speech.stop(); 178 }; 179 180 _pause = async () => { 181 await Speech.pause(); 182 this.setState({ paused: true }); 183 }; 184 185 _resume = () => { 186 Speech.resume(); 187 this.setState({ paused: false }); 188 }; 189 190 _increasePitch = () => { 191 this.setState(state => ({ 192 ...state, 193 pitch: state.pitch + 0.1, 194 })); 195 }; 196 197 _increaseRate = () => { 198 this.setState(state => ({ 199 ...state, 200 rate: state.rate + 0.1, 201 })); 202 }; 203 204 _decreasePitch = () => { 205 this.setState(state => ({ 206 ...state, 207 pitch: state.pitch - 0.1, 208 })); 209 }; 210 211 _decreaseRate = () => { 212 this.setState(state => ({ 213 ...state, 214 rate: state.rate - 0.1, 215 })); 216 }; 217 218 _renderExample = (example: { language: string; text: string }, i: number) => { 219 const { selectedExample } = this.state; 220 const isSelected = selectedExample === example; 221 222 return ( 223 <TouchableOpacity 224 key={i} 225 hitSlop={{ top: 10, bottom: 10, left: 20, right: 20 }} 226 onPress={() => this._selectExample(example)}> 227 <Text style={[styles.exampleText, isSelected && styles.selectedExampleText]}> 228 {example.text} ({example.language}) 229 </Text> 230 </TouchableOpacity> 231 ); 232 }; 233 234 _selectExample = (example: { language: string; text: string }) => { 235 this.setState({ selectedExample: example }); 236 }; 237} 238 239const styles = StyleSheet.create({ 240 container: { 241 flex: 1, 242 padding: 10, 243 paddingBottom: 24, 244 }, 245 separator: { 246 height: 1, 247 backgroundColor: '#eee', 248 marginTop: 0, 249 marginBottom: 15, 250 }, 251 exampleText: { 252 fontSize: 15, 253 color: '#ccc', 254 marginVertical: 10, 255 }, 256 examplesContainer: { 257 paddingTop: 15, 258 paddingBottom: 10, 259 paddingHorizontal: 20, 260 }, 261 selectedExampleText: { 262 color: 'black', 263 }, 264 resultText: { 265 padding: 20, 266 }, 267 errorResultText: { 268 padding: 20, 269 color: 'red', 270 }, 271 button: { 272 ...Platform.select({ 273 android: { 274 marginBottom: 10, 275 }, 276 }), 277 }, 278 controlText: { 279 fontSize: 16, 280 fontWeight: '500', 281 marginTop: 5, 282 textAlign: 'center', 283 }, 284 controlRow: { 285 flexDirection: 'row', 286 alignItems: 'center', 287 justifyContent: 'center', 288 marginBottom: 10, 289 }, 290}); 291