@mentra/bluetooth-sdk 0.1.8 → 0.1.10

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.
Files changed (147) hide show
  1. package/README.md +116 -18
  2. package/android/build.gradle +289 -16
  3. package/android/gradle.properties.example +9 -0
  4. package/android/lc3Lib/build.gradle +95 -6
  5. package/android/settings.gradle +3 -0
  6. package/android/silero/LICENSE.md +17 -0
  7. package/android/silero/build.gradle +42 -0
  8. package/android/silero/proguard-rules.pro +24 -0
  9. package/android/silero/src/androidTest/assets/hello.wav +0 -0
  10. package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
  11. package/android/silero/src/main/assets/LICENSE +21 -0
  12. package/android/silero/src/main/assets/silero_vad.onnx +0 -0
  13. package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
  14. package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
  15. package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
  16. package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
  17. package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
  18. package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
  19. package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
  20. package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +6 -0
  22. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +165 -27
  23. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +89 -21
  24. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +189 -16
  25. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +22 -2
  26. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +787 -85
  27. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -2
  28. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +123 -10
  29. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +12 -2
  30. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +8 -2
  31. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +33 -17
  32. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +101 -0
  33. package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +8 -0
  34. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
  35. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +399 -74
  36. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +100 -17
  37. package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +17 -0
  38. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +2 -2
  39. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +569 -220
  40. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +2 -2
  41. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +271 -66
  42. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +176 -104
  43. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +12753 -27036
  44. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +54 -2
  45. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +16 -4
  46. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +60 -3
  47. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +12 -3
  48. package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +87 -36
  49. package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +13 -4
  50. package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
  51. package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +180 -0
  52. package/android/src/main/java/com/mentra/bluetoothsdk/utils/AvifExifStripper.java +573 -0
  53. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +488 -50
  54. package/android/src/main/java/com/mentra/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
  55. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +18 -1
  56. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +64 -18
  57. package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +48 -39
  58. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -0
  59. package/android/src/test/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
  60. package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +30 -0
  61. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +98 -0
  62. package/android/src/test/resources/avif_with_exif.avif +0 -0
  63. package/build/BluetoothSdk.types.d.ts +241 -43
  64. package/build/BluetoothSdk.types.d.ts.map +1 -1
  65. package/build/BluetoothSdk.types.js +11 -10
  66. package/build/BluetoothSdk.types.js.map +1 -1
  67. package/build/_private/BluetoothSdkModule.d.ts +36 -23
  68. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  69. package/build/_private/BluetoothSdkModule.js +30 -52
  70. package/build/_private/BluetoothSdkModule.js.map +1 -1
  71. package/build/_private/photoRequestPayload.d.ts +4 -0
  72. package/build/_private/photoRequestPayload.d.ts.map +1 -0
  73. package/build/_private/photoRequestPayload.js +25 -0
  74. package/build/_private/photoRequestPayload.js.map +1 -0
  75. package/build/index.d.ts +1 -1
  76. package/build/index.d.ts.map +1 -1
  77. package/build/index.js +23 -0
  78. package/build/index.js.map +1 -1
  79. package/build/react/useMentraBluetooth.d.ts +2 -2
  80. package/build/react/useMentraBluetooth.d.ts.map +1 -1
  81. package/build/react/useMentraBluetooth.js +3 -3
  82. package/build/react/useMentraBluetooth.js.map +1 -1
  83. package/ios/BluetoothSdkModule.swift +174 -70
  84. package/ios/LocalPhotoUploadServer.swift +370 -105
  85. package/ios/MentraBluetoothSDK.podspec +3 -1
  86. package/ios/MentraPhotoReceiverModule.swift +62 -10
  87. package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
  88. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +668 -26
  89. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +13 -9
  90. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
  91. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
  92. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
  93. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
  94. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
  95. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
  96. package/ios/Source/BluetoothSdkDefaults.swift +6 -0
  97. package/ios/Source/Bridge.swift +58 -19
  98. package/ios/Source/DeviceManager.swift +149 -17
  99. package/ios/Source/DeviceStore.swift +16 -6
  100. package/ios/Source/MentraBluetoothSDK.swift +803 -47
  101. package/ios/Source/ObservableStore.swift +8 -2
  102. package/ios/Source/{Audio → audio}/AudioModels.swift +9 -7
  103. package/ios/Source/{Camera → camera}/CameraModels.swift +225 -16
  104. package/ios/Source/controllers/ControllerManager.swift +4 -3
  105. package/ios/Source/controllers/R1.swift +3 -3
  106. package/ios/Source/{Events → events}/BluetoothEvents.swift +183 -1
  107. package/ios/Source/{Internal → internal}/ValueParsing.swift +10 -1
  108. package/ios/Source/{Requests → requests}/DisplayRequests.swift +28 -4
  109. package/ios/Source/services/PhoneMic.swift +1 -0
  110. package/ios/Source/sgcs/G1.swift +2 -2
  111. package/ios/Source/sgcs/G2.swift +645 -403
  112. package/ios/Source/sgcs/Mach1.swift +2 -2
  113. package/ios/Source/sgcs/MentraLive.swift +812 -319
  114. package/ios/Source/sgcs/MentraNex.swift +412 -182
  115. package/ios/Source/sgcs/SGCManager.swift +57 -3
  116. package/ios/Source/sgcs/Simulated.swift +7 -3
  117. package/ios/Source/sgcs/mentraos_ble.pb.swift +3073 -3134
  118. package/ios/Source/status/DeviceStatus.swift +974 -0
  119. package/ios/Source/{Status → status}/WifiHotspotStatus.swift +4 -4
  120. package/ios/Source/{Streaming → streaming}/StreamModels.swift +24 -9
  121. package/ios/Source/stt/STTTools.swift +27 -12
  122. package/ios/Source/stt/SherpaOnnxTranscriber.swift +31 -8
  123. package/ios/Source/tts/TTSTools.swift +164 -0
  124. package/ios/Source/utils/JSCExperiment.swift +7 -7
  125. package/ios/Source/utils/MessageChunkReassembler.swift +20 -1
  126. package/ios/Source/utils/MessageChunker.swift +78 -21
  127. package/ios/Source/utils/TarBz2Extractor.swift +53 -2
  128. package/package.json +1 -1
  129. package/plugin/build/withAndroid.js +68 -4
  130. package/src/BluetoothSdk.types.ts +331 -59
  131. package/src/_private/BluetoothSdkModule.ts +113 -102
  132. package/src/_private/photoRequestPayload.ts +28 -0
  133. package/src/index.ts +58 -15
  134. package/src/react/useMentraBluetooth.ts +6 -4
  135. package/android/.project +0 -28
  136. package/android/lc3Lib/.project +0 -28
  137. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +0 -162
  138. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +0 -1852
  139. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +0 -674
  140. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
  141. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
  142. package/ios/Source/Status/DeviceStatus.swift +0 -471
  143. /package/ios/Source/{Connection → connection}/ScanSession.swift +0 -0
  144. /package/ios/Source/{Errors → errors}/BluetoothError.swift +0 -0
  145. /package/ios/Source/{Internal → internal}/BluetoothAvailability.swift +0 -0
  146. /package/ios/Source/{Status → status}/RuntimeState.swift +0 -0
  147. /package/ios/Source/{Types → types}/DeviceModels.swift +0 -0
