Skip to content

Commit d1038f3

Browse files
authored
Text to speech example (#216)
* feat: working on open ai example * feat: cleanup and renaming
1 parent bdad8eb commit d1038f3

File tree

8 files changed

+160
-1
lines changed

8 files changed

+160
-1
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,6 @@ react-native-audio-api*.tgz
8383
# Android
8484
.kotlin
8585

86+
87+
# Envs
8688
.env
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import React, { useState, FC } from 'react';
2+
import { AudioBuffer, AudioContext } from 'react-native-audio-api';
3+
import { ActivityIndicator, TextInput, StyleSheet } from 'react-native';
4+
5+
import { Container, Button, Spacer } from '../../components';
6+
import Env from '../../utils/env';
7+
import { colors } from '../../styles';
8+
9+
async function getOpenAIResponse(input: string, voice: string = 'alloy') {
10+
return await fetch('https://api.openai.com/v1/audio/speech', {
11+
method: 'POST',
12+
headers: {
13+
'Content-Type': 'application/json',
14+
'Authorization': `Bearer ${Env.openAiToken}`,
15+
},
16+
body: JSON.stringify({
17+
model: 'tts-1-hd',
18+
voice: voice,
19+
input: input,
20+
response_format: 'pcm',
21+
}),
22+
}).then((response) => response.arrayBuffer());
23+
}
24+
25+
const openAISampleRate = 24000;
26+
const maxInputValue = 32768.0;
27+
28+
// TODO: this should ideally be done using native code through .decodeAudioData
29+
function goofyResample(
30+
audioContext: AudioContext,
31+
input: Int16Array
32+
): AudioBuffer {
33+
const scale = audioContext.sampleRate / openAISampleRate;
34+
35+
const outputBuffer = audioContext.createBuffer(
36+
2,
37+
input.length * scale,
38+
audioContext.sampleRate
39+
);
40+
41+
const processingChannel: Array<number> = [];
42+
const upSampleChannel: Array<number> = [];
43+
44+
for (let i = 0; i < input.length; i += 1) {
45+
processingChannel[i] = input[i] / maxInputValue;
46+
}
47+
48+
for (let i = 0; i < input.length; i += 1) {
49+
const isLast = i === input.length - 1;
50+
const currentSample = processingChannel[i];
51+
const nextSample = isLast ? currentSample : processingChannel[i + 1];
52+
53+
upSampleChannel[2 * i] = currentSample;
54+
upSampleChannel[2 * i + 1] = (currentSample + nextSample) / 2;
55+
}
56+
57+
outputBuffer.copyToChannel(upSampleChannel, 0);
58+
outputBuffer.copyToChannel(upSampleChannel, 1);
59+
60+
return outputBuffer;
61+
}
62+
63+
const TextToSpeech: FC = () => {
64+
const [isLoading, setIsLoading] = useState(false);
65+
const [textToRead, setTextToRead] = useState('');
66+
67+
const onReadText = async () => {
68+
if (isLoading) {
69+
return;
70+
}
71+
72+
const aCtx = new AudioContext();
73+
74+
setIsLoading(true);
75+
const results = await getOpenAIResponse(textToRead, 'alloy');
76+
setIsLoading(false);
77+
78+
const audioBuffer = goofyResample(aCtx, new Int16Array(results));
79+
const sourceNode = aCtx.createBufferSource();
80+
const duration = audioBuffer.duration;
81+
const now = aCtx.currentTime;
82+
83+
sourceNode.buffer = audioBuffer;
84+
85+
sourceNode.connect(aCtx.destination);
86+
87+
sourceNode.start(now);
88+
sourceNode.stop(now + duration);
89+
};
90+
91+
return (
92+
<Container style={styles.container}>
93+
<Spacer.Vertical size={60} />
94+
<TextInput
95+
value={textToRead}
96+
onChangeText={setTextToRead}
97+
style={styles.textInput}
98+
multiline
99+
/>
100+
<Spacer.Vertical size={24} />
101+
<Button onPress={onReadText} title="Read Text" />
102+
<Spacer.Vertical size={24} />
103+
{isLoading && <ActivityIndicator />}
104+
</Container>
105+
);
106+
};
107+
108+
export default TextToSpeech;
109+
110+
const styles = StyleSheet.create({
111+
container: {
112+
alignItems: 'center',
113+
},
114+
textInput: {
115+
backgroundColor: 'transparent',
116+
borderColor: colors.border,
117+
color: colors.white,
118+
borderWidth: 1,
119+
fontSize: 16,
120+
padding: 16,
121+
width: 280,
122+
height: 200,
123+
borderRadius: 6,
124+
},
125+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './TextToSpeech';

apps/common-app/src/examples/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { StackNavigationProp } from '@react-navigation/stack';
22

33
import Piano from './Piano';
4+
import TextToSpeech from './TextToSpeech';
45
import Metronome from './Metronome';
56
import Oscillator from './Oscillator';
67
import DrumMachine from './DrumMachine';
@@ -11,6 +12,7 @@ type NavigationParamList = {
1112
Metronome: undefined;
1213
DrumMachine: undefined;
1314
Piano: undefined;
15+
TextToSpeech: undefined;
1416
AudioFile: undefined;
1517
};
1618

@@ -37,6 +39,12 @@ export const Examples: Example[] = [
3739
subtitle: 'Play some notes',
3840
screen: Piano,
3941
},
42+
{
43+
key: 'TextToSpeech',
44+
title: 'Text to Speech',
45+
subtitle: 'type some text and hear it spoken',
46+
screen: TextToSpeech,
47+
},
4048
{
4149
key: 'Metronome',
4250
title: 'Metronome',

apps/common-app/src/utils/env.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
openAiToken: process.env.OPENAI_API_TOKEN,
3+
};

apps/fabric-example/babel.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
module.exports = {
22
presets: ['module:@react-native/babel-preset'],
3-
plugins: ['react-native-reanimated/plugin'],
3+
plugins: ['react-native-reanimated/plugin', 'module:react-native-dotenv'],
44
};

apps/fabric-example/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"eslint": "^8.19.0",
4343
"jest": "^29.6.3",
4444
"prettier": "2.8.8",
45+
"react-native-dotenv": "^3.4.11",
4546
"react-test-renderer": "18.3.1",
4647
"typescript": "5.0.4"
4748
},

yarn.lock

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5418,6 +5418,13 @@ __metadata:
54185418
languageName: node
54195419
linkType: hard
54205420

5421+
"dotenv@npm:^16.4.5":
5422+
version: 16.4.5
5423+
resolution: "dotenv@npm:16.4.5"
5424+
checksum: 10/55a3134601115194ae0f924e54473459ed0d9fc340ae610b676e248cca45aa7c680d86365318ea964e6da4e2ea80c4514c1adab5adb43d6867fb57ff068f95c8
5425+
languageName: node
5426+
linkType: hard
5427+
54215428
"eastasianwidth@npm:^0.2.0":
54225429
version: 0.2.0
54235430
resolution: "eastasianwidth@npm:0.2.0"
@@ -6150,6 +6157,7 @@ __metadata:
61506157
react-dom: "npm:18.2.0"
61516158
react-native: "npm:0.76.0"
61526159
react-native-audio-api: "workspace:*"
6160+
react-native-dotenv: "npm:^3.4.11"
61536161
react-native-gesture-handler: "npm:^2.20.2"
61546162
react-native-reanimated: "npm:^3.16.1"
61556163
react-native-safe-area-context: "npm:^4.12.0"
@@ -10219,6 +10227,17 @@ __metadata:
1021910227
languageName: node
1022010228
linkType: hard
1022110229

10230+
"react-native-dotenv@npm:^3.4.11":
10231+
version: 3.4.11
10232+
resolution: "react-native-dotenv@npm:3.4.11"
10233+
dependencies:
10234+
dotenv: "npm:^16.4.5"
10235+
peerDependencies:
10236+
"@babel/runtime": ^7.20.6
10237+
checksum: 10/09e8a7310fcb01ac021e71db9328e9d342d1e117bf68026b12de0392bfe17292ac6a071f03b88e7fb42c82a8f2fdf03bc520c7dedd2f80a1448cb3de5e03d4fb
10238+
languageName: node
10239+
linkType: hard
10240+
1022210241
"react-native-gesture-handler@npm:^2.20.2":
1022310242
version: 2.21.1
1022410243
resolution: "react-native-gesture-handler@npm:2.21.1"

0 commit comments

Comments
 (0)