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