@iternio/react-native-tts 4.1.3 → 5.0.0

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.
@@ -13,7 +13,7 @@ buildscript {
13
13
  }
14
14
 
15
15
  dependencies {
16
- classpath 'com.android.tools.build:gradle:1.3.1'
16
+ classpath 'com.android.tools.build:gradle'
17
17
  }
18
18
  }
19
19
  }
@@ -21,12 +21,12 @@ buildscript {
21
21
  apply plugin: 'com.android.library'
22
22
 
23
23
  android {
24
- compileSdkVersion safeExtGet('compileSdkVersion', 26)
25
- buildToolsVersion safeExtGet('buildToolsVersion', '26.0.3')
24
+ compileSdkVersion safeExtGet('compileSdkVersion', 36)
25
+ buildToolsVersion safeExtGet('buildToolsVersion', '36.0.0')
26
26
 
27
27
  defaultConfig {
28
- minSdkVersion safeExtGet('minSdkVersion', 16)
29
- targetSdkVersion safeExtGet('targetSdkVersion', 26)
28
+ minSdkVersion safeExtGet('minSdkVersion', 26)
29
+ targetSdkVersion safeExtGet('targetSdkVersion', 36)
30
30
  versionCode 1
31
31
  versionName "1.0"
32
32
  }
@@ -1,6 +1,8 @@
1
1
  package net.no_mad.tts;
2
2
 
3
+ import android.app.Activity;
3
4
  import android.content.ActivityNotFoundException;
5
+ import android.content.Context;
4
6
  import android.content.Intent;
5
7
  import android.content.pm.PackageInfo;
6
8
  import android.content.pm.PackageManager;
@@ -9,12 +11,13 @@ import android.media.AudioAttributes;
9
11
  import android.media.AudioFocusRequest;
10
12
  import android.media.AudioManager;
11
13
  import android.net.Uri;
12
- import android.os.Build;
13
14
  import android.os.Bundle;
14
15
  import android.speech.tts.TextToSpeech;
15
16
  import android.speech.tts.UtteranceProgressListener;
16
17
  import android.speech.tts.Voice;
17
18
 
19
+ import androidx.annotation.NonNull;
20
+
18
21
  import com.facebook.react.bridge.Arguments;
19
22
  import com.facebook.react.bridge.Promise;
20
23
  import com.facebook.react.bridge.ReactApplicationContext;
@@ -26,47 +29,38 @@ import com.facebook.react.bridge.WritableMap;
26
29
  import com.facebook.react.modules.core.DeviceEventManagerModule;
27
30
 
28
31
  import java.util.ArrayList;
29
- import java.util.HashMap;
30
32
  import java.util.Locale;
31
- import java.util.Map;
33
+ import java.util.Optional;
32
34
 
33
35
  public class TextToSpeechModule extends ReactContextBaseJavaModule {
34
36
 
35
37
  private TextToSpeech tts;
36
38
  private Boolean ready = null;
37
- private ArrayList<Promise> initStatusPromises;
39
+ private final ArrayList<Promise> initStatusPromises;
38
40
 
39
41
  private boolean ducking = false;
40
- private AudioManager audioManager;
41
- private AudioManager.OnAudioFocusChangeListener afChangeListener = i -> {};
42
-
43
- private Map<String, Locale> localeCountryMap;
44
- private Map<String, Locale> localeLanguageMap;
42
+ private final AudioManager audioManager;
43
+ private AudioFocusRequest audioFocusRequest = null;
45
44
 
46
- private AudioAttributes audioAttributes;
45
+ private final AudioAttributes audioAttributes;
47
46
 
48
47
  public TextToSpeechModule(ReactApplicationContext reactContext) {
49
48
  super(reactContext);
50
- audioManager = (AudioManager) reactContext.getApplicationContext().getSystemService(reactContext.AUDIO_SERVICE);
49
+ audioManager = (AudioManager) reactContext.getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
51
50
  audioAttributes = new AudioAttributes.Builder()
52
51
  .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
53
52
  .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
54
53
  .build();
55
54
 
56
- initStatusPromises = new ArrayList<Promise>();
57
- //initialize ISO3, ISO2 languague country code mapping.
58
- initCountryLanguageCodeMapping();
59
-
60
- tts = new TextToSpeech(getReactApplicationContext(), new TextToSpeech.OnInitListener() {
61
- @Override
62
- public void onInit(int status) {
63
- synchronized(initStatusPromises) {
64
- ready = (status == TextToSpeech.SUCCESS) ? Boolean.TRUE : Boolean.FALSE;
65
- for(Promise p: initStatusPromises) {
66
- resolveReadyPromise(p);
67
- }
68
- initStatusPromises.clear();
55
+ initStatusPromises = new ArrayList<>();
56
+
57
+ tts = new TextToSpeech(reactContext, status -> {
58
+ synchronized(initStatusPromises) {
59
+ ready = status == TextToSpeech.SUCCESS;
60
+ for(Promise p: initStatusPromises) {
61
+ resolveReadyPromise(p);
69
62
  }
63
+ initStatusPromises.clear();
70
64
  }
71
65
  });
72
66
 
@@ -85,7 +79,8 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
85
79
  @Override
86
80
  public void onDone(String utteranceId) {
87
81
  if(ducking) {
88
- audioManager.abandonAudioFocus(afChangeListener);
82
+ audioManager.abandonAudioFocusRequest(audioFocusRequest);
83
+ audioFocusRequest = null;
89
84
  }
90
85
  sendEvent("tts-finish", utteranceId);
91
86
  }
@@ -93,7 +88,8 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
93
88
  @Override
94
89
  public void onError(String utteranceId) {
95
90
  if(ducking) {
96
- audioManager.abandonAudioFocus(afChangeListener);
91
+ audioManager.abandonAudioFocusRequest(audioFocusRequest);
92
+ audioFocusRequest = null;
97
93
  }
98
94
  sendEvent("tts-error", utteranceId);
99
95
  }
@@ -101,7 +97,8 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
101
97
  @Override
102
98
  public void onStop(String utteranceId, boolean interrupted) {
103
99
  if(ducking) {
104
- audioManager.abandonAudioFocus(afChangeListener);
100
+ audioManager.abandonAudioFocusRequest(audioFocusRequest);
101
+ audioFocusRequest = null;
105
102
  }
106
103
  sendEvent("tts-cancel", utteranceId);
107
104
  }
@@ -120,34 +117,10 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
120
117
  }
121
118
  }
122
119
 
123
- private void initCountryLanguageCodeMapping() {
124
- String[] countries = Locale.getISOCountries();
125
- localeCountryMap = new HashMap<String, Locale>(countries.length);
126
- for (String country: countries) {
127
- Locale locale = new Locale("", country);
128
- localeCountryMap.put(locale.getISO3Country().toUpperCase(), locale);
129
- }
130
- String[] languages = Locale.getISOLanguages();
131
- localeLanguageMap = new HashMap<String, Locale>(languages.length);
132
- for (String language: languages) {
133
- Locale locale = new Locale(language);
134
- localeLanguageMap.put(locale.getISO3Language(), locale);
135
- }
136
- }
137
-
138
- private String iso3CountryCodeToIso2CountryCode(String iso3CountryCode) {
139
- return localeCountryMap.get(iso3CountryCode).getCountry();
140
- }
141
-
142
- private String iso3LanguageCodeToIso2LanguageCode(String iso3LanguageCode) {
143
- return localeLanguageMap.get(iso3LanguageCode).getLanguage();
144
- }
145
-
146
120
  private void resolveReadyPromise(Promise promise) {
147
121
  if (ready == Boolean.TRUE) {
148
122
  promise.resolve("success");
149
- }
150
- else {
123
+ } else {
151
124
  promise.reject("no_engine", "No TTS engine installed");
152
125
  }
153
126
  }
@@ -193,7 +166,7 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
193
166
  default:
194
167
  promise.reject("error", "Unknown error code: " + statusCode);
195
168
  break;
196
- }
169
+ }
197
170
  }
198
171
 
199
172
  private boolean isPackageInstalled(String packageName) {
@@ -206,6 +179,7 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
206
179
  }
207
180
  }
208
181
 
182
+ @NonNull
209
183
  @Override
210
184
  public String getName() {
211
185
  return "TextToSpeech";
@@ -226,27 +200,17 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
226
200
  public void speak(String utterance, ReadableMap params, Promise promise) {
227
201
  if(notReady(promise)) return;
228
202
 
229
- if(ducking) {
230
- int amResult;
203
+ if(ducking && audioFocusRequest == null) {
231
204
  // Request audio focus for playback
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
- }
205
+ audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
206
+ .setAudioAttributes(audioAttributes)
207
+ .setAcceptsDelayedFocusGain(false)
208
+ .build();
209
+
210
+ int requestStatus = audioManager.requestAudioFocus(audioFocusRequest);
247
211
 
248
- if(amResult != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
249
- promise.reject("Android AudioManager error, failed to request audio focus");
212
+ if(requestStatus != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
213
+ promise.reject("request_focus_failed", "Android AudioManager error, failed to request audio focus");
250
214
  return;
251
215
  }
252
216
  }
@@ -265,9 +229,9 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
265
229
  public void setDefaultLanguage(String language, Promise promise) {
266
230
  if(notReady(promise)) return;
267
231
 
268
- Locale locale = null;
232
+ Locale locale;
269
233
 
270
- if(language.indexOf("-") != -1) {
234
+ if(language.contains("-")) {
271
235
  String[] parts = language.split("-");
272
236
  locale = new Locale(parts[0], parts[1]);
273
237
  } else {
@@ -275,10 +239,10 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
275
239
  }
276
240
 
277
241
  try {
278
- int result = tts.setLanguage(locale);
279
- resolvePromiseWithStatusCode(result, promise);
242
+ int result = tts.setLanguage(locale);
243
+ resolvePromiseWithStatusCode(result, promise);
280
244
  } catch (Exception e) {
281
- promise.reject("error", "Unknown error code");
245
+ promise.reject("error", "Unknown error code");
282
246
  }
283
247
  }
284
248
 
@@ -301,9 +265,9 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
301
265
  // rate value will be in the range 0.0 to 1.0
302
266
  // let's convert it to the range of values Android platform expects,
303
267
  // where 1.0 is no change of rate and 2.0 is the twice faster rate
304
- float androidRate = rate.floatValue() < 0.5f ?
305
- rate.floatValue() * 2 : // linear fit {0, 0}, {0.25, 0.5}, {0.5, 1}
306
- rate.floatValue() * 4 - 1; // linear fit {{0.5, 1}, {0.75, 2}, {1, 3}}
268
+ float androidRate = rate < 0.5f ?
269
+ rate * 2 : // linear fit {0, 0}, {0.25, 0.5}, {0.5, 1}
270
+ rate * 4 - 1; // linear fit {{0.5, 1}, {0.75, 2}, {1, 3}}
307
271
  int result = tts.setSpeechRate(androidRate);
308
272
  resolvePromiseWithStatusCode(result, promise);
309
273
  }
@@ -320,23 +284,19 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
320
284
  public void setDefaultVoice(String voiceId, Promise promise) {
321
285
  if(notReady(promise)) return;
322
286
 
323
- if (Build.VERSION.SDK_INT >= 21) {
324
- try {
325
- for(Voice voice: tts.getVoices()) {
326
- if(voice.getName().equals(voiceId)) {
327
- int result = tts.setVoice(voice);
328
- resolvePromiseWithStatusCode(result, promise);
329
- return;
330
- }
287
+ try {
288
+ for (Voice voice : tts.getVoices()) {
289
+ if (voice.getName().equals(voiceId)) {
290
+ int result = tts.setVoice(voice);
291
+ resolvePromiseWithStatusCode(result, promise);
292
+ return;
331
293
  }
332
- } catch (Exception e) {
333
- // Purposefully ignore exceptions here due to some buggy TTS engines.
334
- // See http://stackoverflow.com/questions/26730082/illegalargumentexception-invalid-int-os-with-samsung-tts
335
294
  }
336
- promise.reject("not_found", "The selected voice was not found");
337
- } else {
338
- promise.reject("not_available", "Android API 21 level or higher is required");
295
+ } catch (Exception e) {
296
+ // Purposefully ignore exceptions here due to some buggy TTS engines.
297
+ // See http://stackoverflow.com/questions/26730082/illegalargumentexception-invalid-int-os-with-samsung-tts
339
298
  }
299
+ promise.reject("not_found", "The selected voice was not found");
340
300
  }
341
301
 
342
302
  @ReactMethod
@@ -357,30 +317,28 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
357
317
 
358
318
  WritableArray voiceArray = Arguments.createArray();
359
319
 
360
- if (Build.VERSION.SDK_INT >= 21) {
361
- try {
362
- for(Voice voice: tts.getVoices()) {
363
- WritableMap voiceMap = Arguments.createMap();
364
- voiceMap.putString("id", voice.getName());
365
- voiceMap.putString("name", voice.getName());
366
-
367
- String language = iso3LanguageCodeToIso2LanguageCode(voice.getLocale().getISO3Language());
368
- String country = voice.getLocale().getISO3Country();
369
- if(country != "") {
370
- language += "-" + iso3CountryCodeToIso2CountryCode(country);
371
- }
372
-
373
- voiceMap.putString("language", language);
374
- voiceMap.putInt("quality", voice.getQuality());
375
- voiceMap.putInt("latency", voice.getLatency());
376
- voiceMap.putBoolean("networkConnectionRequired", voice.isNetworkConnectionRequired());
377
- voiceMap.putBoolean("notInstalled", voice.getFeatures().contains(TextToSpeech.Engine.KEY_FEATURE_NOT_INSTALLED));
378
- voiceArray.pushMap(voiceMap);
320
+ try {
321
+ for (Voice voice : tts.getVoices()) {
322
+ WritableMap voiceMap = Arguments.createMap();
323
+ voiceMap.putString("id", voice.getName());
324
+ voiceMap.putString("name", voice.getName());
325
+
326
+ String language = voice.getLocale().getLanguage();
327
+ String country = voice.getLocale().getCountry();
328
+ if (!country.isEmpty()) {
329
+ language += "-" + country;
379
330
  }
380
- } catch (Exception e) {
381
- // Purposefully ignore exceptions here due to some buggy TTS engines.
382
- // See http://stackoverflow.com/questions/26730082/illegalargumentexception-invalid-int-os-with-samsung-tts
331
+
332
+ voiceMap.putString("language", language);
333
+ voiceMap.putInt("quality", voice.getQuality());
334
+ voiceMap.putInt("latency", voice.getLatency());
335
+ voiceMap.putBoolean("networkConnectionRequired", voice.isNetworkConnectionRequired());
336
+ voiceMap.putBoolean("notInstalled", voice.getFeatures().contains(TextToSpeech.Engine.KEY_FEATURE_NOT_INSTALLED));
337
+ voiceArray.pushMap(voiceMap);
383
338
  }
339
+ } catch (Exception e) {
340
+ // Purposefully ignore exceptions here due to some buggy TTS engines.
341
+ // See http://stackoverflow.com/questions/26730082/illegalargumentexception-invalid-int-os-with-samsung-tts
384
342
  }
385
343
 
386
344
  promise.resolve(voiceArray);
@@ -392,12 +350,12 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
392
350
 
393
351
  if(isPackageInstalled(engineName)) {
394
352
  ready = null;
395
- onCatalystInstanceDestroy();
353
+ invalidate();
396
354
  tts = new TextToSpeech(getReactApplicationContext(), new TextToSpeech.OnInitListener() {
397
355
  @Override
398
356
  public void onInit(int status) {
399
357
  synchronized(initStatusPromises) {
400
- ready = (status == TextToSpeech.SUCCESS) ? Boolean.TRUE : Boolean.FALSE;
358
+ ready = status == TextToSpeech.SUCCESS;
401
359
  for(Promise p: initStatusPromises) {
402
360
  resolveReadyPromise(p);
403
361
  }
@@ -419,22 +377,20 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
419
377
 
420
378
  WritableArray engineArray = Arguments.createArray();
421
379
 
422
- if (Build.VERSION.SDK_INT >= 14) {
423
- try {
424
- String defaultEngineName = tts.getDefaultEngine();
425
- for(TextToSpeech.EngineInfo engine: tts.getEngines()) {
426
- WritableMap engineMap = Arguments.createMap();
380
+ try {
381
+ String defaultEngineName = tts.getDefaultEngine();
382
+ for (TextToSpeech.EngineInfo engine : tts.getEngines()) {
383
+ WritableMap engineMap = Arguments.createMap();
427
384
 
428
- engineMap.putString("name", engine.name);
429
- engineMap.putString("label", engine.label);
430
- engineMap.putBoolean("default", engine.name.equals(defaultEngineName));
431
- engineMap.putInt("icon", engine.icon);
385
+ engineMap.putString("name", engine.name);
386
+ engineMap.putString("label", engine.label);
387
+ engineMap.putBoolean("default", engine.name.equals(defaultEngineName));
388
+ engineMap.putInt("icon", engine.icon);
432
389
 
433
- engineArray.pushMap(engineMap);
434
- }
435
- } catch (Exception e) {
436
- promise.reject("error", "Unknown error code");
390
+ engineArray.pushMap(engineMap);
437
391
  }
392
+ } catch (Exception e) {
393
+ promise.reject("error", "Unknown error code");
438
394
  }
439
395
 
440
396
  promise.resolve(engineArray);
@@ -454,10 +410,12 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
454
410
  Intent intent = new Intent(Intent.ACTION_VIEW);
455
411
  intent.setData(Uri.parse("market://details?id=com.google.android.tts"));
456
412
  try {
457
- getCurrentActivity().startActivity(intent);
413
+ startActivity(intent);
458
414
  promise.resolve("success");
459
- } catch (Exception e) {
415
+ } catch (ActivityNotFoundException e) {
460
416
  promise.reject("error", "Could not open Google Text to Speech App in the Play Store");
417
+ } catch (Exception e) {
418
+ promise.reject("unknown_error", e.getMessage());
461
419
  }
462
420
  }
463
421
 
@@ -466,94 +424,71 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
466
424
  Intent intent = new Intent();
467
425
  intent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
468
426
  try {
469
- getCurrentActivity().startActivity(intent);
427
+ startActivity(intent);
470
428
  promise.resolve("success");
471
429
  } catch (ActivityNotFoundException e) {
472
430
  promise.reject("no_engine", "No TTS engine installed");
431
+ } catch (Exception e) {
432
+ promise.reject("unknown_error", e.getMessage());
473
433
  }
474
434
  }
475
435
 
476
436
  /**
477
- * called on React Native Reloading JavaScript
478
- * https://stackoverflow.com/questions/15563361/tts-leaked-serviceconnection
437
+ * <a href="https://stackoverflow.com/questions/15563361/tts-leaked-serviceconnection">called on React Native Reloading JavaScript</a>
479
438
  */
480
439
  @Override
481
- public void onCatalystInstanceDestroy() {
482
- super.onCatalystInstanceDestroy();
440
+ public void invalidate() {
483
441
  if(tts != null) {
484
442
  tts.stop();
485
443
  tts.shutdown();
486
444
  }
487
445
  }
488
446
 
447
+ private void startActivity(Intent intent) {
448
+ Activity activity = getReactApplicationContext().getCurrentActivity();
449
+ if (activity == null) {
450
+ throw new ActivityNotFoundException();
451
+ }
452
+ activity.startActivity(intent);
453
+ }
454
+
489
455
  private boolean notReady(Promise promise) {
490
456
  if(ready == null) {
491
457
  promise.reject("not_ready", "TTS is not ready");
492
458
  return true;
493
459
  }
494
- else if(ready != Boolean.TRUE) {
460
+ else if(!ready) {
495
461
  resolveReadyPromise(promise);
496
462
  return true;
497
463
  }
498
464
  return false;
499
465
  }
500
466
 
501
- @SuppressWarnings("deprecation")
502
467
  private int speak(String utterance, String utteranceId, ReadableMap inputParams) {
503
468
  String audioStreamTypeString = inputParams.hasKey("KEY_PARAM_STREAM") ? inputParams.getString("KEY_PARAM_STREAM") : "";
504
469
  float volume = inputParams.hasKey("KEY_PARAM_VOLUME") ? (float) inputParams.getDouble("KEY_PARAM_VOLUME") : 1.0f;
505
470
  float pan = inputParams.hasKey("KEY_PARAM_PAN") ? (float) inputParams.getDouble("KEY_PARAM_PAN") : 0.0f;
506
471
 
507
- int audioStreamType;
508
- switch(audioStreamTypeString) {
509
- /*
510
- // This has been added in API level 26, commenting out for now
511
-
512
- case "STREAM_ACCESSIBILITY":
513
- audioStreamType = AudioManager.STREAM_ACCESSIBILITY;
514
- break;
515
- */
516
- case "STREAM_ALARM":
517
- audioStreamType = AudioManager.STREAM_ALARM;
518
- break;
519
- case "STREAM_DTMF":
520
- audioStreamType = AudioManager.STREAM_DTMF;
521
- break;
522
- case "STREAM_MUSIC":
523
- audioStreamType = AudioManager.STREAM_MUSIC;
524
- break;
525
- case "STREAM_NOTIFICATION":
526
- audioStreamType = AudioManager.STREAM_NOTIFICATION;
527
- break;
528
- case "STREAM_RING":
529
- audioStreamType = AudioManager.STREAM_RING;
530
- break;
531
- case "STREAM_SYSTEM":
532
- audioStreamType = AudioManager.STREAM_SYSTEM;
533
- break;
534
- case "STREAM_VOICE_CALL":
535
- audioStreamType = AudioManager.STREAM_VOICE_CALL;
536
- break;
537
- default:
538
- audioStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE;
539
- }
472
+ int audioStreamType = switch (Optional.ofNullable(audioStreamTypeString).orElse("")) {
473
+ case "STREAM_ACCESSIBILITY" -> AudioManager.STREAM_ACCESSIBILITY;
474
+ case "STREAM_ALARM" -> AudioManager.STREAM_ALARM;
475
+ case "STREAM_DTMF" -> AudioManager.STREAM_DTMF;
476
+ case "STREAM_MUSIC" -> AudioManager.STREAM_MUSIC;
477
+ case "STREAM_NOTIFICATION" -> AudioManager.STREAM_NOTIFICATION;
478
+ case "STREAM_RING" -> AudioManager.STREAM_RING;
479
+ case "STREAM_SYSTEM" -> AudioManager.STREAM_SYSTEM;
480
+ case "STREAM_VOICE_CALL" -> AudioManager.STREAM_VOICE_CALL;
481
+ default -> AudioManager.USE_DEFAULT_STREAM_TYPE;
482
+ };
540
483
 
541
484
  tts.setAudioAttributes(audioAttributes);
542
485
 
543
- if (Build.VERSION.SDK_INT >= 21) {
544
- Bundle params = new Bundle();
545
- params.putInt(TextToSpeech.Engine.KEY_PARAM_STREAM, audioStreamType);
546
- params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, volume);
547
- params.putFloat(TextToSpeech.Engine.KEY_PARAM_PAN, pan);
548
- return tts.speak(utterance, TextToSpeech.QUEUE_ADD, params, utteranceId);
549
- } else {
550
- HashMap<String, String> params = new HashMap();
551
- params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId);
552
- params.put(TextToSpeech.Engine.KEY_PARAM_STREAM, String.valueOf(audioStreamType));
553
- params.put(TextToSpeech.Engine.KEY_PARAM_VOLUME, String.valueOf(volume));
554
- params.put(TextToSpeech.Engine.KEY_PARAM_PAN, String.valueOf(pan));
555
- return tts.speak(utterance, TextToSpeech.QUEUE_ADD, params);
556
- }
486
+ Bundle params = new Bundle();
487
+ params.putInt(TextToSpeech.Engine.KEY_PARAM_STREAM, audioStreamType);
488
+ params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, volume);
489
+ params.putFloat(TextToSpeech.Engine.KEY_PARAM_PAN, pan);
490
+
491
+ return tts.speak(utterance, TextToSpeech.QUEUE_ADD, params, utteranceId);
557
492
  }
558
493
 
559
494
  private void sendEvent(String eventName, String utteranceId) {
@@ -567,7 +502,7 @@ public class TextToSpeechModule extends ReactContextBaseJavaModule {
567
502
  .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
568
503
  .emit(eventName, params);
569
504
  }
570
-
505
+
571
506
  @ReactMethod
572
507
  public void removeListeners(Integer count) {
573
508
  // Keep: Required for RN built in Event Emitter Calls.
@@ -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,11 +152,11 @@ 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];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iternio/react-native-tts",
3
- "version": "4.1.3",
3
+ "version": "5.0.0",
4
4
  "description": "React Native Text-To-Speech module for Android and iOS",
5
5
  "main": "index.js",
6
6
  "repository": {