@@ -0,0 +1,449 @@
1
+ package com.konovalov.vad.silero
2
+
3
+ import ai.onnxruntime.OnnxTensor
4
+ import ai.onnxruntime.OrtEnvironment
5
+ import ai.onnxruntime.OrtException
6
+ import ai.onnxruntime.OrtSession
7
+ import ai.onnxruntime.OrtSession.SessionOptions
8
+ import android.content.Context
9
+ import com.konovalov.vad.silero.config.FrameSize
10
+ import com.konovalov.vad.silero.config.Mode
11
+ import com.konovalov.vad.silero.config.SampleRate
12
+ import com.konovalov.vad.silero.utils.AudioUtils.getFramesCount
13
+ import com.konovalov.vad.silero.utils.AudioUtils.toFloatArray
14
+ import com.konovalov.vad.silero.utils.TensorMap
15
+ import java.io.Closeable
16
+ import java.nio.FloatBuffer
17
+ import java.nio.LongBuffer
18
+ import kotlin.reflect.safeCast
19
+
20
+ /**
21
+ * Created by Georgiy Konovalov on 6/1/2023.
22
+ *
23
+ * The Silero VAD algorithm, based on DNN, analyzes the audio signal to determine whether it
24
+ * contains speech or non-speech segments. It offers higher accuracy in differentiating speech from
25
+ * background noise compared to the WebRTC VAD algorithm.
26
+ *
27
+ * The Silero VAD supports the following parameters:
28
+ *
29
+ * Sample Rates:
30
+ *
31
+ * 8000Hz,
32
+ * 16000Hz
33
+ *
34
+ * Frame Sizes (per sample rate):
35
+ *
36
+ * For 8000Hz: 80, 160, 240
37
+ * For 16000Hz: 160, 320, 480
38
+ *
39
+ * Mode:
40
+ *
41
+ * NORMAL,
42
+ * LOW_BITRATE,
43
+ * AGGRESSIVE,
44
+ * VERY_AGGRESSIVE
45
+ *
46
+ * Please note that the VAD class supports these specific combinations of sample
47
+ * rates and frame sizes, and the classifiers determine the aggressiveness of the voice
48
+ * activity detection algorithm.
49
+ *
50
+ * @param context is required for reading the model file from file system.
51
+ * @param sampleRate is required for processing audio input.
52
+ * @param frameSize is required for processing audio input.
53
+ * @param mode is required for the VAD model.
54
+ * @param speechDurationMs is minimum duration in milliseconds for speech segments (optional).
55
+ * @param silenceDurationMs is minimum duration in milliseconds for silence segments (optional).
56
+ */
57
+ class VadSilero(
58
+ context: Context,
59
+ sampleRate: SampleRate,
60
+ frameSize: FrameSize,
61
+ mode: Mode,
62
+ speechDurationMs: Int = 0,
63
+ silenceDurationMs: Int = 0
64
+ ) : Closeable {
65
+
66
+ /**
67
+ * Valid Sample Rates and Frame Sizes for Silero VAD DNN model.
68
+ */
69
+ var supportedParameters: Map<SampleRate, Set<FrameSize>> = mapOf(
70
+ SampleRate.SAMPLE_RATE_8K to setOf(
71
+ FrameSize.FRAME_SIZE_256,
72
+ FrameSize.FRAME_SIZE_512,
73
+ FrameSize.FRAME_SIZE_768
74
+ ),
75
+ SampleRate.SAMPLE_RATE_16K to setOf(
76
+ FrameSize.FRAME_SIZE_512,
77
+ FrameSize.FRAME_SIZE_1024,
78
+ FrameSize.FRAME_SIZE_1536
79
+ )
80
+ )
81
+ private set
82
+
83
+ private val env: OrtEnvironment
84
+ private val session: OrtSession
85
+ private var isInitiated: Boolean = false
86
+
87
+ private var h = FloatArray(128)
88
+ private var c = FloatArray(128)
89
+
90
+ private var speechFramesCount = 0
91
+ private var silenceFramesCount = 0
92
+ private var maxSpeechFramesCount = 0
93
+ private var maxSilenceFramesCount = 0
94
+
95
+ /**
96
+ * Determines if the provided audio data contains speech based on the inference result.
97
+ * The audio data is passed to the model for prediction. The result is obtained and compared
98
+ * with the threshold value to determine if it represents speech.
99
+ *
100
+ * @param audioData audio data to analyze.
101
+ * @return 'true' if speech is detected, 'false' otherwise.
102
+ */
103
+ fun isSpeech(audioData: ShortArray): Boolean {
104
+ return isContinuousSpeech(predict(toFloatArray(audioData)))
105
+ }
106
+
107
+ /**
108
+ * Determines if the provided audio data contains speech based on the inference result.
109
+ * The audio data is passed to the model for prediction. The result is obtained and compared
110
+ * with the threshold value to determine if it represents speech.
111
+ * Size of audio chunk for ByteArray should be 2x of Frame size.
112
+ *
113
+ * @param audioData audio data to analyze.
114
+ * @return 'true' if speech is detected, 'false' otherwise.
115
+ */
116
+ fun isSpeech(audioData: ByteArray): Boolean {
117
+ return isContinuousSpeech(predict(toFloatArray(audioData)))
118
+ }
119
+
120
+ /**
121
+ * Determines if the provided audio data contains speech based on the inference result.
122
+ * The audio data is passed to the model for prediction. The result is obtained and compared
123
+ * with the threshold value to determine if it represents speech.
124
+ *
125
+ * @param audioData audio data to analyze.
126
+ * @return 'true' if speech is detected, 'false' otherwise.
127
+ */
128
+ fun isSpeech(audioData: FloatArray): Boolean {
129
+ return isContinuousSpeech(predict(audioData))
130
+ }
131
+
132
+ /**
133
+ * This method designed to detect long utterances without returning false
134
+ * positive results when user makes pauses between sentences.
135
+ *
136
+ * @param isSpeech predicted frame result.
137
+ * @return 'true' if speech is detected, 'false' otherwise.
138
+ */
139
+ private fun isContinuousSpeech(isSpeech: Boolean): Boolean {
140
+ if (isSpeech) {
141
+ if (speechFramesCount <= maxSpeechFramesCount) speechFramesCount++
142
+
143
+ if (speechFramesCount > maxSpeechFramesCount) {
144
+ silenceFramesCount = 0
145
+ return true
146
+ }
147
+ } else {
148
+ if (silenceFramesCount <= maxSilenceFramesCount) silenceFramesCount++
149
+
150
+ if (silenceFramesCount > maxSilenceFramesCount) {
151
+ speechFramesCount = 0
152
+ return false
153
+ } else if (speechFramesCount > maxSpeechFramesCount) {
154
+ return true
155
+ }
156
+ }
157
+ return false
158
+ }
159
+
160
+ /**
161
+ * Determines if the provided audio data contains speech based on the inference result.
162
+ * The audio data is passed to the model for prediction. The result is extracted and compared
163
+ * with the threshold value to determine if it represents speech.
164
+ *
165
+ * @param audioData audio data to analyze.
166
+ * @return 'true' if speech is detected, 'false' otherwise.
167
+ */
168
+ private fun predict(audioData: FloatArray): Boolean {
169
+ checkState()
170
+
171
+ return createInputTensors(audioData).use { tensors ->
172
+ session.run(tensors).use { result ->
173
+ extractResult(result) > threshold()
174
+ }
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Retrieves and processes the output tensors to obtain the confidence value.
180
+ * The output tensor contains the confidence value, as well as the updated hidden state (H)
181
+ * and cell state (C) values. The H and C values are flattened and converted to float arrays
182
+ * for further processing.
183
+ *
184
+ * @param result output result of the inference session.
185
+ * @return confidence value.
186
+ */
187
+ private fun extractResult(result: OrtSession.Result): Float {
188
+ val confidence: Array<FloatArray>? = unpack(result, OutputTensors.OUTPUT)
189
+
190
+ flattenArray(unpack(result, OutputTensors.CN))?.let { c = it }
191
+ flattenArray(unpack(result, OutputTensors.HN))?.let { h = it }
192
+
193
+ return confidence?.getOrNull(0)?.getOrNull(0) ?: 0f
194
+ }
195
+
196
+ /**
197
+ * Unpacks the value of the specified tensor from an OrtSession.Result object
198
+ * and attempts to cast it to an array of the specified generic type {@code T}.
199
+ *
200
+ * @param output OrtSession.Result object from which to retrieve the value.
201
+ * @param index specifying the position from which to retrieve the value.
202
+ * @param <T> generic type to which the value should be cast.
203
+ * @return array of type {@code T} if the casting is successful,
204
+ * or {@code null} if an exception occurs
205
+ * or if the value cannot be cast to the specified type.
206
+ */
207
+ private inline fun <reified T> unpack(output: OrtSession.Result, index: Int): Array<T>? {
208
+ return try {
209
+ Array<T>::class.safeCast(output.get(index).value)
210
+ } catch (e: OrtException) {
211
+ null
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Flattens a multi-dimensional array of FloatArrays into a one-dimensional FloatArray.
217
+ *
218
+ * @param array multi-dimensional array to be flattened.
219
+ * @return flattened one-dimensional FloatArray if the input array is not null,
220
+ * or {@code null} if the input array is null.
221
+ */
222
+ private fun flattenArray(array: Array<Array<FloatArray>>?): FloatArray? {
223
+ return array?.flatten()?.flatMap { it.asIterable() }?.toFloatArray()
224
+ }
225
+
226
+ /**
227
+ * Creates and returns a map of input tensors for the given audio data, Sample Rate and Frame Size.
228
+ * The audio data is converted to a float array and wrapped in an OnnxTensor with the
229
+ * corresponding tensor shape. The sample rate, hidden state (H), and cell state (C) tensors
230
+ * are also created and added to the map.
231
+ *
232
+ * @param audioData audio data to analyze.
233
+ * @throws OrtException if there was an error in creating the tensors or getting the OrtEnvironment.
234
+ * @return map of input tensors as a TensorMap<String, OnnxTensor>.
235
+ */
236
+ private fun createInputTensors(audioData: FloatArray): TensorMap<String, OnnxTensor> {
237
+ return TensorMap<String, OnnxTensor>().apply {
238
+ InputTensors.INPUT to OnnxTensor.createTensor(
239
+ env,
240
+ FloatBuffer.wrap(audioData),
241
+ longArrayOf(1, frameSize.value.toLong())
242
+ )
243
+ InputTensors.SR to OnnxTensor.createTensor(
244
+ env,
245
+ LongBuffer.wrap(longArrayOf(sampleRate.value.toLong())),
246
+ longArrayOf(1)
247
+ )
248
+ InputTensors.H to OnnxTensor.createTensor(
249
+ env,
250
+ FloatBuffer.wrap(h),
251
+ longArrayOf(2, 1, 64)
252
+ )
253
+ InputTensors.C to OnnxTensor.createTensor(
254
+ env,
255
+ FloatBuffer.wrap(c),
256
+ longArrayOf(2, 1, 64)
257
+ )
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Retrieves the model data as a byte array from silero_vad.onnx.
263
+ *
264
+ * @param context android context.
265
+ * @return model data as a ByteArray.
266
+ */
267
+ private fun getModel(context: Context): ByteArray {
268
+ return context.assets.open("silero_vad.onnx").use { it.readBytes() }
269
+ }
270
+
271
+ /**
272
+ * Calculates and returns the threshold value based on the value of detection mode.
273
+ * The threshold value represents the confidence level required for VAD to make proper decision.
274
+ * ex. Mode.VERY_AGGRESSIVE requiring a very high prediction accuracy from the model.
275
+ *
276
+ * @return threshold Float value.
277
+ */
278
+ private fun threshold(): Float = when (mode) {
279
+ Mode.NORMAL -> 0.5f
280
+ Mode.AGGRESSIVE -> 0.8f
281
+ Mode.VERY_AGGRESSIVE -> 0.95f
282
+ else -> 0f
283
+ }
284
+
285
+ /**
286
+ * Set, retrieve and validate sample rate for Vad Model.
287
+ *
288
+ * Valid Sample Rates:
289
+ *
290
+ * 8000Hz,
291
+ * 16000Hz
292
+ *
293
+ * @param sampleRate is required for processing audio input.
294
+ * @throws IllegalArgumentException if there was invalid sample rate.
295
+ */
296
+ var sampleRate: SampleRate = sampleRate
297
+ set(sampleRate) {
298
+ require(supportedParameters.containsKey(sampleRate)) {
299
+ "VAD doesn't support Sample Rate:${sampleRate}!"
300
+ }
301
+ field = sampleRate
302
+ }
303
+
304
+ /**
305
+ * Set, retrieve and validate frame size for Vad Model.
306
+ *
307
+ * Valid Frame Sizes (per sample rate):
308
+ *
309
+ * For 8000Hz: 256, 512, 768
310
+ * For 16000Hz: 512, 1024, 1536
311
+ *
312
+ * @param frameSize is required for processing audio input.
313
+ * @throws IllegalArgumentException if there was invalid frame size.
314
+ */
315
+ var frameSize: FrameSize = frameSize
316
+ set(frameSize) {
317
+ require(supportedParameters[sampleRate]?.contains(frameSize) ?: false) {
318
+ "VAD doesn't support Sample rate:${sampleRate} and Frame Size:${frameSize}!"
319
+ }
320
+ field = frameSize
321
+ }
322
+
323
+ /**
324
+ * Set and retrieve detection mode for Vad model.
325
+ *
326
+ * Mode:
327
+ *
328
+ * NORMAL,
329
+ * LOW_BITRATE,
330
+ * AGGRESSIVE,
331
+ * VERY_AGGRESSIVE
332
+ *
333
+ * @param mode is required for the VAD model.
334
+ */
335
+ var mode: Mode = mode
336
+ set(mode) {
337
+ field = mode
338
+ }
339
+
340
+ /**
341
+ * Set, retrieve and validate speechDurationMs for Vad Model.
342
+ * The value of this parameter will define the necessary and sufficient duration of positive
343
+ * results to recognize result as speech. This parameter is optional.
344
+ *
345
+ * Permitted range (0ms >= speechDurationMs <= 300000ms).
346
+ *
347
+ * Parameters used for {@link VadSilero.isSpeech}.
348
+ *
349
+ * @param speechDurationMs speech duration ms.
350
+ * @throws IllegalArgumentException if out of permitted range.
351
+ */
352
+ var speechDurationMs: Int = speechDurationMs
353
+ set(speechDurationMs) {
354
+ require(speechDurationMs in 0..300000) {
355
+ "The parameter 'speechDurationMs' should 0ms >= speechDurationMs <= 300000ms!"
356
+ }
357
+
358
+ field = speechDurationMs
359
+ maxSpeechFramesCount = getFramesCount(sampleRate.value, frameSize.value, speechDurationMs)
360
+ }
361
+
362
+ /**
363
+ * Set, retrieve and validate silenceDurationMs for Vad Model.
364
+ * The value of this parameter will define the necessary and sufficient duration of
365
+ * negative results to recognize it as silence. This parameter is optional.
366
+ *
367
+ * Permitted range (0ms >= silenceDurationMs <= 300000ms).
368
+ *
369
+ * Parameters used in {@link VadSilero.isSpeech}.
370
+ *
371
+ * @param silenceDurationMs silence duration ms.
372
+ * @throws IllegalArgumentException if out of permitted range.
373
+ */
374
+ var silenceDurationMs: Int = silenceDurationMs
375
+ set(silenceDurationMs) {
376
+ require(silenceDurationMs in 0..300000) {
377
+ "The parameter 'silenceDurationMs' should be 0ms >= silenceDurationMs <= 300000ms!"
378
+ }
379
+
380
+ field = silenceDurationMs
381
+ maxSilenceFramesCount = getFramesCount(sampleRate.value, frameSize.value, silenceDurationMs)
382
+ }
383
+
384
+ /**
385
+ * Closes the ONNX Session and releases all associated resources.
386
+ * This method should be called when the VAD is no longer needed to free up system resources.
387
+ */
388
+ override fun close() {
389
+ checkState()
390
+ isInitiated = false
391
+
392
+ session.close()
393
+ env.close()
394
+ }
395
+
396
+ /**
397
+ * Check if VAD session already closed.
398
+ *
399
+ * @throws IllegalArgumentException if session already closed.
400
+ */
401
+ private fun checkState() {
402
+ require(isInitiated) { "You can't use Vad after closing session!" }
403
+ }
404
+
405
+ /**
406
+ * Constants representing the input tensor names used during model prediction.
407
+ */
408
+ private object InputTensors {
409
+ const val INPUT = "input"
410
+ const val SR = "sr"
411
+ const val H = "h"
412
+ const val C = "c"
413
+ }
414
+
415
+ /**
416
+ * Constants representing the output tensor names used when the model returns a result.
417
+ */
418
+ private object OutputTensors {
419
+ const val OUTPUT = 0
420
+ const val HN = 1
421
+ const val CN = 2
422
+ }
423
+
424
+ /**
425
+ * Initializes the ONNIX Runtime by creating a session with the provided
426
+ * model file and session options.
427
+ *
428
+ * @param context is required for accessing the model file.
429
+ * @throws IllegalArgumentException if invalid parameters have been set for the model.
430
+ * @throws OrtException if the model failed to parse, wasn't compatible or caused an error.
431
+ */
432
+ init {
433
+ this.sampleRate = sampleRate
434
+ this.frameSize = frameSize
435
+ this.mode = mode
436
+ this.silenceDurationMs = silenceDurationMs
437
+ this.speechDurationMs = speechDurationMs
438
+
439
+ val sessionOptions = SessionOptions().apply {
440
+ setIntraOpNumThreads(1)
441
+ setInterOpNumThreads(1)
442
+ setOptimizationLevel(SessionOptions.OptLevel.ALL_OPT)
443
+ }
444
+
445
+ this.env = OrtEnvironment.getEnvironment()
446
+ this.session = env.createSession(getModel(context), sessionOptions)
447
+ this.isInitiated = true
448
+ }
449
+ }
@@ -0,0 +1,15 @@
1
+ package com.konovalov.vad.silero.config
2
+
3
+ /**
4
+ * Created by Georgiy Konovalov on 6/1/2023.
5
+ *
6
+ * Enum class representing different Frame Sizes used in the VAD algorithm.
7
+ * @property value numeric value associated with the FrameSize.
8
+ */
9
+ enum class FrameSize(val value: Int) {
10
+ FRAME_SIZE_256(256),
11
+ FRAME_SIZE_512(512),
12
+ FRAME_SIZE_768(768),
13
+ FRAME_SIZE_1024(1024),
14
+ FRAME_SIZE_1536(1536);
15
+ }
@@ -0,0 +1,14 @@
1
+ package com.konovalov.vad.silero.config
2
+
3
+ /**
4
+ * Created by Georgiy Konovalov on 6/1/2023.
5
+ *
6
+ * Enum class representing different Modes used in the VAD algorithm.
7
+ * @property value numeric value associated with the Mode.
8
+ */
9
+ enum class Mode(val value: Int) {
10
+ OFF(0),
11
+ NORMAL(1),
12
+ AGGRESSIVE(2),
13
+ VERY_AGGRESSIVE(3);
14
+ }
@@ -0,0 +1,12 @@
1
+ package com.konovalov.vad.silero.config
2
+
3
+ /**
4
+ * Created by Georgiy Konovalov on 6/2/2023.
5
+ *
6
+ * Enum class representing different Sample Rates used in the VAD algorithm.
7
+ * @property value numeric value associated with the SampleRate.
8
+ */
9
+ enum class SampleRate(val value:Int) {
10
+ SAMPLE_RATE_8K(8000),
11
+ SAMPLE_RATE_16K(16000);
12
+ }
@@ -0,0 +1,47 @@
1
+ package com.konovalov.vad.silero.utils
2
+
3
+ /**
4
+ * Created by Georgiy Konovalov on 7/18/2023.
5
+ *
6
+ * AudioUtils is a utility class that provides various functions to work with audio data
7
+ * in different formats. It contains static methods that facilitate audio data conversions,
8
+ * processing, and manipulation. This class aims to simplify common audio-related tasks
9
+ * and ensure reusability of audio processing logic across the application.
10
+ */
11
+ object AudioUtils {
12
+
13
+ /**
14
+ * Convert audio data from ByteArray to FloatArray.
15
+ *
16
+ * @param audio is audio data for conversion.
17
+ * @return converted audio data as FloatArray.
18
+ */
19
+ fun toFloatArray(audio: ByteArray): FloatArray {
20
+ return FloatArray(audio.size / 2) { i ->
21
+ ((audio[2 * i].toInt() and 0xFF) or (audio[2 * i + 1].toInt() shl 8)) / 32767.0f
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Convert audio data from ShortArray to FloatArray.
27
+ *
28
+ * @param audio is audio data for conversion.
29
+ * @return converted audio data as FloatArray.
30
+ */
31
+ fun toFloatArray(audio: ShortArray): FloatArray {
32
+ return FloatArray(audio.size) { i ->
33
+ audio[i] / 32767.0f
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Calculates the frame count based on the duration in milliseconds,
39
+ * frequency and frame size.
40
+ *
41
+ * @param durationMs duration in milliseconds.
42
+ * @return frame count.
43
+ */
44
+ fun getFramesCount(sampleRate: Int, frameSize: Int, durationMs: Int): Int {
45
+ return durationMs / (frameSize / (sampleRate / 1000))
46
+ }
47
+ }
@@ -0,0 +1,41 @@
1
+ package com.konovalov.vad.silero.utils
2
+
3
+ import ai.onnxruntime.OnnxTensorLike
4
+ import java.io.Closeable
5
+
6
+ /**
7
+ * Created by Georgiy Konovalov on 1/25/2025.
8
+ *
9
+ * TensorMap is a utility class designed to manage a collection of tensors while
10
+ * ensuring proper resource cleanup. It extends `HashMap` to provide standard
11
+ * map functionality and implements `Closeable` to handle the lifecycle of
12
+ * the tensors within the map.
13
+ *
14
+ * This class is particularly useful in scenarios where tensors need to be
15
+ * systematically organized and safely closed to avoid resource leaks.
16
+ *
17
+ * @param K the type of keys in the map, representing identifiers for the tensors.
18
+ * @param V the type of tensor values in the map, restricted to `OnnxTensorLike` implementations.
19
+ */
20
+ class TensorMap<K, V : OnnxTensorLike> : LinkedHashMap<K, V>(), Closeable {
21
+
22
+ /**
23
+ * Infix function that allows for a more natural syntax when adding entries to the map.
24
+ * Instead of using put() or map[key] = value, you can write: key to tensor
25
+ *
26
+ * @param tensor The OnnxTensor to associate with the key
27
+ * @return The tensor that was just added
28
+ */
29
+ infix fun K.to(tensor: V): V {
30
+ this@TensorMap[this] = tensor
31
+ return tensor
32
+ }
33
+
34
+ /**
35
+ * Releases resources associated with all tensors in the map.
36
+ * Iterates through the map's values and calls the `close` method on each tensor.
37
+ */
38
+ override fun close() {
39
+ values.forEach { it.close() }
40
+ }
41
+ }
@@ -0,0 +1,44 @@
1
+ package com.konovalov.vad.silero.utils
2
+
3
+ import com.konovalov.vad.silero.utils.AudioUtils.getFramesCount
4
+ import com.konovalov.vad.silero.utils.AudioUtils.toFloatArray
5
+ import org.junit.Assert.assertArrayEquals
6
+ import org.junit.Assert.assertEquals
7
+ import org.junit.Test
8
+
9
+ /**
10
+ * Created by Georgiy Konovalov on 12/04/2023.
11
+ *
12
+ * Unit tests for AudioUtils class.
13
+ */
14
+ class AudioUtilsTest {
15
+
16
+ @Test
17
+ fun `convert ShortArray to FloatArray`() {
18
+ val input = shortArrayOf(100, 200, 300)
19
+ val expected = floatArrayOf(0.003051851f, 0.006103702f, 0.009155553f)
20
+ val result = toFloatArray(input)
21
+
22
+ assertArrayEquals(expected, result, 0f)
23
+ }
24
+
25
+ @Test
26
+ fun `convert ByteArray to FloatArray`() {
27
+ val input = byteArrayOf(10, 20, 30)
28
+ val expected = floatArrayOf(0.15655996f)
29
+ val result = toFloatArray(input)
30
+
31
+ assertArrayEquals(expected, result, 0f)
32
+ }
33
+
34
+ @Test
35
+ fun `getFramesCount calculates frame count correctly`() {
36
+ val sampleRate = 8000
37
+ val frameSize = 256
38
+ val durationMs = 300
39
+ val expected = 9
40
+ val result = getFramesCount(sampleRate, frameSize, durationMs)
41
+
42
+ assertEquals(expected, result)
43
+ }
44
+ }
@@ -0,0 +1,6 @@
1
+ package com.mentra.bluetoothsdk
2
+
3
+ /** Defaults for the public Bluetooth SDK surface. */
4
+ object BluetoothSdkDefaults {
5
+ const val VOICE_ACTIVITY_DETECTION_ENABLED = false
6
+ }