Skip to content

Commit 5a1f404

Browse files
committed
feat(android): adding getEngines and setEngine methods
1 parent 3d5ab9e commit 5a1f404

6 files changed

Lines changed: 118 additions & 6 deletions

File tree

android/src/main/java/com/speech/SpeechModule.kt

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,18 @@ class SpeechModule(reactContext: ReactApplicationContext) :
3838
private val queueLock = Any()
3939

4040
private val isSupportedPausing = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
41+
4142
private lateinit var synthesizer: TextToSpeech
43+
44+
private var selectedEngine: String? = null
45+
private var cachedEngines: List<TextToSpeech.EngineInfo>? = null
46+
4247
private var isInitialized = false
4348
private var isInitializing = false
4449
private val pendingOperations = mutableListOf<Pair<() -> Unit, Promise>>()
50+
4551
private var globalOptions: MutableMap<String, Any> = defaultOptions.toMutableMap()
52+
4653
private var isPaused = false
4754
private var isResuming = false
4855
private var currentQueueIndex = -1
@@ -112,11 +119,12 @@ class SpeechModule(reactContext: ReactApplicationContext) :
112119
if (isInitializing) return
113120
isInitializing = true
114121

115-
synthesizer = TextToSpeech(reactApplicationContext) { status ->
122+
synthesizer = TextToSpeech(reactApplicationContext, { status ->
116123
isInitialized = status == TextToSpeech.SUCCESS
117124
isInitializing = false
118125

119126
if (isInitialized) {
127+
cachedEngines = synthesizer.engines
120128
synthesizer.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
121129
override fun onStart(utteranceId: String) {
122130
synchronized(queueLock) {
@@ -187,7 +195,7 @@ class SpeechModule(reactContext: ReactApplicationContext) :
187195
} else {
188196
rejectPendingOperations()
189197
}
190-
}
198+
}, selectedEngine)
191199
}
192200

193201
private fun ensureInitialized(promise: Promise, operation: () -> Unit) {
@@ -223,7 +231,7 @@ class SpeechModule(reactContext: ReactApplicationContext) :
223231
globalOptions["voice"]?.let { voiceId ->
224232
synthesizer.voices?.forEach { voice ->
225233
if (voice.name == voiceId) {
226-
synthesizer.setVoice(voice)
234+
synthesizer.voice = voice
227235
return@forEach
228236
}
229237
}
@@ -247,7 +255,7 @@ class SpeechModule(reactContext: ReactApplicationContext) :
247255
tempOptions["voice"]?.let { voiceId ->
248256
synthesizer.voices?.forEach { voice ->
249257
if (voice.name == voiceId) {
250-
synthesizer.setVoice(voice)
258+
synthesizer.voice = voice
251259
return@forEach
252260
}
253261
}
@@ -443,12 +451,49 @@ class SpeechModule(reactContext: ReactApplicationContext) :
443451
}
444452
}
445453

454+
override fun getEngines(promise: Promise) {
455+
ensureInitialized(promise) {
456+
val enginesArray = Arguments.createArray()
457+
cachedEngines?.forEach { engine ->
458+
enginesArray.pushMap(
459+
Arguments.createMap().apply {
460+
putString("name", engine.name)
461+
putString("label", engine.label)
462+
putBoolean("isDefault", engine.name == synthesizer.defaultEngine)
463+
}
464+
)
465+
}
466+
promise.resolve(enginesArray)
467+
}
468+
}
469+
470+
override fun setEngine(engineName: String, promise: Promise) {
471+
if (cachedEngines?.any { it.name == engineName } == false) {
472+
promise.reject("engine_error", "Engine '$engineName' is not available")
473+
return
474+
}
475+
if (isInitialized) {
476+
val activeEngine = selectedEngine ?: synthesizer.defaultEngine
477+
if (activeEngine == engineName) {
478+
promise.resolve(null)
479+
return
480+
}
481+
}
482+
selectedEngine = engineName
483+
484+
invalidate()
485+
486+
pendingOperations.add(Pair({ promise.resolve(null) }, promise))
487+
initializeTTS()
488+
}
489+
446490
override fun invalidate() {
447491
super.invalidate()
448492
if (::synthesizer.isInitialized) {
449493
synthesizer.stop()
450494
synthesizer.shutdown()
451495
resetQueueState()
452496
}
497+
isInitialized = false
453498
}
454499
}

example/src/views/RootView.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ const RootView: React.FC = () => {
6969
// });
7070
// })();
7171

72+
// (async () => {
73+
// const engines = await Speech.getEngines();
74+
// if (engines?.[0]) {
75+
// await Speech.setEngine(engines[0].name);
76+
// }
77+
// })();
78+
7279
return () => {
7380
startSubscription.remove();
7481
finishSubscription.remove();

ios/Speech.mm

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,16 @@ - (void)getAvailableVoices:(NSString *)language
146146
resolve(voicesArray);
147147
}
148148

149+
- (void)getEngines:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
150+
resolve(@[]);
151+
}
152+
153+
- (void)setEngine:(NSString *)engineName
154+
resolve:(RCTPromiseResolveBlock)resolve
155+
reject:(RCTPromiseRejectBlock)reject {
156+
resolve(nil);
157+
}
158+
149159
- (void)isSpeaking:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
150160
BOOL speaking = self.synthesizer.isSpeaking;
151161
resolve(@(speaking));

jest/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ class Speech {
2020
static isSpeaking = mockPromiseBool;
2121
static speakWithOptions = mockPromise;
2222
static getAvailableVoices = mockPromiseArr;
23+
static getEngines = mockPromiseArr;
24+
static setEngine = mockPromise;
2325
static onError = eventMock;
2426
static onStart = eventMock;
2527
static onFinish = eventMock;

src/NativeSpeech.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,31 @@ export interface VoiceOptions {
5858
*/
5959
rate?: number;
6060
}
61-
61+
export interface EngineProps {
62+
/**
63+
* The unique system identifier for the engine.
64+
* This is typically the package name (e.g., "com.google.android.tts")
65+
*/
66+
name: string;
67+
/**
68+
* The human-readable display name for the engine (e.g., "Google Text-to-Speech Engine").
69+
*/
70+
label: string;
71+
/**
72+
* A boolean flag indicating if this is the default engine
73+
*/
74+
isDefault: boolean;
75+
}
6276
export interface Spec extends TurboModule {
6377
reset: () => void;
6478
stop: () => Promise<void>;
6579
pause: () => Promise<boolean>;
6680
resume: () => Promise<boolean>;
6781
isSpeaking: () => Promise<boolean>;
6882
speak: (text: string) => Promise<void>;
83+
getEngines: () => Promise<EngineProps[]>;
6984
initialize: (options: VoiceOptions) => void;
85+
setEngine: (engineName: string) => Promise<void>;
7086
getAvailableVoices: (language: string) => Promise<VoiceProps[]>;
7187
speakWithOptions: (text: string, options: VoiceOptions) => Promise<void>;
7288

src/Speech.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import TurboSpeech from './NativeSpeech';
2-
import type {VoiceProps, VoiceOptions} from './NativeSpeech';
2+
import type {VoiceProps, VoiceOptions, EngineProps} from './NativeSpeech';
33

44
export default class Speech {
55
/**
@@ -18,6 +18,38 @@ export default class Speech {
1818
public static getAvailableVoices(language?: string): Promise<VoiceProps[]> {
1919
return TurboSpeech.getAvailableVoices(language ?? '');
2020
}
21+
/**
22+
* Gets a list of all available text-to-speech engines on the device
23+
* @returns Promise<EngineProps[]> Array of engine properties including name, label, and isDefault flag
24+
* @platform android
25+
* @example
26+
* const engines = await Speech.getEngines();
27+
* engines.forEach(engine => {
28+
* console.log(`Engine: ${engine.label} (${engine.name})`);
29+
* if (engine.isDefault) {
30+
* console.log('This is the default engine');
31+
* }
32+
* });
33+
*/
34+
public static getEngines(): Promise<EngineProps[]> {
35+
return TurboSpeech.getEngines();
36+
}
37+
/**
38+
* Sets the text-to-speech engine to use for speech synthesis
39+
* @param engineName - The name of the engine to use (obtained from getEngines())
40+
* @returns Promise<void> Resolves when engine is set
41+
* @platform android
42+
* @example
43+
* // First, get available engines
44+
* const engines = await Speech.getEngines();
45+
* // Then set a specific engine
46+
* await Speech.setEngine(engines[0].name);
47+
* // Or set by known engine name
48+
* await Speech.setEngine('com.google.android.tts');
49+
*/
50+
public static setEngine(engineName: string): Promise<void> {
51+
return TurboSpeech.setEngine(engineName);
52+
}
2153
/**
2254
* Sets the global options for all subsequent speak() calls
2355
* @param options - Voice configuration options

0 commit comments

Comments
 (0)