@iternio/react-native-tts 4.1.2 → 4.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
package net.no_mad.tts;
|
|
2
2
|
|
|
3
|
+
import android.content.ActivityNotFoundException;
|
|
4
|
+
import android.content.Intent;
|
|
5
|
+
import android.content.pm.PackageInfo;
|
|
6
|
+
import android.content.pm.PackageManager;
|
|
7
|
+
import android.content.pm.PackageManager.NameNotFoundException;
|
|
8
|
+
import android.media.AudioAttributes;
|
|
9
|
+
import android.media.AudioFocusRequest;
|
|
3
10
|
import android.media.AudioManager;
|
|
11
|
+
import android.net.Uri;
|
|
4
12
|
import android.os.Build;
|
|
5
13
|
import android.os.Bundle;
|
|
6
|
-
import android.content.Intent;
|
|
7
|
-
import android.content.ActivityNotFoundException;
|
|
8
|
-
import android.app.Activity;
|
|
9
|
-
import android.net.Uri;
|
|
10
14
|
import android.speech.tts.TextToSpeech;
|
|
11
15
|
import android.speech.tts.UtteranceProgressListener;
|
|
12
16
|
import android.speech.tts.Voice;
|
|
13
|
-
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import com.facebook.react.bridge
|
|
17
|
+
|
|
18
|
+
import com.facebook.react.bridge.Arguments;
|
|
19
|
+
import com.facebook.react.bridge.Promise;
|
|
20
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
21
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
22
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
23
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
24
|
+
import com.facebook.react.bridge.WritableArray;
|
|
25
|
+
import com.facebook.react.bridge.WritableMap;
|
|
17
26
|
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
18
27
|
|
|
19
28
|
import java.util.ArrayList;
|
|
@@ -29,14 +38,21 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
|
|
|
29
38
|
|
|
30
39
|
private boolean ducking = false;
|
|
31
40
|
private AudioManager audioManager;
|
|
32
|
-
private AudioManager.OnAudioFocusChangeListener afChangeListener;
|
|
41
|
+
private AudioManager.OnAudioFocusChangeListener afChangeListener = i -> {};
|
|
33
42
|
|
|
34
43
|
private Map<String, Locale> localeCountryMap;
|
|
35
44
|
private Map<String, Locale> localeLanguageMap;
|
|
36
45
|
|
|
46
|
+
private AudioAttributes audioAttributes;
|
|
47
|
+
|
|
37
48
|
public TextToSpeechModule(ReactApplicationContext reactContext) {
|
|
38
49
|
super(reactContext);
|
|
39
50
|
audioManager = (AudioManager) reactContext.getApplicationContext().getSystemService(reactContext.AUDIO_SERVICE);
|
|
51
|
+
audioAttributes = new AudioAttributes.Builder()
|
|
52
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
53
|
+
.setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
|
|
54
|
+
.build();
|
|
55
|
+
|
|
40
56
|
initStatusPromises = new ArrayList<Promise>();
|
|
41
57
|
//initialize ISO3, ISO2 languague country code mapping.
|
|
42
58
|
initCountryLanguageCodeMapping();
|
|
@@ -97,6 +113,7 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
|
|
|
97
113
|
params.putInt("start", start);
|
|
98
114
|
params.putInt("end", end);
|
|
99
115
|
params.putInt("frame", frame);
|
|
116
|
+
params.putInt("length", end - start);
|
|
100
117
|
sendEvent("tts-progress", params);
|
|
101
118
|
}
|
|
102
119
|
});
|
|
@@ -210,12 +227,23 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
|
|
|
210
227
|
if(notReady(promise)) return;
|
|
211
228
|
|
|
212
229
|
if(ducking) {
|
|
230
|
+
int amResult;
|
|
213
231
|
// Request audio focus for playback
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
232
|
+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
|
233
|
+
AudioFocusRequest audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
|
|
234
|
+
.setAudioAttributes(audioAttributes)
|
|
235
|
+
.setAcceptsDelayedFocusGain(false)
|
|
236
|
+
.setOnAudioFocusChangeListener(afChangeListener)
|
|
237
|
+
.build();
|
|
238
|
+
|
|
239
|
+
amResult = audioManager.requestAudioFocus(audioFocusRequest);
|
|
240
|
+
} else {
|
|
241
|
+
amResult = audioManager.requestAudioFocus(afChangeListener,
|
|
242
|
+
// Use the music stream.
|
|
243
|
+
AudioManager.STREAM_MUSIC,
|
|
244
|
+
// Request permanent focus.
|
|
245
|
+
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
|
|
246
|
+
}
|
|
219
247
|
|
|
220
248
|
if(amResult != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
|
221
249
|
promise.reject("Android AudioManager error, failed to request audio focus");
|
|
@@ -311,6 +339,18 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
|
|
|
311
339
|
}
|
|
312
340
|
}
|
|
313
341
|
|
|
342
|
+
@ReactMethod
|
|
343
|
+
public void getDefaultVoiceIdentifier(String language, Promise promise) {
|
|
344
|
+
if(notReady(promise)) return;
|
|
345
|
+
|
|
346
|
+
Voice currentVoice = tts.getVoice();
|
|
347
|
+
if (currentVoice == null) {
|
|
348
|
+
promise.reject("not_found", "Language not found");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
promise.resolve(currentVoice.getName());
|
|
352
|
+
}
|
|
353
|
+
|
|
314
354
|
@ReactMethod
|
|
315
355
|
public void voices(Promise promise) {
|
|
316
356
|
if(notReady(promise)) return;
|
|
@@ -498,6 +538,8 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
|
|
|
498
538
|
audioStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE;
|
|
499
539
|
}
|
|
500
540
|
|
|
541
|
+
tts.setAudioAttributes(audioAttributes);
|
|
542
|
+
|
|
501
543
|
if (Build.VERSION.SDK_INT >= 21) {
|
|
502
544
|
Bundle params = new Bundle();
|
|
503
545
|
params.putInt(TextToSpeech.Engine.KEY_PARAM_STREAM, audioStreamType);
|
package/index.d.ts
CHANGED
|
@@ -62,7 +62,7 @@ export type Engine = {
|
|
|
62
62
|
|
|
63
63
|
export type AndroidOptions = {
|
|
64
64
|
/** Parameter key to specify the audio stream type to be used when speaking text or playing back a file */
|
|
65
|
-
KEY_PARAM_STREAM
|
|
65
|
+
KEY_PARAM_STREAM?:
|
|
66
66
|
| "STREAM_VOICE_CALL"
|
|
67
67
|
| "STREAM_SYSTEM"
|
|
68
68
|
| "STREAM_RING"
|
|
@@ -72,17 +72,17 @@ export type AndroidOptions = {
|
|
|
72
72
|
| "STREAM_DTMF"
|
|
73
73
|
| "STREAM_ACCESSIBILITY";
|
|
74
74
|
/** Parameter key to specify the speech volume relative to the current stream type volume used when speaking text. Volume is specified as a float ranging from 0 to 1 where 0 is silence, and 1 is the maximum volume (the default behavior). */
|
|
75
|
-
KEY_PARAM_VOLUME
|
|
75
|
+
KEY_PARAM_VOLUME?: number;
|
|
76
76
|
/** Parameter key to specify how the speech is panned from left to right when speaking text. Pan is specified as a float ranging from -1 to +1 where -1 maps to a hard-left pan, 0 to center (the default behavior), and +1 to hard-right. */
|
|
77
|
-
KEY_PARAM_PAN
|
|
77
|
+
KEY_PARAM_PAN?: number;
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
export type Options =
|
|
81
81
|
| string
|
|
82
82
|
| {
|
|
83
|
-
iosVoiceId
|
|
84
|
-
rate
|
|
85
|
-
androidParams
|
|
83
|
+
iosVoiceId?: string;
|
|
84
|
+
rate?: number;
|
|
85
|
+
androidParams?: AndroidOptions;
|
|
86
86
|
};
|
|
87
87
|
|
|
88
88
|
export class ReactNativeTts extends RN.NativeEventEmitter {
|
|
@@ -97,9 +97,10 @@ export class ReactNativeTts extends RN.NativeEventEmitter {
|
|
|
97
97
|
setDefaultLanguage: (language: string) => Promise<"success">;
|
|
98
98
|
setIgnoreSilentSwitch: (ignoreSilentSwitch: IOSSilentSwitchBehavior) => Promise<boolean>;
|
|
99
99
|
voices: () => Promise<Voice[]>;
|
|
100
|
+
getDefaultVoiceIdentifier: (language: String) => Promise<String>;
|
|
100
101
|
engines: () => Promise<Engine[]>;
|
|
101
102
|
/** Read the sentence and return an id for the task. */
|
|
102
|
-
speak: (utterance: string, options?: Options) => string | number
|
|
103
|
+
speak: (utterance: string, options?: Options) => Promise<string | number>;
|
|
103
104
|
stop: (onWordBoundary?: boolean) => Promise<boolean>;
|
|
104
105
|
pause: (onWordBoundary?: boolean) => Promise<boolean>;
|
|
105
106
|
resume: () => Promise<boolean>;
|
package/index.js
CHANGED
|
@@ -69,6 +69,10 @@ class Tts extends NativeEventEmitter {
|
|
|
69
69
|
return TextToSpeech.voices();
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
getDefaultVoiceIdentifier(language) {
|
|
73
|
+
return TextToSpeech.getDefaultVoiceIdentifier(language);
|
|
74
|
+
}
|
|
75
|
+
|
|
72
76
|
engines() {
|
|
73
77
|
if (Platform.OS === 'ios' || Platform.OS === 'windows') {
|
|
74
78
|
return Promise.resolve([]);
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
#import "TextToSpeech.h"
|
|
14
14
|
|
|
15
|
+
// Singleton instance for New Architecture compatibility
|
|
16
|
+
static TextToSpeech *_sharedInstance = nil;
|
|
17
|
+
|
|
15
18
|
@implementation TextToSpeech {
|
|
16
19
|
NSString * _ignoreSilentSwitch;
|
|
17
20
|
}
|
|
@@ -20,6 +23,11 @@
|
|
|
20
23
|
|
|
21
24
|
RCT_EXPORT_MODULE()
|
|
22
25
|
|
|
26
|
+
+ (instancetype)sharedInstance
|
|
27
|
+
{
|
|
28
|
+
return _sharedInstance;
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
-(NSArray<NSString *> *)supportedEvents
|
|
24
32
|
{
|
|
25
33
|
return @[@"tts-start", @"tts-finish", @"tts-pause", @"tts-resume", @"tts-progress", @"tts-cancel"];
|
|
@@ -27,12 +35,19 @@ RCT_EXPORT_MODULE()
|
|
|
27
35
|
|
|
28
36
|
-(instancetype)init
|
|
29
37
|
{
|
|
38
|
+
// If we already have a shared instance, return it instead of creating a new one
|
|
39
|
+
// This ensures singleton behavior in React Native New Architecture
|
|
40
|
+
if (_sharedInstance != nil) {
|
|
41
|
+
return _sharedInstance;
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
self = [super init];
|
|
31
45
|
if (self) {
|
|
32
46
|
_synthesizer = [AVSpeechSynthesizer new];
|
|
33
47
|
_synthesizer.delegate = self;
|
|
34
48
|
_ducking = false;
|
|
35
49
|
_ignoreSilentSwitch = @"inherit"; // inherit, ignore, obey
|
|
50
|
+
_sharedInstance = self;
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
return self;
|
|
@@ -79,11 +94,16 @@ RCT_EXPORT_METHOD(speak:(NSString *)text
|
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
if([_ignoreSilentSwitch isEqualToString:@"ignore"]) {
|
|
97
|
+
// Build options based on ducking setting
|
|
98
|
+
AVAudioSessionCategoryOptions options = AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers;
|
|
99
|
+
if (_ducking) {
|
|
100
|
+
options |= AVAudioSessionCategoryOptionDuckOthers;
|
|
101
|
+
}
|
|
102
|
+
|
|
82
103
|
[[AVAudioSession sharedInstance]
|
|
83
104
|
setCategory:AVAudioSessionCategoryPlayback
|
|
84
105
|
mode:AVAudioSessionModeVoicePrompt
|
|
85
|
-
|
|
86
|
-
options:AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers
|
|
106
|
+
options:options
|
|
87
107
|
error:nil
|
|
88
108
|
];
|
|
89
109
|
} else if([_ignoreSilentSwitch isEqualToString:@"obey"]) {
|
|
@@ -132,23 +152,23 @@ RCT_EXPORT_METHOD(resume:(RCTPromiseResolveBlock)resolve reject:(__unused RCTPro
|
|
|
132
152
|
}
|
|
133
153
|
|
|
134
154
|
|
|
135
|
-
RCT_EXPORT_METHOD(setDucking:(
|
|
155
|
+
RCT_EXPORT_METHOD(setDucking:(BOOL)ducking
|
|
136
156
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
137
157
|
reject:(__unused RCTPromiseRejectBlock)reject)
|
|
138
158
|
{
|
|
139
|
-
_ducking = ducking;
|
|
159
|
+
_ducking = (bool)ducking;
|
|
140
160
|
|
|
141
161
|
if(ducking) {
|
|
142
162
|
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
143
163
|
[session setCategory:AVAudioSessionCategoryPlayback
|
|
144
|
-
|
|
164
|
+
mode:AVAudioSessionModeVoicePrompt
|
|
165
|
+
options:(AVAudioSessionCategoryOptionDuckOthers|AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers)
|
|
145
166
|
error:nil];
|
|
146
167
|
}
|
|
147
168
|
|
|
148
169
|
resolve(@"success");
|
|
149
170
|
}
|
|
150
171
|
|
|
151
|
-
|
|
152
172
|
RCT_EXPORT_METHOD(setDefaultLanguage:(NSString *)language
|
|
153
173
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
154
174
|
reject:(RCTPromiseRejectBlock)reject)
|
|
@@ -212,18 +232,40 @@ RCT_EXPORT_METHOD(setIgnoreSilentSwitch:(NSString *)ignoreSilentSwitch
|
|
|
212
232
|
}
|
|
213
233
|
}
|
|
214
234
|
|
|
235
|
+
RCT_EXPORT_METHOD(getDefaultVoiceIdentifier:(NSString *)language
|
|
236
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
237
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
238
|
+
{
|
|
239
|
+
AVSpeechSynthesisVoice *voice = [AVSpeechSynthesisVoice voiceWithLanguage:language];
|
|
240
|
+
|
|
241
|
+
if(voice) {
|
|
242
|
+
resolve(voice.identifier);
|
|
243
|
+
} else {
|
|
244
|
+
reject(@"not_found", @"Language not found", nil);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
215
248
|
RCT_EXPORT_METHOD(voices:(RCTPromiseResolveBlock)resolve
|
|
216
249
|
reject:(__unused RCTPromiseRejectBlock)reject)
|
|
217
250
|
{
|
|
218
251
|
NSMutableArray *voices = [NSMutableArray new];
|
|
219
252
|
|
|
220
253
|
for (AVSpeechSynthesisVoice *voice in [AVSpeechSynthesisVoice speechVoices]) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
254
|
+
if (@available(iOS 16.0, *)) {
|
|
255
|
+
[voices addObject:@{
|
|
256
|
+
@"id": voice.identifier,
|
|
257
|
+
@"name": voice.name,
|
|
258
|
+
@"language": voice.language,
|
|
259
|
+
@"quality": (voice.quality == AVSpeechSynthesisVoiceQualityEnhanced) ? @500 : (voice.quality == AVSpeechSynthesisVoiceQualityPremium) ? @800 : @300
|
|
260
|
+
}];
|
|
261
|
+
} else {
|
|
262
|
+
[voices addObject:@{
|
|
263
|
+
@"id": voice.identifier,
|
|
264
|
+
@"name": voice.name,
|
|
265
|
+
@"language": voice.language,
|
|
266
|
+
@"quality": (voice.quality == AVSpeechSynthesisVoiceQualityEnhanced) ? @500 : @300
|
|
267
|
+
}];
|
|
268
|
+
}
|
|
227
269
|
}
|
|
228
270
|
|
|
229
271
|
resolve(voices);
|
|
@@ -232,7 +274,9 @@ RCT_EXPORT_METHOD(voices:(RCTPromiseResolveBlock)resolve
|
|
|
232
274
|
-(void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didStartSpeechUtterance:(AVSpeechUtterance *)utterance
|
|
233
275
|
{
|
|
234
276
|
if(_ducking) {
|
|
235
|
-
|
|
277
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
278
|
+
[[AVAudioSession sharedInstance] setActive:true error:nil];
|
|
279
|
+
});
|
|
236
280
|
}
|
|
237
281
|
|
|
238
282
|
[self sendEventWithName:@"tts-start" body:@{@"utteranceId":[NSNumber numberWithUnsignedLong:utterance.hash]}];
|
|
@@ -241,7 +285,9 @@ RCT_EXPORT_METHOD(voices:(RCTPromiseResolveBlock)resolve
|
|
|
241
285
|
-(void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance
|
|
242
286
|
{
|
|
243
287
|
if(_ducking) {
|
|
244
|
-
|
|
288
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
289
|
+
[[AVAudioSession sharedInstance] setActive:false error:nil];
|
|
290
|
+
});
|
|
245
291
|
}
|
|
246
292
|
|
|
247
293
|
[self sendEventWithName:@"tts-finish" body:@{@"utteranceId":[NSNumber numberWithUnsignedLong:utterance.hash]}];
|
|
@@ -250,7 +296,9 @@ RCT_EXPORT_METHOD(voices:(RCTPromiseResolveBlock)resolve
|
|
|
250
296
|
-(void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didPauseSpeechUtterance:(AVSpeechUtterance *)utterance
|
|
251
297
|
{
|
|
252
298
|
if(_ducking) {
|
|
253
|
-
|
|
299
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
300
|
+
[[AVAudioSession sharedInstance] setActive:false error:nil];
|
|
301
|
+
});
|
|
254
302
|
}
|
|
255
303
|
|
|
256
304
|
[self sendEventWithName:@"tts-pause" body:@{@"utteranceId":[NSNumber numberWithUnsignedLong:utterance.hash]}];
|
|
@@ -259,7 +307,9 @@ RCT_EXPORT_METHOD(voices:(RCTPromiseResolveBlock)resolve
|
|
|
259
307
|
-(void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didContinueSpeechUtterance:(AVSpeechUtterance *)utterance
|
|
260
308
|
{
|
|
261
309
|
if(_ducking) {
|
|
262
|
-
|
|
310
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
311
|
+
[[AVAudioSession sharedInstance] setActive:true error:nil];
|
|
312
|
+
});
|
|
263
313
|
}
|
|
264
314
|
|
|
265
315
|
[self sendEventWithName:@"tts-resume" body:@{@"utteranceId":[NSNumber numberWithUnsignedLong:utterance.hash]}];
|
|
@@ -276,7 +326,9 @@ RCT_EXPORT_METHOD(voices:(RCTPromiseResolveBlock)resolve
|
|
|
276
326
|
-(void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didCancelSpeechUtterance:(AVSpeechUtterance *)utterance
|
|
277
327
|
{
|
|
278
328
|
if(_ducking) {
|
|
279
|
-
|
|
329
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
330
|
+
[[AVAudioSession sharedInstance] setActive:false error:nil];
|
|
331
|
+
});
|
|
280
332
|
}
|
|
281
333
|
|
|
282
334
|
[self sendEventWithName:@"tts-cancel" body:@{@"utteranceId":[NSNumber numberWithUnsignedLong:utterance.hash]}];
|