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