@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
- import android.content.pm.PackageInfo;
14
- import android.content.pm.PackageManager;
15
- import android.content.pm.PackageManager.NameNotFoundException;
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
- int amResult = audioManager.requestAudioFocus(afChangeListener,
215
- // Use the music stream.
216
- AudioManager.STREAM_MUSIC,
217
- // Request permanent focus.
218
- AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
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: number;
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: number;
77
+ KEY_PARAM_PAN?: number;
78
78
  };
79
79
 
80
80
  export type Options =
81
81
  | string
82
82
  | {
83
- iosVoiceId: string;
84
- rate: number;
85
- androidParams: AndroidOptions;
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([]);
@@ -17,4 +17,7 @@
17
17
  @property (nonatomic) float defaultRate;
18
18
  @property (nonatomic) float defaultPitch;
19
19
  @property (nonatomic) bool ducking;
20
+
21
+ // Singleton accessor for New Architecture compatibility
22
+ + (instancetype)sharedInstance;
20
23
  @end
@@ -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
- // This will pause a spoken audio like podcast or audiobook and duck the volume for music
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:(bool *)ducking
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
- withOptions:AVAudioSessionCategoryOptionDuckOthers
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
- [voices addObject:@{
222
- @"id": voice.identifier,
223
- @"name": voice.name,
224
- @"language": voice.language,
225
- @"quality": (voice.quality == AVSpeechSynthesisVoiceQualityEnhanced) ? @500 : @300
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
- [[AVAudioSession sharedInstance] setActive:true error:nil];
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
- [[AVAudioSession sharedInstance] setActive:false error:nil];
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
- [[AVAudioSession sharedInstance] setActive:false error:nil];
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
- [[AVAudioSession sharedInstance] setActive:true error:nil];
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
- [[AVAudioSession sharedInstance] setActive:false error:nil];
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]}];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iternio/react-native-tts",
3
- "version": "4.1.2",
3
+ "version": "4.1.4",
4
4
  "description": "React Native Text-To-Speech module for Android and iOS",
5
5
  "main": "index.js",
6
6
  "repository": {