react-native-davoice 1.0.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.
- package/README.md +319 -0
- package/TTSRNBridge.podspec +38 -0
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build.gradle +47 -0
- package/android/libs/com/davoice/tts/1.0.0/tts-1.0.0.aar +0 -0
- package/android/libs/com/davoice/tts/1.0.0/tts-1.0.0.aar.md5 +1 -0
- package/android/libs/com/davoice/tts/1.0.0/tts-1.0.0.aar.sha1 +1 -0
- package/android/libs/com/davoice/tts/1.0.0/tts-1.0.0.pom +38 -0
- package/android/libs/com/davoice/tts/1.0.0/tts-1.0.0.pom.md5 +1 -0
- package/android/libs/com/davoice/tts/1.0.0/tts-1.0.0.pom.sha1 +1 -0
- package/android/settings.gradle +2 -0
- package/android/src/main/AndroidManifest.xml +14 -0
- package/android/src/main/java/com/davoice/rn/DaVoicePackage.java +29 -0
- package/android/src/main/java/com/davoice/stt/rn/STTModule.kt +208 -0
- package/android/src/main/java/com/davoice/tts/rn/DaVoiceTTSBridge.java +733 -0
- package/android/src/main/libs/MyLibrary-release.aar +0 -0
- package/app.plugin.js +60 -0
- package/ios/STTRNBridge/STTBridge.h +7 -0
- package/ios/STTRNBridge/STTBridge.m +130 -0
- package/ios/SpeechBridge/SpeechBridge.h +7 -0
- package/ios/SpeechBridge/SpeechBridge.m +761 -0
- package/ios/TTSRNBridge/DaVoiceTTSBridge.h +7 -0
- package/ios/TTSRNBridge/DaVoiceTTSBridge.m +177 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/Info.plist +44 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64/DavoiceTTS.framework/DavoiceTTS +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64/DavoiceTTS.framework/Headers/DavoiceTTS-Swift.h +424 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64/DavoiceTTS.framework/Info.plist +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/arm64-apple-ios.abi.json +13253 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/arm64-apple-ios.private.swiftinterface +213 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/arm64-apple-ios.swiftinterface +213 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64/DavoiceTTS.framework/Modules/module.modulemap +4 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/DavoiceTTS +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/Headers/DavoiceTTS-Swift.h +844 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/Info.plist +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/arm64-apple-ios-simulator.abi.json +13253 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +213 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/arm64-apple-ios-simulator.swiftinterface +213 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/x86_64-apple-ios-simulator.abi.json +13253 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +213 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/Modules/DavoiceTTS.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +213 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/Modules/module.modulemap +4 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/_CodeSignature/CodeRequirements +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/_CodeSignature/CodeRequirements-1 +0 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/_CodeSignature/CodeResources +282 -0
- package/ios/TTSRNBridge/DavoiceTTS.xcframework/ios-arm64_x86_64-simulator/DavoiceTTS.framework/_CodeSignature/CodeSignature +0 -0
- package/ios/TTSRNBridge/libphonemes.a +0 -0
- package/ios/TTSRNBridge/libucd.a +0 -0
- package/package.json +46 -0
- package/react-native.config.js +10 -0
- package/speech/index.ts +1055 -0
- package/stt/index.d.ts +54 -0
- package/stt/index.ts +222 -0
- package/tts/DaVoiceTTSBridge.d.ts +18 -0
- package/tts/DaVoiceTTSBridge.js +71 -0
- package/tts/index.d.ts +3 -0
- package/tts/index.js +4 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
// android/src/main/java/com/davoice/tts/rn/DaVoiceTTSBridge.java
|
|
2
|
+
package com.davoice.tts.rn;
|
|
3
|
+
|
|
4
|
+
import androidx.annotation.Nullable;
|
|
5
|
+
import com.facebook.react.bridge.Promise;
|
|
6
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
7
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
8
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
9
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
10
|
+
import com.facebook.react.bridge.WritableMap;
|
|
11
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
12
|
+
// ADD
|
|
13
|
+
import android.net.Uri;
|
|
14
|
+
import java.io.File;
|
|
15
|
+
import java.io.FileOutputStream;
|
|
16
|
+
import java.nio.ByteBuffer;
|
|
17
|
+
import java.nio.ByteOrder;
|
|
18
|
+
import java.util.Locale;
|
|
19
|
+
import android.util.Base64;
|
|
20
|
+
import android.media.MediaPlayer;
|
|
21
|
+
import android.content.Context;
|
|
22
|
+
|
|
23
|
+
import android.util.Log;
|
|
24
|
+
import android.content.res.AssetFileDescriptor;
|
|
25
|
+
import java.io.IOException;
|
|
26
|
+
import java.net.URL;
|
|
27
|
+
import java.net.URLConnection;
|
|
28
|
+
|
|
29
|
+
import com.davoice.tts.DaVoiceTTSInterface;
|
|
30
|
+
import com.davoice.tts.LicenseManager;
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
public class DaVoiceTTSBridge extends ReactContextBaseJavaModule {
|
|
34
|
+
|
|
35
|
+
private final DaVoiceTTSInterface tts;
|
|
36
|
+
private final ReactApplicationContext reactCtx;
|
|
37
|
+
final String TAG = "TTS";
|
|
38
|
+
|
|
39
|
+
public DaVoiceTTSBridge(ReactApplicationContext context) {
|
|
40
|
+
super(context);
|
|
41
|
+
this.reactCtx = context;
|
|
42
|
+
this.tts = new DaVoiceTTSInterface(context);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Override
|
|
46
|
+
public String getName() { return "DaVoiceTTSBridge"; }
|
|
47
|
+
|
|
48
|
+
private void sendEvent(String name, @Nullable WritableMap params) {
|
|
49
|
+
reactCtx.runOnUiQueueThread(() ->
|
|
50
|
+
reactCtx
|
|
51
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
|
52
|
+
.emit(name, params)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@ReactMethod
|
|
57
|
+
public void setLicense(String licenseKey, Promise promise) {
|
|
58
|
+
try {
|
|
59
|
+
if (licenseKey == null || licenseKey.trim().isEmpty()) {
|
|
60
|
+
promise.reject("invalid_args", "Missing licenseKey");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
promise.resolve(tts.setLicenseKey(licenseKey));
|
|
64
|
+
} catch (Exception e) {
|
|
65
|
+
promise.reject("LicenseError", e.getMessage(), e);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@ReactMethod
|
|
70
|
+
public void isLicenseValid(String licenseKey, Promise promise) {
|
|
71
|
+
try {
|
|
72
|
+
if (licenseKey == null || licenseKey.trim().isEmpty()) {
|
|
73
|
+
promise.reject("invalid_args", "Missing licenseKey");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
promise.resolve(LicenseManager.isLicenseValid(licenseKey));
|
|
77
|
+
} catch (Exception e) {
|
|
78
|
+
promise.reject("LicenseError", e.getMessage(), e);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@ReactMethod
|
|
83
|
+
public void initTTS(ReadableMap config, Promise promise) {
|
|
84
|
+
// DEBUG CODE
|
|
85
|
+
// try {
|
|
86
|
+
// final String fileName = config.getString("model");
|
|
87
|
+
// // Basic input debug
|
|
88
|
+
// Log.i(TAG, "ASSETDBG model raw input='" + fileName + "'");
|
|
89
|
+
// final String pkg = reactCtx.getPackageName();
|
|
90
|
+
// Log.i(TAG, "ASSETDBG packageName=" + pkg);
|
|
91
|
+
// // RN sometimes passes "assets_xxx" (no extension). Print normalized variants.
|
|
92
|
+
// final String[] candidates = new String[] {
|
|
93
|
+
// fileName,
|
|
94
|
+
// fileName + ".mp3",
|
|
95
|
+
// fileName + ".wav",
|
|
96
|
+
// fileName + ".dm",
|
|
97
|
+
// fileName + ".onnx",
|
|
98
|
+
// fileName + ".json",
|
|
99
|
+
// };
|
|
100
|
+
// Log.i(TAG, "ASSETDBG candidates=" + java.util.Arrays.toString(candidates));
|
|
101
|
+
|
|
102
|
+
// int res =
|
|
103
|
+
// reactCtx.getResources().getIdentifier(fileName, "raw", pkg);
|
|
104
|
+
|
|
105
|
+
// if (res != 0) {
|
|
106
|
+
// Log.i(TAG, "ASSETDBG res!=0" + res);
|
|
107
|
+
|
|
108
|
+
// android.content.res.AssetFileDescriptor afd = reactCtx.getResources().openRawResourceFd(res);
|
|
109
|
+
// Log.i(TAG, "ASSETDBG res!=0 afd == " + afd);
|
|
110
|
+
// afd.close();
|
|
111
|
+
// }
|
|
112
|
+
// else {
|
|
113
|
+
// Log.i(TAG, "ASSETDBG res==0" + res);
|
|
114
|
+
// }
|
|
115
|
+
|
|
116
|
+
// // Now the important part: res/raw identifier lookup (this is what RN-Sound relies on)
|
|
117
|
+
// for (String name : candidates) {
|
|
118
|
+
// try {
|
|
119
|
+
// int resId = reactCtx.getResources().getIdentifier(name, "raw", pkg);
|
|
120
|
+
// Log.i(TAG, "ASSETDBG getIdentifier(name='" + name + "', type='raw') => resId=" + resId);
|
|
121
|
+
|
|
122
|
+
// if (resId != 0) {
|
|
123
|
+
// // Print extra details about the resource
|
|
124
|
+
// try {
|
|
125
|
+
// String entry = reactCtx.getResources().getResourceEntryName(resId);
|
|
126
|
+
// String type = reactCtx.getResources().getResourceTypeName(resId);
|
|
127
|
+
// String full = reactCtx.getResources().getResourceName(resId);
|
|
128
|
+
// Log.i(TAG, "ASSETDBG resource: entry=" + entry + " type=" + type + " full=" + full);
|
|
129
|
+
// } catch (Exception ignored) {
|
|
130
|
+
// Log.i(TAG, "ASSETDBG resource name lookup failed (non-fatal) resId=" + resId);
|
|
131
|
+
// }
|
|
132
|
+
|
|
133
|
+
// // Try openRawResourceFd (works for non-compressed raw)
|
|
134
|
+
// try {
|
|
135
|
+
// android.content.res.AssetFileDescriptor afd =
|
|
136
|
+
// reactCtx.getResources().openRawResourceFd(resId);
|
|
137
|
+
|
|
138
|
+
// if (afd == null) {
|
|
139
|
+
// Log.w(TAG, "ASSETDBG openRawResourceFd returned NULL for resId=" + resId + " (likely compressed) -> will try openRawResource()");
|
|
140
|
+
// } else {
|
|
141
|
+
// Log.i(TAG, "ASSETDBG openRawResourceFd OK resId=" + resId
|
|
142
|
+
// + " startOffset=" + afd.getStartOffset()
|
|
143
|
+
// + " length=" + afd.getLength()
|
|
144
|
+
// + " fdValid=" + (afd.getFileDescriptor() != null));
|
|
145
|
+
// afd.close();
|
|
146
|
+
// }
|
|
147
|
+
// } catch (Exception e) {
|
|
148
|
+
// Log.w(TAG, "ASSETDBG openRawResourceFd FAILED resId=" + resId + " err=" + e.getMessage(), e);
|
|
149
|
+
// }
|
|
150
|
+
|
|
151
|
+
// // Always try openRawResource() too (works even if compressed)
|
|
152
|
+
// try (java.io.InputStream in = reactCtx.getResources().openRawResource(resId)) {
|
|
153
|
+
// byte[] head = new byte[32];
|
|
154
|
+
// int n = in.read(head);
|
|
155
|
+
// Log.i(TAG, "ASSETDBG openRawResource OK resId=" + resId + " firstBytesRead=" + n);
|
|
156
|
+
// } catch (Exception e) {
|
|
157
|
+
// Log.w(TAG, "ASSETDBG openRawResource FAILED resId=" + resId + " err=" + e.getMessage(), e);
|
|
158
|
+
// }
|
|
159
|
+
// }
|
|
160
|
+
// } catch (Exception e) {
|
|
161
|
+
// Log.w(TAG, "ASSETDBG getIdentifier loop exception name='" + name + "' err=" + e.getMessage(), e);
|
|
162
|
+
// }
|
|
163
|
+
// }
|
|
164
|
+
// } catch (Exception e) {
|
|
165
|
+
// Log.w(TAG, "ASSETDBG list assets failed: " + e.getMessage());
|
|
166
|
+
// }
|
|
167
|
+
try {
|
|
168
|
+
if (config == null || !config.hasKey("model") || config.isNull("model")) {
|
|
169
|
+
promise.reject("invalid_config", "Missing required 'model' key");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
final String modelPathOrURL = config.getString("model");
|
|
173
|
+
final String modelExt =
|
|
174
|
+
(config.hasKey("modelExt") && !config.isNull("modelExt"))
|
|
175
|
+
? config.getString("modelExt") : null;
|
|
176
|
+
|
|
177
|
+
// ✅ FIX: keep old behavior for plain asset-relative paths like "model.onnx" or "models/model.onnx"
|
|
178
|
+
// (i.e. files you manually put under android/app/src/main/assets or any merged asset pack).
|
|
179
|
+
final String s = (modelPathOrURL == null) ? "" : modelPathOrURL.trim();
|
|
180
|
+
if (s.isEmpty()) {
|
|
181
|
+
promise.reject("invalid_config", "Empty 'model'");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
final boolean looksLikePlainAssetPath =
|
|
186
|
+
!s.contains("://") &&
|
|
187
|
+
!s.startsWith("/") &&
|
|
188
|
+
!s.toLowerCase(Locale.US).startsWith("file:") &&
|
|
189
|
+
!s.toLowerCase(Locale.US).startsWith("content:") &&
|
|
190
|
+
!s.toLowerCase(Locale.US).startsWith("asset:") &&
|
|
191
|
+
// ✅ IMPORTANT: RN release resource names are NOT AssetManager assets (res/raw)
|
|
192
|
+
!s.startsWith("assets_") && !s.startsWith("src_assets_");
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
final String modelName;
|
|
196
|
+
if (looksLikePlainAssetPath) {
|
|
197
|
+
File f = resolvePlainModelToFile(s, modelExt != null ? modelExt : "onnx");
|
|
198
|
+
if (f != null) {
|
|
199
|
+
modelName = f.getAbsolutePath(); // pass a real filesystem path to the engine
|
|
200
|
+
} else {
|
|
201
|
+
// Last resort: keep old behavior (in case engine itself can open via AssetManager)
|
|
202
|
+
modelName = s;
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
// Use provided extension when RN gave us assets_* (no ext), otherwise fall back safely.
|
|
206
|
+
final String ext = (modelExt != null && !modelExt.isEmpty()) ? modelExt : "onnx";
|
|
207
|
+
final File modelFile = resolveToLocalFileForRead(s, ext);
|
|
208
|
+
if (modelFile == null || !modelFile.exists()) {
|
|
209
|
+
promise.reject("model_missing", "Model file missing/unresolvable: " + s);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
modelName = modelFile.getAbsolutePath();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// fire RN event when the last utterance finishes
|
|
216
|
+
tts.setOnFinishedSpeakingListenerJava(() -> sendEvent("onFinishedSpeaking", null));
|
|
217
|
+
|
|
218
|
+
DaVoiceTTSInterface.Config cfg = new DaVoiceTTSInterface.Config(
|
|
219
|
+
modelName,
|
|
220
|
+
"en-US",
|
|
221
|
+
"phonemes_dir"
|
|
222
|
+
);
|
|
223
|
+
boolean ok = tts.initTTS(cfg);
|
|
224
|
+
if (!ok) {
|
|
225
|
+
promise.reject("InitFailed", "Engine/TTS init failed");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
promise.resolve(true);
|
|
229
|
+
} catch (Exception e) {
|
|
230
|
+
promise.reject("InitError", e.getMessage(), e);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@ReactMethod
|
|
235
|
+
public void initTTSPlaybackOnly(Promise promise) {
|
|
236
|
+
try {
|
|
237
|
+
// Always wire completion events for JS queueing.
|
|
238
|
+
tts.setOnFinishedSpeakingListenerJava(() -> sendEvent("onFinishedSpeaking", null));
|
|
239
|
+
|
|
240
|
+
boolean ok = true;
|
|
241
|
+
// Keep backwards compatibility with older TTS AARs that don't have initPlaybackOnly().
|
|
242
|
+
try {
|
|
243
|
+
java.lang.reflect.Method m = tts.getClass().getMethod("initPlaybackOnly");
|
|
244
|
+
Object out = m.invoke(tts);
|
|
245
|
+
if (out instanceof Boolean) ok = (Boolean) out;
|
|
246
|
+
} catch (NoSuchMethodException noSuchMethod) {
|
|
247
|
+
Log.w(TAG, "initTTSPlaybackOnly: initPlaybackOnly() not found on TTS interface; using lazy playback fallback");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!ok) {
|
|
251
|
+
promise.reject("InitFailed", "Playback-only TTS init failed");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
promise.resolve(true);
|
|
255
|
+
} catch (Exception e) {
|
|
256
|
+
promise.reject("InitError", e.getMessage(), e);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private @Nullable File tryGetModelsPackAssetsPath(Context ctx) {
|
|
261
|
+
try {
|
|
262
|
+
Class<?> factoryClz = Class.forName("com.google.android.play.core.assetpacks.AssetPackManagerFactory");
|
|
263
|
+
java.lang.reflect.Method getInstance = factoryClz.getMethod("getInstance", Context.class);
|
|
264
|
+
Object apm = getInstance.invoke(null, ctx.getApplicationContext());
|
|
265
|
+
|
|
266
|
+
Class<?> apmClz = Class.forName("com.google.android.play.core.assetpacks.AssetPackManager");
|
|
267
|
+
java.lang.reflect.Method getPackLocation = apmClz.getMethod("getPackLocation", String.class);
|
|
268
|
+
Object loc = getPackLocation.invoke(apm, "models_pack");
|
|
269
|
+
if (loc == null) return null;
|
|
270
|
+
|
|
271
|
+
Class<?> locClz = Class.forName("com.google.android.play.core.assetpacks.AssetPackLocation");
|
|
272
|
+
java.lang.reflect.Method assetsPath = locClz.getMethod("assetsPath");
|
|
273
|
+
String path = (String) assetsPath.invoke(loc);
|
|
274
|
+
if (path == null) return null;
|
|
275
|
+
return new File(path);
|
|
276
|
+
} catch (Throwable t) {
|
|
277
|
+
// Play Core not on classpath or not installed -> ignore
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** models_pack → absolute file (if installed), else null */
|
|
283
|
+
@Nullable
|
|
284
|
+
private File getFileFromAssetPack(String relative) {
|
|
285
|
+
try {
|
|
286
|
+
// assetsPath is the root of the "assets" folder inside the models_pack
|
|
287
|
+
File assetsRoot = tryGetModelsPackAssetsPath(reactCtx);
|
|
288
|
+
if (assetsRoot == null) return null; // pack not present → caller will fall back
|
|
289
|
+
|
|
290
|
+
// Accept both "model.onnx" and "models/model.onnx"
|
|
291
|
+
String rel = relative.startsWith("models/") ? relative : ("models/" + relative);
|
|
292
|
+
File f = new File(assetsRoot, rel);
|
|
293
|
+
return f.exists() ? f : null;
|
|
294
|
+
} catch (Throwable t) {
|
|
295
|
+
Log.w(TAG, "getFileFromAssetPack failed: " + t.getMessage());
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** If in res/raw, copy to cache and return File; else null. */
|
|
301
|
+
@Nullable
|
|
302
|
+
private File tryResolveFromResRawToCache(String nameMaybeWithExt, @Nullable String defaultExt) {
|
|
303
|
+
// Accept "model.onnx" or "model"
|
|
304
|
+
String rawName = nameMaybeWithExt;
|
|
305
|
+
if (rawName == null) return null;
|
|
306
|
+
rawName = rawName.trim();
|
|
307
|
+
if (rawName.isEmpty()) return null;
|
|
308
|
+
int dot = rawName.lastIndexOf('.');
|
|
309
|
+
if (dot > 0) rawName = rawName.substring(0, dot);
|
|
310
|
+
return copyRawResourceToCache(rawName, defaultExt);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** For plain names, resolve to an absolute File using res/raw or asset-pack. */
|
|
314
|
+
@Nullable
|
|
315
|
+
private File resolvePlainModelToFile(String plain, @Nullable String defaultExt) {
|
|
316
|
+
// 1) res/raw (RN may map bundled assets there in release)
|
|
317
|
+
File fromRaw = tryResolveFromResRawToCache(plain, defaultExt);
|
|
318
|
+
if (fromRaw != null && fromRaw.exists()) return fromRaw;
|
|
319
|
+
|
|
320
|
+
// 2) fast-follow asset pack
|
|
321
|
+
File fromPack = getFileFromAssetPack(plain);
|
|
322
|
+
if (fromPack != null && fromPack.exists()) return fromPack;
|
|
323
|
+
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@ReactMethod
|
|
328
|
+
public void speak(String text, int speakerId, double speed, Promise promise) {
|
|
329
|
+
try {
|
|
330
|
+
float s = (float) speed;
|
|
331
|
+
if (!Float.isFinite(s) || s <= 0.0f) s = 1.0f;
|
|
332
|
+
|
|
333
|
+
// ✅ 3 args only
|
|
334
|
+
tts.speak(text, speakerId, s);
|
|
335
|
+
|
|
336
|
+
promise.resolve(null);
|
|
337
|
+
} catch (Exception e) {
|
|
338
|
+
promise.reject("SpeakError", e.getMessage(), e);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
private File copyRawResourceToCache(String rawName, @Nullable String defaultExt) {
|
|
342
|
+
try {
|
|
343
|
+
String pkg = reactCtx.getPackageName();
|
|
344
|
+
int resId = reactCtx.getResources().getIdentifier(rawName, "raw", pkg);
|
|
345
|
+
|
|
346
|
+
Log.d(TAG, "copyRawResourceToCache: rawName=" + rawName + " pkg=" + pkg + " resId=" + resId);
|
|
347
|
+
|
|
348
|
+
if (resId == 0) return null;
|
|
349
|
+
|
|
350
|
+
String ext = (defaultExt != null && !defaultExt.isEmpty()) ? defaultExt : "bin";
|
|
351
|
+
File out = new File(
|
|
352
|
+
reactCtx.getCacheDir(),
|
|
353
|
+
"rn_raw_" + rawName + "_" + System.currentTimeMillis() + "." + ext
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
try (java.io.InputStream in = reactCtx.getResources().openRawResource(resId);
|
|
357
|
+
FileOutputStream fos = new FileOutputStream(out)) {
|
|
358
|
+
byte[] buf = new byte[8192];
|
|
359
|
+
int len;
|
|
360
|
+
while ((len = in.read(buf)) != -1) fos.write(buf, 0, len);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
Log.d(TAG, "copyRawResourceToCache: wrote " + out.getAbsolutePath() + " size=" + out.length());
|
|
364
|
+
return out;
|
|
365
|
+
} catch (Exception e) {
|
|
366
|
+
Log.e(TAG, "copyRawResourceToCache FAILED rawName=" + rawName + " err=" + e.getMessage(), e);
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private File tryCopyRawToCacheIfExists(String name, @Nullable String defaultExt) {
|
|
372
|
+
if (name == null) return null;
|
|
373
|
+
final String s = name.trim();
|
|
374
|
+
if (s.isEmpty()) return null;
|
|
375
|
+
|
|
376
|
+
// Only reasonable resource entry names (no scheme, no slashes)
|
|
377
|
+
if (s.contains("://") || s.contains("/") || s.contains("\\") || s.startsWith(".")) return null;
|
|
378
|
+
|
|
379
|
+
// IMPORTANT: res/raw entry names have NO extension in Android resources
|
|
380
|
+
// If caller passed "foo.wav", strip extension for resource lookup.
|
|
381
|
+
String rawName = s;
|
|
382
|
+
int dot = rawName.lastIndexOf('.');
|
|
383
|
+
if (dot > 0) rawName = rawName.substring(0, dot);
|
|
384
|
+
|
|
385
|
+
File out = copyRawResourceToCache(rawName, defaultExt);
|
|
386
|
+
return (out != null && out.exists()) ? out : null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Resolve pathOrURL -> local readable File (supports http(s), asset:/, file://, plain path)
|
|
390
|
+
// defaultExt can be "wav", "onnx", etc (used for tmp file naming when extension missing)
|
|
391
|
+
private File resolveToLocalFileForRead(String pathOrURL, @Nullable String defaultExt) throws Exception {
|
|
392
|
+
if (pathOrURL == null) return null;
|
|
393
|
+
final String s = pathOrURL.trim();
|
|
394
|
+
if (s.isEmpty()) return null;
|
|
395
|
+
|
|
396
|
+
// 0) RN bundled assets on Android are usually in res/raw and resolve to a resource entry name
|
|
397
|
+
// like "assets_*" OR "src_assets_*" (no extension). Try res/raw first.
|
|
398
|
+
{
|
|
399
|
+
File out = tryCopyRawToCacheIfExists(s, defaultExt);
|
|
400
|
+
if (out != null) {
|
|
401
|
+
Log.i(TAG, "resolveToLocalFileForRead: copied res/raw '" + s + "' -> " + out.getAbsolutePath());
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 0) RN bundled assets resolve to res/raw names like "assets_cashregistersound" (NO extension).
|
|
407
|
+
// They are NOT APK /assets files. Handle exactly like react-native-sound: Resources.getIdentifier(..., "raw", ...)
|
|
408
|
+
if (s.startsWith("assets_")) {
|
|
409
|
+
File out = copyRawResourceToCache(s, defaultExt);
|
|
410
|
+
if (out != null && out.exists()) {
|
|
411
|
+
Log.i(TAG, "resolveToLocalFileForRead: copied res/raw '" + s + "' -> " + out.getAbsolutePath());
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
Log.w(TAG, "resolveToLocalFileForRead: res/raw lookup failed for '" + s + "'");
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
// 1) http(s) -> download into cache
|
|
418
|
+
if (isHttpOrHttps(s)) {
|
|
419
|
+
URL url = new URL(s);
|
|
420
|
+
String ext = guessExtFromUrl(s, defaultExt);
|
|
421
|
+
File out = new File(reactCtx.getCacheDir(), "rn_dl_" + System.currentTimeMillis() + (ext != null ? ("." + ext) : ""));
|
|
422
|
+
try (java.io.InputStream in = url.openStream();
|
|
423
|
+
FileOutputStream fos = new FileOutputStream(out)) {
|
|
424
|
+
byte[] buf = new byte[8192];
|
|
425
|
+
int len;
|
|
426
|
+
while ((len = in.read(buf)) != -1) fos.write(buf, 0, len);
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
String assetName = null;
|
|
431
|
+
|
|
432
|
+
// RN "asset:/" form
|
|
433
|
+
if (s.startsWith("asset:/") || s.startsWith("asset://")) {
|
|
434
|
+
assetName = s.replaceFirst("^asset:/+", ""); // handles asset:/ and asset://
|
|
435
|
+
} else if (s.startsWith("file:")) {
|
|
436
|
+
// Android asset URL forms
|
|
437
|
+
// - file:///android_asset/foo.wav => path "/android_asset/foo.wav", host null
|
|
438
|
+
// - file://android_asset/foo.wav => host "android_asset", path "/foo.wav"
|
|
439
|
+
Uri u = Uri.parse(s);
|
|
440
|
+
if (u != null) {
|
|
441
|
+
String host = u.getHost(); // "android_asset" in file://android_asset/...
|
|
442
|
+
String path = u.getPath(); // "/android_asset/..." OR "/foo.wav"
|
|
443
|
+
if (path != null) {
|
|
444
|
+
if ("android_asset".equalsIgnoreCase(host)) {
|
|
445
|
+
// file://android_asset/foo.wav => path "/foo.wav"
|
|
446
|
+
assetName = path.startsWith("/") ? path.substring(1) : path;
|
|
447
|
+
} else if (path.startsWith("/android_asset/")) {
|
|
448
|
+
// file:///android_asset/foo.wav => path "/android_asset/foo.wav"
|
|
449
|
+
assetName = path.substring("/android_asset/".length());
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (assetName != null) {
|
|
456
|
+
// normalize
|
|
457
|
+
assetName = Uri.decode(assetName);
|
|
458
|
+
while (assetName.startsWith("/")) assetName = assetName.substring(1);
|
|
459
|
+
|
|
460
|
+
String ext = guessExtFromName(assetName, defaultExt);
|
|
461
|
+
File out = new File(
|
|
462
|
+
reactCtx.getCacheDir(),
|
|
463
|
+
"rn_asset_" + System.currentTimeMillis() + (ext != null ? ("." + ext) : "")
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
try (java.io.InputStream in = reactCtx.getAssets().open(assetName);
|
|
467
|
+
FileOutputStream fos = new FileOutputStream(out)) {
|
|
468
|
+
byte[] buf = new byte[8192];
|
|
469
|
+
int len;
|
|
470
|
+
while ((len = in.read(buf)) != -1) fos.write(buf, 0, len);
|
|
471
|
+
}
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 3) file:// -> local file path
|
|
476
|
+
String path = s;
|
|
477
|
+
if (isFileUrl(s)) {
|
|
478
|
+
Uri u = Uri.parse(s);
|
|
479
|
+
path = (u != null) ? u.getPath() : null;
|
|
480
|
+
}
|
|
481
|
+
if (path == null) return null;
|
|
482
|
+
return new File(path);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private static String guessExtFromUrl(String url, @Nullable String defaultExt) {
|
|
486
|
+
try {
|
|
487
|
+
String p = Uri.parse(url).getPath();
|
|
488
|
+
return guessExtFromName(p, defaultExt);
|
|
489
|
+
} catch (Exception ignored) {
|
|
490
|
+
return defaultExt;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private static String guessExtFromName(@Nullable String name, @Nullable String defaultExt) {
|
|
495
|
+
if (name == null) return defaultExt;
|
|
496
|
+
int dot = name.lastIndexOf('.');
|
|
497
|
+
if (dot >= 0 && dot + 1 < name.length()) {
|
|
498
|
+
String ext = name.substring(dot + 1);
|
|
499
|
+
return ext.isEmpty() ? defaultExt : ext;
|
|
500
|
+
}
|
|
501
|
+
return defaultExt;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
@ReactMethod
|
|
505
|
+
public void stopSpeaking(Promise promise) {
|
|
506
|
+
try {
|
|
507
|
+
tts.stopSpeaking();
|
|
508
|
+
promise.resolve(null);
|
|
509
|
+
} catch (Exception e) {
|
|
510
|
+
promise.reject("StopError", e.getMessage(), e);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
@ReactMethod
|
|
515
|
+
public void destroy(Promise promise) {
|
|
516
|
+
try {
|
|
517
|
+
// stop & free native resources; after this, JS should create a fresh instance before use
|
|
518
|
+
tts.destroy();
|
|
519
|
+
promise.resolve(null);
|
|
520
|
+
} catch (Exception e) {
|
|
521
|
+
promise.reject("DestroyError", e.getMessage(), e);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// ADD
|
|
525
|
+
private static boolean isHttpOrHttps(String s) {
|
|
526
|
+
if (s == null) return false;
|
|
527
|
+
String ls = s.toLowerCase(Locale.US);
|
|
528
|
+
return ls.startsWith("http://") || ls.startsWith("https://");
|
|
529
|
+
}
|
|
530
|
+
// ADD
|
|
531
|
+
private static boolean isFileUrl(String s) {
|
|
532
|
+
if (s == null) return false;
|
|
533
|
+
String ls = s.toLowerCase(Locale.US);
|
|
534
|
+
return ls.startsWith("file://");
|
|
535
|
+
}
|
|
536
|
+
// ADD
|
|
537
|
+
private static byte[] b64(String base64) throws Exception {
|
|
538
|
+
return Base64.decode(base64, Base64.DEFAULT);
|
|
539
|
+
}
|
|
540
|
+
// ADD
|
|
541
|
+
private static byte[] intLE(int v) {
|
|
542
|
+
return new byte[] {
|
|
543
|
+
(byte)(v & 0xFF),
|
|
544
|
+
(byte)((v >> 8) & 0xFF),
|
|
545
|
+
(byte)((v >> 16) & 0xFF),
|
|
546
|
+
(byte)((v >> 24) & 0xFF)
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
// ADD
|
|
550
|
+
private static byte[] shortLE(short v) {
|
|
551
|
+
return new byte[] {
|
|
552
|
+
(byte)(v & 0xFF),
|
|
553
|
+
(byte)((v >> 8) & 0xFF)
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
// ADD
|
|
557
|
+
private static void writeWavF32(File out, float[] pcm, int sampleRate) throws Exception {
|
|
558
|
+
int numChannels = 1;
|
|
559
|
+
int bitsPerSample = 32;
|
|
560
|
+
int byteRate = sampleRate * numChannels * (bitsPerSample / 8);
|
|
561
|
+
int subchunk2Size = pcm.length * numChannels * (bitsPerSample / 8);
|
|
562
|
+
int chunkSize = 36 + subchunk2Size;
|
|
563
|
+
|
|
564
|
+
try (FileOutputStream fos = new FileOutputStream(out)) {
|
|
565
|
+
fos.write(new byte[] { 'R','I','F','F' });
|
|
566
|
+
fos.write(intLE(chunkSize));
|
|
567
|
+
fos.write(new byte[] { 'W','A','V','E' });
|
|
568
|
+
|
|
569
|
+
fos.write(new byte[] { 'f','m','t',' ' });
|
|
570
|
+
fos.write(intLE(16));
|
|
571
|
+
fos.write(shortLE((short) 3)); // IEEE float
|
|
572
|
+
fos.write(shortLE((short) numChannels));
|
|
573
|
+
fos.write(intLE(sampleRate));
|
|
574
|
+
fos.write(intLE(byteRate));
|
|
575
|
+
fos.write(shortLE((short) (numChannels * (bitsPerSample / 8))));
|
|
576
|
+
fos.write(shortLE((short) bitsPerSample));
|
|
577
|
+
|
|
578
|
+
fos.write(new byte[] { 'd','a','t','a' });
|
|
579
|
+
fos.write(intLE(subchunk2Size));
|
|
580
|
+
|
|
581
|
+
ByteBuffer bb = ByteBuffer.allocate(pcm.length * 4).order(ByteOrder.LITTLE_ENDIAN);
|
|
582
|
+
for (float v : pcm) bb.putFloat(v);
|
|
583
|
+
fos.write(bb.array());
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// ADD
|
|
587
|
+
private static void mixI16InterleavedToMonoF32(short[] src, int channels, float[] dst) {
|
|
588
|
+
int frames = dst.length, idx = 0;
|
|
589
|
+
for (int f = 0; f < frames; f++) {
|
|
590
|
+
int acc = 0;
|
|
591
|
+
for (int ch = 0; ch < channels; ch++) acc += src[idx++];
|
|
592
|
+
dst[f] = (acc / (float) channels) / 32768.0f;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
// ADD
|
|
596
|
+
private static void mixI16PlanarToMonoF32(short[] src, int frames, int channels, float[] dst) {
|
|
597
|
+
int plane = frames;
|
|
598
|
+
for (int f = 0; f < frames; f++) {
|
|
599
|
+
int acc = 0;
|
|
600
|
+
for (int ch = 0; ch < channels; ch++) acc += src[ch * plane + f];
|
|
601
|
+
dst[f] = (acc / (float) channels) / 32768.0f;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// ADD
|
|
605
|
+
private static void mixF32InterleavedToMono(float[] src, int channels, float[] dst) {
|
|
606
|
+
int frames = dst.length, idx = 0;
|
|
607
|
+
for (int f = 0; f < frames; f++) {
|
|
608
|
+
float acc = 0f;
|
|
609
|
+
for (int ch = 0; ch < channels; ch++) acc += src[idx++];
|
|
610
|
+
dst[f] = acc / channels;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// ADD
|
|
614
|
+
private static void mixF32PlanarToMono(float[] src, int frames, int channels, float[] dst) {
|
|
615
|
+
int plane = frames;
|
|
616
|
+
for (int f = 0; f < frames; f++) {
|
|
617
|
+
float acc = 0f;
|
|
618
|
+
for (int ch = 0; ch < channels; ch++) acc += src[ch * plane + f];
|
|
619
|
+
dst[f] = acc / channels;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
@ReactMethod
|
|
624
|
+
public void playWav(String pathOrURL, boolean markAsLast, Promise promise) {
|
|
625
|
+
final String TAG = "TTS";
|
|
626
|
+
Log.d(TAG, "playWav() called with: " + pathOrURL + " | markAsLast=" + markAsLast);
|
|
627
|
+
// 1️⃣ Handle RN dev server URLs (http://localhost:8081/...) by downloading them to cache first
|
|
628
|
+
try {
|
|
629
|
+
File f = resolveToLocalFileForRead(pathOrURL, "wav");
|
|
630
|
+
if (f == null) {
|
|
631
|
+
promise.reject("bad_path", "Empty/invalid pathOrURL");
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (!f.exists()) {
|
|
635
|
+
promise.reject("file_missing", "WAV file does not exist at path: " + f.getAbsolutePath());
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
Log.d(TAG, "Playing WAV from: " + f.getAbsolutePath());
|
|
639
|
+
tts.playWav(f, markAsLast);
|
|
640
|
+
promise.resolve("queued");
|
|
641
|
+
} catch (Exception e) {
|
|
642
|
+
Log.e(TAG, "PlayWavError: " + e.getMessage(), e);
|
|
643
|
+
promise.reject("PlayWavError", e.getMessage(), e);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
@ReactMethod
|
|
649
|
+
public void playBuffer(ReadableMap desc, Promise promise) {
|
|
650
|
+
try {
|
|
651
|
+
if (desc == null ||
|
|
652
|
+
!desc.hasKey("base64") || desc.isNull("base64") ||
|
|
653
|
+
!desc.hasKey("sampleRate") || desc.isNull("sampleRate") ||
|
|
654
|
+
!desc.hasKey("format") || desc.isNull("format")) {
|
|
655
|
+
promise.reject("invalid_args", "Missing one of base64/sampleRate/format");
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
final String base64 = desc.getString("base64");
|
|
659
|
+
final int sampleRate = desc.getInt("sampleRate");
|
|
660
|
+
final String format = desc.getString("format");
|
|
661
|
+
final int channels = desc.hasKey("channels") && !desc.isNull("channels") ? desc.getInt("channels") : 1;
|
|
662
|
+
final boolean interleaved = desc.hasKey("interleaved") && !desc.isNull("interleaved") ? desc.getBoolean("interleaved") : true;
|
|
663
|
+
final boolean markAsLast = desc.hasKey("markAsLast") && !desc.isNull("markAsLast") ? desc.getBoolean("markAsLast") : true;
|
|
664
|
+
if (sampleRate <= 0 || channels <= 0) {
|
|
665
|
+
promise.reject("bad_params", "sampleRate and channels must be > 0");
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
final byte[] raw = b64(base64);
|
|
670
|
+
if (raw == null || raw.length == 0) {
|
|
671
|
+
promise.reject("bad_base64", "Could not decode base64 payload");
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
float[] mono;
|
|
676
|
+
if ("i16".equalsIgnoreCase(format)) {
|
|
677
|
+
if (raw.length % 2 != 0) {
|
|
678
|
+
promise.reject("bad_buffer", "i16 payload length not multiple of 2");
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
int frames = (raw.length / 2) / Math.max(1, channels);
|
|
682
|
+
mono = new float[frames];
|
|
683
|
+
|
|
684
|
+
short[] s = new short[raw.length / 2];
|
|
685
|
+
ByteBuffer bb = ByteBuffer.wrap(raw).order(ByteOrder.LITTLE_ENDIAN);
|
|
686
|
+
for (int i = 0; i < s.length; i++) s[i] = bb.getShort();
|
|
687
|
+
|
|
688
|
+
if (channels == 1) {
|
|
689
|
+
for (int i = 0; i < frames; i++) mono[i] = s[i] / 32768.0f;
|
|
690
|
+
} else if (interleaved) {
|
|
691
|
+
mixI16InterleavedToMonoF32(s, channels, mono);
|
|
692
|
+
} else {
|
|
693
|
+
mixI16PlanarToMonoF32(s, frames, channels, mono);
|
|
694
|
+
}
|
|
695
|
+
} else if ("f32".equalsIgnoreCase(format)) {
|
|
696
|
+
if (raw.length % 4 != 0) {
|
|
697
|
+
promise.reject("bad_buffer", "f32 payload length not multiple of 4");
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
int totalFloats = raw.length / 4;
|
|
701
|
+
float[] f = new float[totalFloats];
|
|
702
|
+
ByteBuffer bb = ByteBuffer.wrap(raw).order(ByteOrder.LITTLE_ENDIAN);
|
|
703
|
+
for (int i = 0; i < totalFloats; i++) f[i] = bb.getFloat();
|
|
704
|
+
|
|
705
|
+
int frames = totalFloats / Math.max(1, channels);
|
|
706
|
+
mono = new float[frames];
|
|
707
|
+
|
|
708
|
+
if (channels == 1) {
|
|
709
|
+
System.arraycopy(f, 0, mono, 0, frames);
|
|
710
|
+
} else if (interleaved) {
|
|
711
|
+
mixF32InterleavedToMono(f, channels, mono);
|
|
712
|
+
} else {
|
|
713
|
+
mixF32PlanarToMono(f, frames, channels, mono);
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
promise.reject("bad_format", "format must be 'i16' or 'f32'");
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
File out = new File(reactCtx.getCacheDir(), "extbuf_" + System.currentTimeMillis() + ".wav");
|
|
721
|
+
writeWavF32(out, mono, sampleRate);
|
|
722
|
+
|
|
723
|
+
tts.playWav(out, markAsLast);
|
|
724
|
+
promise.resolve("queued");
|
|
725
|
+
} catch (Exception e) {
|
|
726
|
+
promise.reject("PlayBufferError", e.getMessage(), e);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// RN event API stubs (required by RN)
|
|
731
|
+
@ReactMethod public void addListener(String eventName) { /* no-op */ }
|
|
732
|
+
@ReactMethod public void removeListeners(double count) { /* no-op */ }
|
|
733
|
+
}
|