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