@gmessier/nitro-speech 0.4.0 → 0.4.1

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 (39) hide show
  1. package/README.md +23 -12
  2. package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/AutoStopper.kt +7 -7
  3. package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/HybridRecognizer.kt +29 -14
  4. package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/Logger.kt +16 -0
  5. package/android/src/main/java/com/margelo/nitro/nitrospeech/recognizer/RecognitionListenerSession.kt +11 -12
  6. package/ios/Audio/AudioLevelTracker.swift +16 -22
  7. package/ios/Engines/RecognizerEngine.swift +16 -13
  8. package/ios/HybridRecognizer.swift +8 -0
  9. package/ios/Shared/AutoStopper.swift +1 -1
  10. package/lib/Recognizer/RecognizerRef.d.ts +2 -0
  11. package/lib/Recognizer/RecognizerRef.js +4 -1
  12. package/lib/Recognizer/methods.d.ts +1 -0
  13. package/lib/Recognizer/methods.js +4 -0
  14. package/lib/Recognizer/types.d.ts +1 -1
  15. package/lib/Recognizer/useRecognizer.js +9 -9
  16. package/lib/Recognizer/useRecognizerIsActive.d.ts +25 -0
  17. package/lib/Recognizer/useRecognizerIsActive.js +40 -0
  18. package/lib/Recognizer/useVoiceInputVolume.d.ts +1 -1
  19. package/lib/index.d.ts +6 -5
  20. package/lib/index.js +6 -5
  21. package/lib/specs/Recognizer.nitro.d.ts +7 -5
  22. package/nitrogen/generated/android/c++/JHybridRecognizerSpec.cpp +5 -0
  23. package/nitrogen/generated/android/c++/JHybridRecognizerSpec.hpp +1 -0
  24. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrospeech/HybridRecognizerSpec.kt +4 -0
  25. package/nitrogen/generated/ios/NitroSpeech-Swift-Cxx-Bridge.hpp +9 -0
  26. package/nitrogen/generated/ios/c++/HybridRecognizerSpecSwift.hpp +8 -0
  27. package/nitrogen/generated/ios/swift/HybridRecognizerSpec.swift +1 -0
  28. package/nitrogen/generated/ios/swift/HybridRecognizerSpec_cxx.swift +12 -0
  29. package/nitrogen/generated/shared/c++/HybridRecognizerSpec.cpp +1 -0
  30. package/nitrogen/generated/shared/c++/HybridRecognizerSpec.hpp +1 -0
  31. package/package.json +1 -1
  32. package/src/Recognizer/RecognizerRef.ts +4 -0
  33. package/src/Recognizer/methods.ts +5 -0
  34. package/src/Recognizer/types.ts +1 -0
  35. package/src/Recognizer/useRecognizer.ts +9 -7
  36. package/src/Recognizer/useRecognizerIsActive.ts +49 -0
  37. package/src/Recognizer/useVoiceInputVolume.ts +1 -1
  38. package/src/index.ts +12 -5
  39. package/src/specs/Recognizer.nitro.ts +8 -5
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  #### Key Features:
15
15
 
16
- - ⚡ Built on Nitro Modules for zero-overhead native bridging
16
+ - ⚡ Built with Nitro Modules for low-overhead native binding
17
17
  - 🌎 Supports 60+ languages
18
18
  - 🍎 The only library that uses new `SpeechAnalyzer` with `SpeechTranscriber` or `DictationTranscriber` API for iOS 26+ (with fallback to legacy `SFSpeechRecognition` for older versions)
19
19
  - ⏱️ Timer for silence
@@ -21,16 +21,16 @@
21
21
  - Callback `onAutoFinishProgress` fires periodically with interval
22
22
  - Configurable interval `autoFinishProgressIntervalMs` value (default: 1 sec)
23
23
  - Method `updateConfig` with `autoFinishRecognitionMs` and `autoFinishProgressIntervalMs`
24
- allows to change value on the fly
24
+ allows changing the value on the fly
25
25
  - Method `resetAutoFinishTime` resets the Timer to the threshold
26
26
  - Method `addAutoFinishTime` adds ms once without changing threshold
27
27
  - Configurable volume-based sensitivity `resetAutoFinishVoiceSensitivity` for the timer from 0 to 1
28
28
  - 🎤 Rich user voice input management
29
- - Hook `useVoiceInputVolume()` for `raw` or `smoothed` normalized
30
- volume level from 0 to 1 -> easy to use for UI animations;
29
+ - Hook `useVoiceInputVolume()` for `raw` or `smoothed` normalized volume level from 0 to 1 -> easy to use for UI animations;
31
30
  And `db` as human-friendly value
32
31
  - Flexible callback `onVolumeChange` for custom behavior
33
- - 🧩 Lifecycle methods: `prewarm` | `updateConfig` | `getIsActive` | `getSupportedLocalesIOS`
32
+ - Static method `getVoiceInputVolume()`
33
+ - 🧩 Lifecycle methods: `prewarm` | `updateConfig` | `getIsActive`
34
34
  - 👆 Configurable Haptic Feedback on start and finish
35
35
  - 🎚️ Speech-quality configurations:
36
36
  - Result is grouped by speech segments into Batches.
@@ -53,6 +53,7 @@
53
53
  - [Cross-component control: RecognizerRef](#cross-component-control-recognizerref)
54
54
  - [Multithreading (react-native-worklets)](#multithreading-react-native-worklets)
55
55
  - [Voice input volume](#voice-input-volume)
56
+ - [useRecognizerIsActive](#userecognizerisactive)
56
57
  - [Unsafe: SpeechRecognizer](#unsafe-speechrecognizer)
57
58
  - [Requirements](#requirements)
58
59
  - [Compatibility](#compatibility)
@@ -123,11 +124,11 @@ Both permissions are required for speech recognition to work on iOS.
123
124
  | **Auto-finish progress** | Callback `onAutoFinishProgress` with countdown until auto-stop | ✅ | ✅ |
124
125
  | **Add Auto-finish Time** | Adds time to the auto finish timer once without changing the timer threshold | ✅ | ✅ |
125
126
  | **Reset Auto-finish Time** | Resets the Timer to the threshold | ✅ | ✅ |
126
- | **Voice input volume** | Hook `useVoiceInputVolume` and `onVolumeChange` callback | ✅ | ✅ |
127
+ | **Voice input volume** | `useVoiceInputVolume`, `getVoiceInputVolume()`, `onVolumeChange` | ✅ | ✅ |
127
128
  | **Reset Auto-finish Sensitivity** | The voice detector sensitivity to reset the Auto-finish time | ✅ | ✅ |
128
129
  | **Prewarm** | Prepares resources, downloads assets, confirms locale availability | ✅ | ✅ |
129
- | **Update config** | Static method `updateConfig` allows update config on the fly | ✅ | ✅ |
130
- | **isActive** | Static method `getIsActive()` | ✅ | ✅ |
130
+ | **Update config** | Static method `updateConfig` allows updating the config on the fly | ✅ | ✅ |
131
+ | **Is Active** | Static method `getIsActive()` | ✅ | ✅ |
131
132
  | **Haptic feedback** | Haptic feedback on recording start/stop | ✅ | ✅ |
132
133
  | **Permission handling** | Dedicated `onPermissionDenied` callback | ✅ | ✅ |
133
134
  | **Background handling** | Stop when app loses focus/goes to background | ✅ | ✅ |
@@ -162,6 +163,7 @@ function MyComponent() {
162
163
  updateConfig,
163
164
  getSupportedLocalesIOS,
164
165
  getIsActive,
166
+ getVoiceInputVolume,
165
167
  } = useRecognizer({
166
168
  onReadyForSpeech: () => {
167
169
  console.log('Listening...');
@@ -271,6 +273,7 @@ RecognizerRef.updateConfig(
271
273
  true
272
274
  );
273
275
  RecognizerRef.getIsActive();
276
+ RecognizerRef.getVoiceInputVolume();
274
277
  RecognizerRef.stopListening();
275
278
  // iOS only
276
279
  RecognizerRef.getSupportedLocalesIOS();
@@ -321,8 +324,6 @@ function VoiceMeter() {
321
324
  As a better alternative you can control volume via SharedValue and apply it only on UI thread with Reanimated.
322
325
  This way you will avoid re-renders since the volume will be stored on UI thread
323
326
 
324
- Warning: this approach will disable the built-in `useVoiceInputVolume` hook.
325
-
326
327
  ```typescript
327
328
  function VoiceMeter() {
328
329
  const sharedVolume = useSharedValue(0)
@@ -341,6 +342,16 @@ function VoiceMeter() {
341
342
  }
342
343
  ```
343
344
 
345
+ ### useRecognizerIsActive
346
+
347
+ ```typescript
348
+ import { useRecognizerIsActive } from '@gmessier/nitro-speech';
349
+
350
+ function MyComponent() {
351
+ const isActive = useRecognizerIsActive();
352
+ return <Text>{isActive ? 'Listening...' : 'Not listening'}</Text>;
353
+ }
354
+ ```
344
355
 
345
356
  ### Unsafe: SpeechRecognizer
346
357
 
@@ -415,7 +426,7 @@ The `SpeechRecognizer.dispose()` method is **NOT SAFE** and should rarely be use
415
426
 
416
427
  ## Compatibility
417
428
 
418
- Latest versions of `@gmessier/nitro-speech` requires [react-native-nitro-modules 0.35.0 or higher](https://github.com/mrousavy/nitro/releases/tag/v0.35.0).
429
+ Latest versions of `@gmessier/nitro-speech` require [react-native-nitro-modules 0.35.0 or higher](https://github.com/mrousavy/nitro/releases/tag/v0.35.0).
419
430
 
420
431
 
421
432
  | Compatibility | Supported versions |
@@ -427,7 +438,7 @@ Latest versions of `@gmessier/nitro-speech` requires [react-native-nitro-modules
427
438
 
428
439
  ### Android Gradle sync issues
429
440
 
430
- If you're having issues with Android Gradle sync, try running the prebuild for the library, that causes the issue:
441
+ If you're having issues with Android Gradle sync, try running the prebuild for the library that causes the issue:
431
442
 
432
443
  e.g. failed in `react-native-nitro-modules`:
433
444
 
@@ -2,7 +2,6 @@ package com.margelo.nitro.nitrospeech.recognizer
2
2
 
3
3
  import android.os.Handler
4
4
  import android.os.Looper
5
- import android.util.Log
6
5
  import kotlin.math.max
7
6
 
8
7
  class AutoStopper(
@@ -12,12 +11,13 @@ class AutoStopper(
12
11
  val onTimeout: () -> Unit,
13
12
  ) {
14
13
  companion object {
15
- private const val TAG = "HybridRecognizer"
16
14
  private const val DEFAULT_SILENCE_THRESHOLD_MS = 8000.0
17
15
  private const val DEFAULT_PROGRESS_INTERVAL_MS = 1000.0
18
16
  private const val MIN_PROGRESS_INTERVAL_MS = 50.0
19
17
  }
20
18
 
19
+ private val logger = Logger(disable = false)
20
+
21
21
  private var silenceThresholdMs: Double = clampMs(silenceThresholdMs ?: DEFAULT_SILENCE_THRESHOLD_MS)
22
22
  private var progressIntervalMs: Double = clampMs(progressIntervalMs ?: DEFAULT_PROGRESS_INTERVAL_MS)
23
23
 
@@ -31,7 +31,7 @@ class AutoStopper(
31
31
  private val tickRunnable = Runnable { tick() }
32
32
 
33
33
  fun resetTimer() {
34
- Log.d(TAG, "resetTimer | isStopped: $isStopped | ms: ${System.currentTimeMillis()}")
34
+ logger.log("resetTimer | isStopped: $isStopped | ms: ${System.currentTimeMillis()}")
35
35
  handler.removeCallbacks(tickRunnable)
36
36
  isTimerScheduled = false
37
37
  if (isStopped) return
@@ -55,7 +55,7 @@ class AutoStopper(
55
55
 
56
56
  fun addMsOnce(extraMs: Double) {
57
57
  if (isStopped || !extraMs.isFinite()) return
58
- Log.d(TAG, "addMsOnce | extraMs: $extraMs")
58
+ logger.log("addMsOnce | extraMs: $extraMs")
59
59
  timeLeftMs += extraMs
60
60
  didTimeout = false
61
61
  if (timeLeftMs > 0 && isTimerScheduled) {
@@ -65,7 +65,7 @@ class AutoStopper(
65
65
 
66
66
  fun updateProgressInterval(newIntervalMs: Double) {
67
67
  if (isStopped) return
68
- Log.d(TAG, "updateProgressInterval | newIntervalMs: $newIntervalMs")
68
+ logger.log("updateProgressInterval | newIntervalMs: $newIntervalMs")
69
69
  progressIntervalMs = clampMs(newIntervalMs)
70
70
  if (isTimerScheduled) {
71
71
  scheduleNextTickLocked()
@@ -83,7 +83,7 @@ class AutoStopper(
83
83
  if (isStopped || didTimeout) return
84
84
  timeLeftMs -= progressIntervalMs
85
85
  if (timeLeftMs > 0) {
86
- Log.d(TAG, "onProgress | timeLeftMs: $timeLeftMs")
86
+ logger.log("onProgress | timeLeftMs: $timeLeftMs")
87
87
  onProgress(timeLeftMs)
88
88
  scheduleNextTickLocked()
89
89
  return
@@ -92,7 +92,7 @@ class AutoStopper(
92
92
  didTimeout = true
93
93
  handler.removeCallbacks(tickRunnable)
94
94
  isTimerScheduled = false
95
- Log.d(TAG, "onTimeout | ms: ${System.currentTimeMillis()}")
95
+ logger.log("onTimeout | ms: ${System.currentTimeMillis()}")
96
96
  onTimeout()
97
97
  }
98
98
 
@@ -7,7 +7,6 @@ import android.os.Handler
7
7
  import android.os.Looper
8
8
  import android.speech.RecognizerIntent
9
9
  import android.speech.SpeechRecognizer
10
- import android.util.Log
11
10
  import androidx.annotation.Keep
12
11
  import com.facebook.proguard.annotations.DoNotStrip
13
12
  import com.margelo.nitro.NitroModules
@@ -21,12 +20,14 @@ import com.margelo.nitro.nitrospeech.VolumeChangeEvent
21
20
  @Keep
22
21
  class HybridRecognizer: HybridRecognizerSpec() {
23
22
  companion object {
24
- private const val TAG = "HybridRecognizer"
25
23
  private const val POST_RECOGNITION_DELAY = 250L
26
24
  }
27
25
 
26
+ private val logger = Logger(disable = false)
27
+
28
28
  private var isActive: Boolean = false
29
29
  private var config: SpeechRecognitionConfig? = null
30
+ private var volumeChangeEvent: VolumeChangeEvent = VolumeChangeEvent(0.0,0.0,null)
30
31
  private var autoStopper: AutoStopper? = null
31
32
  private var speechRecognizer: SpeechRecognizer? = null
32
33
  private val mainHandler = Handler(Looper.getMainLooper())
@@ -51,7 +52,7 @@ class HybridRecognizer: HybridRecognizerSpec() {
51
52
  @DoNotStrip
52
53
  @Keep
53
54
  override fun startListening(params: SpeechRecognitionConfig?) {
54
- Log.d(TAG, "startListening: $params")
55
+ logger.log("startListening: $params")
55
56
  if (isActive) {
56
57
  onFinishRecognition(
57
58
  null,
@@ -94,7 +95,7 @@ class HybridRecognizer: HybridRecognizerSpec() {
94
95
  @DoNotStrip
95
96
  @Keep
96
97
  override fun stopListening() {
97
- Log.d(TAG, "stopListening called")
98
+ logger.log("stopListening called")
98
99
  if (!isActive) return
99
100
  onFinishRecognition(null, null, true)
100
101
  mainHandler.postDelayed({
@@ -117,7 +118,7 @@ class HybridRecognizer: HybridRecognizerSpec() {
117
118
  @DoNotStrip
118
119
  @Keep
119
120
  override fun addAutoFinishTime(additionalTimeMs: Double?) {
120
- Log.d(TAG, "addAutoFinishTime")
121
+ logger.log("addAutoFinishTime")
121
122
  if (!isActive) return
122
123
 
123
124
  if (additionalTimeMs != null) {
@@ -134,7 +135,7 @@ class HybridRecognizer: HybridRecognizerSpec() {
134
135
  newConfig: MutableSpeechRecognitionConfig?,
135
136
  resetAutoFinishTime: Boolean?
136
137
  ) {
137
- Log.d(TAG, "updateConfig $newConfig",)
138
+ logger.log("updateConfig $newConfig",)
138
139
  if (!isActive) return
139
140
 
140
141
  val newTimeMs = if (newConfig?.autoFinishRecognitionMs != null) newConfig.autoFinishRecognitionMs else config?.autoFinishRecognitionMs
@@ -177,6 +178,12 @@ class HybridRecognizer: HybridRecognizerSpec() {
177
178
  return isActive
178
179
  }
179
180
 
181
+ @DoNotStrip
182
+ @Keep
183
+ override fun getVoiceInputVolume(): VolumeChangeEvent {
184
+ return volumeChangeEvent
185
+ }
186
+
180
187
  @DoNotStrip
181
188
  @Keep
182
189
  override fun getSupportedLocalesIOS(): Array<String> {
@@ -204,12 +211,14 @@ class HybridRecognizer: HybridRecognizerSpec() {
204
211
  }
205
212
  )
206
213
  val recognitionListenerSession = RecognitionListenerSession(
207
- autoStopper,
208
- config,
209
- onVolumeChange
210
- ) { result: ArrayList<String>?, errorMessage: String?, recordingStopped: Boolean ->
211
- onFinishRecognition(result, errorMessage, recordingStopped)
212
- }
214
+ autoStopper,
215
+ config,
216
+ fireVolumeChangeEvent = { event -> fireVolumeChangeEvent(event) },
217
+ onFinishRecognition = { result, errorMessage, recordingStopped ->
218
+ onFinishRecognition(result, errorMessage, recordingStopped)
219
+ }
220
+ )
221
+
213
222
  speechRecognizer?.setRecognitionListener(recognitionListenerSession.createRecognitionListener())
214
223
 
215
224
  val languageModel = if (config?.androidUseWebSearchModel == true) RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH else RecognizerIntent.LANGUAGE_MODEL_FREE_FORM
@@ -262,7 +271,7 @@ class HybridRecognizer: HybridRecognizerSpec() {
262
271
 
263
272
  private fun cleanup() {
264
273
  try {
265
- Log.d(TAG, "cleanup called")
274
+ logger.log("cleanup called")
266
275
  autoStopper?.stop()
267
276
  autoStopper = null
268
277
  speechRecognizer?.stopListening()
@@ -270,7 +279,7 @@ class HybridRecognizer: HybridRecognizerSpec() {
270
279
  speechRecognizer = null
271
280
  isActive = false
272
281
  // Reset voice meter in JS consumers after stop/error cleanup.
273
- onVolumeChange?.invoke(VolumeChangeEvent(0.0,0.0,null))
282
+ fireVolumeChangeEvent(VolumeChangeEvent(0.0,0.0,null))
274
283
  } catch (e: Exception) {
275
284
  onFinishRecognition(
276
285
  null,
@@ -291,4 +300,10 @@ class HybridRecognizer: HybridRecognizerSpec() {
291
300
  onResult?.invoke(result.toTypedArray())
292
301
  }
293
302
  }
303
+
304
+ private fun fireVolumeChangeEvent(event: VolumeChangeEvent) {
305
+ logger.log("fireVolumeChangeEvent ${event}")
306
+ volumeChangeEvent = event
307
+ onVolumeChange?.invoke(event)
308
+ }
294
309
  }
@@ -0,0 +1,16 @@
1
+ package com.margelo.nitro.nitrospeech.recognizer
2
+
3
+ import android.util.Log
4
+
5
+ class Logger (
6
+ private val disable: Boolean
7
+ ) {
8
+ private val isLogging = false
9
+ companion object {
10
+ private const val TAG = "HybridRecognizer"
11
+ }
12
+ fun log(message: String) {
13
+ if (disable || !isLogging) return
14
+ Log.d(TAG, message)
15
+ }
16
+ }
@@ -3,7 +3,6 @@ package com.margelo.nitro.nitrospeech.recognizer
3
3
  import android.os.Bundle
4
4
  import android.speech.RecognitionListener
5
5
  import android.speech.SpeechRecognizer
6
- import android.util.Log
7
6
  import com.margelo.nitro.nitrospeech.SpeechRecognitionConfig
8
7
  import com.margelo.nitro.nitrospeech.VolumeChangeEvent
9
8
  import kotlin.math.max
@@ -12,11 +11,11 @@ import kotlin.math.roundToInt
12
11
  class RecognitionListenerSession (
13
12
  private val autoStopper: AutoStopper?,
14
13
  private val config: SpeechRecognitionConfig?,
15
- private val onVolumeChange: ((event: VolumeChangeEvent) -> Unit)?,
14
+ private val fireVolumeChangeEvent: (event: VolumeChangeEvent) -> Unit,
16
15
  private val onFinishRecognition: (result: ArrayList<String>?, errorMessage: String?, recordingStopped: Boolean) -> Unit,
17
16
  ) {
17
+ private val logger = Logger(disable = false)
18
18
  companion object {
19
- private const val TAG = "HybridRecognizer"
20
19
  private const val SPEECH_LEVEL_THRESHOLD = 0.35
21
20
  private const val FLOOR_RISE_ALPHA = 0.01f
22
21
  private const val FLOOR_FALL_ALPHA = 0.20f
@@ -40,11 +39,11 @@ class RecognitionListenerSession (
40
39
  override fun onBeginningOfSpeech() {}
41
40
  override fun onRmsChanged(rmsdB: Float) {
42
41
  val volumeEvent = getVolume(rmsdB)
43
- onVolumeChange?.invoke(volumeEvent)
42
+ fireVolumeChangeEvent(volumeEvent)
44
43
  val threshold =
45
44
  config?.resetAutoFinishVoiceSensitivity?.coerceIn(0.0, 1.0)
46
45
  ?: SPEECH_LEVEL_THRESHOLD.toDouble()
47
- Log.d(TAG, "onRmsChanged: ${volumeEvent}")
46
+ // logger.log("onRmsChanged: ${volumeEvent}")
48
47
  if (volumeEvent.rawVolume > threshold) {
49
48
  autoStopper?.resetTimer()
50
49
  }
@@ -75,7 +74,7 @@ class RecognitionListenerSession (
75
74
  }
76
75
 
77
76
  override fun onResults(results: Bundle?) {
78
- Log.d(TAG, "onResults: $resultBatches")
77
+ logger.log("onResults: $resultBatches")
79
78
  onFinishRecognition(resultBatches, null, true)
80
79
  autoStopper?.stop()
81
80
  autoStopper?.onTimeout()
@@ -85,26 +84,26 @@ class RecognitionListenerSession (
85
84
  val matches = partialResults?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)
86
85
 
87
86
  if (matches.isNullOrEmpty() || matches[0] == "") {
88
- Log.d(TAG, "onPartialResults[0], skip, NO RECOGNIZE")
87
+ logger.log("onPartialResults[0], skip, NO RECOGNIZE")
89
88
  return
90
89
  }
91
90
 
92
91
  autoStopper?.resetTimer()
93
- Log.d(TAG, "onPartialResults[0], add ${matches[0]}")
92
+ logger.log("onPartialResults[0], add ${matches[0]}")
94
93
  var currentBatches = resultBatches
95
94
  if (currentBatches.isNullOrEmpty()) {
96
- Log.d(TAG, "onPartialResults[1], NO BATCHES YET | add first")
95
+ logger.log("onPartialResults[1], NO BATCHES YET | add first")
97
96
  currentBatches = arrayListOf(matches[0])
98
97
  } else {
99
- Log.d(TAG, "onPartialResults[1], current batches $currentBatches")
98
+ logger.log("onPartialResults[1], current batches $currentBatches")
100
99
  val prevBatchLength = currentBatches[currentBatches.lastIndex].length
101
100
  val match = if (config?.disableRepeatingFilter == true) matches[0] else repeatingFilter(matches[0])
102
101
  val matchLength = match.length
103
102
  if (config?.androidDisableBatchHandling == true || matchLength + 3 < prevBatchLength) {
104
- Log.d(TAG, "onPartialResults[2], append new batch")
103
+ logger.log("onPartialResults[2], append new batch")
105
104
  currentBatches.add(match)
106
105
  } else {
107
- Log.d(TAG, "onPartialResults[2], update batch, replace #${currentBatches.lastIndex}")
106
+ logger.log("onPartialResults[2], update batch, replace #${currentBatches.lastIndex}")
108
107
  currentBatches[currentBatches.lastIndex] = match
109
108
  }
110
109
  }
@@ -16,33 +16,18 @@ final class AudioLevelTracker {
16
16
  private static let meterRelease: Float = 0.08
17
17
  private static let defaultAutoStopResetThreshold: Double = 0.4
18
18
 
19
- private var autoStopResetThreshold: Double
20
19
  private var smoothedLevel: Float = 0
20
+
21
+ var currentSample: AudioLevelSample?
21
22
 
22
- init(resetAutoFinishVoiceSensitivity: Double?) {
23
- if let resetAutoFinishVoiceSensitivity {
24
- // Clamp value between 0 and 1
25
- self.autoStopResetThreshold = max(0, min(1, resetAutoFinishVoiceSensitivity))
26
- } else {
27
- self.autoStopResetThreshold = Self.defaultAutoStopResetThreshold
28
- }
29
- }
30
-
31
- func updateResetAutoFinishVoiceSensitivity(newValue: Double?) {
32
- if let newValue {
33
- // Clamp value between 0 and 1
34
- self.autoStopResetThreshold = max(0, min(1, newValue))
35
- } else {
36
- self.autoStopResetThreshold = Self.defaultAutoStopResetThreshold
37
- }
38
- }
23
+ private let lg = Lg(prefix: "RecognizerEngine")
39
24
 
40
25
  func reset() {
41
26
  smoothedLevel = 0
42
- self.autoStopResetThreshold = Self.defaultAutoStopResetThreshold
27
+ currentSample = nil
43
28
  }
44
29
 
45
- func process(_ buffer: AVAudioPCMBuffer) -> AudioLevelSample? {
30
+ func process(_ buffer: AVAudioPCMBuffer,_ autoStopResetThreshold: Double? = nil) -> AudioLevelSample? {
46
31
  guard let samples = buffer.floatChannelData?[0] else { return nil }
47
32
 
48
33
  let frameCount = Int(buffer.frameLength)
@@ -56,11 +41,20 @@ final class AudioLevelTracker {
56
41
  let coeff = normalized > smoothedLevel ? Self.meterAttack : Self.meterRelease
57
42
  smoothedLevel += coeff * (normalized - smoothedLevel)
58
43
 
59
- return AudioLevelSample(
44
+ var threshold = Self.defaultAutoStopResetThreshold
45
+ if let autoStopResetThreshold {
46
+ threshold = max(0, min(1, autoStopResetThreshold))
47
+ }
48
+
49
+ currentSample = AudioLevelSample(
60
50
  smoothed: Double(smoothedLevel * 1_000_000).rounded() / 1_000_000,
61
51
  raw: Double(normalized * 1_000_000).rounded() / 1_000_000,
62
52
  db: Double(db * 1_000).rounded() / 1_000,
63
- resetTimer: Double(normalized) >= self.autoStopResetThreshold
53
+ resetTimer: Double(normalized) >= threshold
64
54
  )
55
+
56
+ lg.log("[AudioLevelTracker.process] autoStopResetThreshold: \(threshold)")
57
+
58
+ return currentSample
65
59
  }
66
60
  }
@@ -18,7 +18,7 @@ class RecognizerEngine {
18
18
  var hardwareFormat: AVAudioFormat?
19
19
  weak var recognizerDelegate: RecognizerDelegate?
20
20
 
21
- private let audioLevelTracker: AudioLevelTracker
21
+ private let audioLevelTracker = AudioLevelTracker()
22
22
  private var appStateObserver: AppStateObserver?
23
23
  private var audioEngine: AVAudioEngine?
24
24
  private var autoStopper: AutoStopper?
@@ -29,9 +29,6 @@ class RecognizerEngine {
29
29
  init(locale: Locale, delegate: RecognizerDelegate) {
30
30
  self.locale = locale
31
31
  self.recognizerDelegate = delegate
32
- self.audioLevelTracker = AudioLevelTracker(
33
- resetAutoFinishVoiceSensitivity: delegate.config?.resetAutoFinishVoiceSensitivity
34
- )
35
32
  }
36
33
 
37
34
  // MARK: - Recognizer Methods
@@ -84,7 +81,10 @@ class RecognizerEngine {
84
81
  format: hardwareFormat
85
82
  ) { [weak self] buffer, _ in
86
83
  guard let self, let recognizerDelegate = self.recognizerDelegate else { return }
87
- if let sample = self.audioLevelTracker.process(buffer) {
84
+ if let sample = self.audioLevelTracker.process(
85
+ buffer,
86
+ recognizerDelegate.config?.resetAutoFinishVoiceSensitivity
87
+ ) {
88
88
  // Send buffer volume data
89
89
  recognizerDelegate.volumeChange(
90
90
  event:
@@ -148,13 +148,7 @@ class RecognizerEngine {
148
148
  from: "updateSession"
149
149
  )
150
150
  }
151
- // Update AutoFinish reset voice sensitivity interval
152
- if let newSensitivity = newConfig?.resetAutoFinishVoiceSensitivity,
153
- newSensitivity != currentConfig?.resetAutoFinishVoiceSensitivity {
154
- audioLevelTracker.updateResetAutoFinishVoiceSensitivity(
155
- newValue: newSensitivity
156
- )
157
- }
151
+
158
152
  if let addMsToTimer {
159
153
  // Add time to the timer once
160
154
  autoStopper?.addMsOnce(
@@ -168,7 +162,16 @@ class RecognizerEngine {
168
162
  // Only update new non-nil values in the config
169
163
  recognizerDelegate.softlyUpdateConfig(newConfig: newConfig)
170
164
  }
171
-
165
+
166
+ func getVoiceInputVolume() -> VolumeChangeEvent? {
167
+ guard let currentSample = audioLevelTracker.currentSample else { return nil }
168
+ return VolumeChangeEvent(
169
+ smoothedVolume: currentSample.smoothed,
170
+ rawVolume: currentSample.raw,
171
+ db: currentSample.db
172
+ )
173
+ }
174
+
172
175
  func cleanup(from: String) {
173
176
  lg.log("[cleanup]: \(from)")
174
177
  let wasActive = isActive
@@ -68,6 +68,14 @@ class HybridRecognizer: HybridRecognizerSpec {
68
68
  func getIsActive() -> Bool {
69
69
  engine?.isActive ?? false
70
70
  }
71
+
72
+ func getVoiceInputVolume() -> VolumeChangeEvent {
73
+ return engine?.getVoiceInputVolume() ?? VolumeChangeEvent(
74
+ smoothedVolume: 0,
75
+ rawVolume: 0,
76
+ db: nil
77
+ )
78
+ }
71
79
 
72
80
  func getSupportedLocalesIOS() -> [String] {
73
81
  return self.coordinator.getSupportedLocales()
@@ -5,7 +5,7 @@ final class AutoStopper {
5
5
  private static let defaultProgressIntervalMs = 1000.0
6
6
  private static let minProgressIntervalMs = 50.0
7
7
 
8
- private let lg = Lg(prefix: "AutoStopper", disable: true)
8
+ private let lg = Lg(prefix: "AutoStopper", disable: false)
9
9
 
10
10
  private let queue = DispatchQueue(label: "com.margelo.nitrospeech.autostopper")
11
11
 
@@ -1,5 +1,7 @@
1
1
  import type { RecognizerMethods } from './types';
2
2
  /**
3
3
  * Safe cross-component reference to the Speech Recognizer methods.
4
+ *
5
+ * All methods support worklets and UI thread calls
4
6
  */
5
7
  export declare const RecognizerRef: RecognizerMethods;
@@ -1,6 +1,8 @@
1
- import { recognizerAddAutoFinishTime, recognizerGetSupportedLocalesIOS, recognizerGetIsActive, recognizerResetAutoFinishTime, recognizerStartListening, recognizerStopListening, recognizerUpdateConfig, } from './methods';
1
+ import { recognizerAddAutoFinishTime, recognizerGetSupportedLocalesIOS, recognizerGetIsActive, recognizerResetAutoFinishTime, recognizerStartListening, recognizerStopListening, recognizerUpdateConfig, recognizerGetVoiceInputVolume, } from './methods';
2
2
  /**
3
3
  * Safe cross-component reference to the Speech Recognizer methods.
4
+ *
5
+ * All methods support worklets and UI thread calls
4
6
  */
5
7
  export const RecognizerRef = {
6
8
  startListening: recognizerStartListening,
@@ -9,5 +11,6 @@ export const RecognizerRef = {
9
11
  addAutoFinishTime: recognizerAddAutoFinishTime,
10
12
  updateConfig: recognizerUpdateConfig,
11
13
  getIsActive: recognizerGetIsActive,
14
+ getVoiceInputVolume: recognizerGetVoiceInputVolume,
12
15
  getSupportedLocalesIOS: recognizerGetSupportedLocalesIOS,
13
16
  };
@@ -5,4 +5,5 @@ export declare const recognizerResetAutoFinishTime: () => void;
5
5
  export declare const recognizerAddAutoFinishTime: (additionalTimeMs?: number) => void;
6
6
  export declare const recognizerUpdateConfig: (newConfig: SpeechRecognitionConfig, resetAutoFinishTime?: boolean) => void;
7
7
  export declare const recognizerGetIsActive: () => boolean;
8
+ export declare const recognizerGetVoiceInputVolume: () => import("./types").VolumeChangeEvent;
8
9
  export declare const recognizerGetSupportedLocalesIOS: () => string[];
@@ -23,6 +23,10 @@ export const recognizerGetIsActive = () => {
23
23
  'worklet';
24
24
  return SpeechRecognizer.getIsActive();
25
25
  };
26
+ export const recognizerGetVoiceInputVolume = () => {
27
+ 'worklet';
28
+ return SpeechRecognizer.getVoiceInputVolume();
29
+ };
26
30
  export const recognizerGetSupportedLocalesIOS = () => {
27
31
  'worklet';
28
32
  return SpeechRecognizer.getSupportedLocalesIOS().sort();
@@ -2,5 +2,5 @@ import type { Recognizer as RecognizerSpec } from '../specs/Recognizer.nitro';
2
2
  import type { SpeechRecognitionConfig } from '../specs/SpeechRecognitionConfig';
3
3
  import type { VolumeChangeEvent } from '../specs/VolumeChangeEvent';
4
4
  type RecognizerCallbacks = Pick<RecognizerSpec, 'onReadyForSpeech' | 'onRecordingStopped' | 'onResult' | 'onAutoFinishProgress' | 'onError' | 'onPermissionDenied' | 'onVolumeChange'>;
5
- type RecognizerMethods = Pick<RecognizerSpec, 'startListening' | 'stopListening' | 'resetAutoFinishTime' | 'addAutoFinishTime' | 'updateConfig' | 'getIsActive' | 'getSupportedLocalesIOS'>;
5
+ type RecognizerMethods = Pick<RecognizerSpec, 'startListening' | 'stopListening' | 'resetAutoFinishTime' | 'addAutoFinishTime' | 'updateConfig' | 'getIsActive' | 'getVoiceInputVolume' | 'getSupportedLocalesIOS'>;
6
6
  export type { RecognizerSpec, SpeechRecognitionConfig, VolumeChangeEvent, RecognizerCallbacks, RecognizerMethods, };
@@ -1,7 +1,8 @@
1
1
  import { useEffect } from 'react';
2
- import { recognizerResetAutoFinishTime, recognizerAddAutoFinishTime, recognizerUpdateConfig, recognizerGetIsActive, recognizerGetSupportedLocalesIOS, recognizerStartListening, recognizerStopListening, } from './methods';
2
+ import { recognizerResetAutoFinishTime, recognizerAddAutoFinishTime, recognizerUpdateConfig, recognizerGetIsActive, recognizerGetSupportedLocalesIOS, recognizerStartListening, recognizerStopListening, recognizerGetVoiceInputVolume, } from './methods';
3
3
  import { SpeechRecognizer } from './SpeechRecognizer';
4
4
  import { speechRecognizerVolumeChangeHandler } from './useVoiceInputVolume';
5
+ import { speechRecognizerActiveStateHandler } from './useRecognizerIsActive';
5
6
  /**
6
7
  * Safe, lifecycle-aware hook to use the recognizer.
7
8
  *
@@ -17,18 +18,12 @@ import { speechRecognizerVolumeChangeHandler } from './useVoiceInputVolume';
17
18
  */
18
19
  export const useRecognizer = (callbacks, destroyDeps = []) => {
19
20
  useEffect(() => {
20
- if (callbacks.onVolumeChange) {
21
- SpeechRecognizer.onVolumeChange = (event) => {
22
- callbacks.onVolumeChange?.(event);
23
- };
24
- }
25
- else {
26
- SpeechRecognizer.onVolumeChange = speechRecognizerVolumeChangeHandler;
27
- }
28
21
  SpeechRecognizer.onReadyForSpeech = () => {
22
+ speechRecognizerActiveStateHandler(true);
29
23
  callbacks.onReadyForSpeech?.();
30
24
  };
31
25
  SpeechRecognizer.onRecordingStopped = () => {
26
+ speechRecognizerActiveStateHandler(false);
32
27
  callbacks.onRecordingStopped?.();
33
28
  };
34
29
  SpeechRecognizer.onResult = (resultBatches) => {
@@ -43,6 +38,10 @@ export const useRecognizer = (callbacks, destroyDeps = []) => {
43
38
  SpeechRecognizer.onPermissionDenied = () => {
44
39
  callbacks.onPermissionDenied?.();
45
40
  };
41
+ SpeechRecognizer.onVolumeChange = (event) => {
42
+ speechRecognizerVolumeChangeHandler(event);
43
+ callbacks.onVolumeChange?.(event);
44
+ };
46
45
  return () => {
47
46
  SpeechRecognizer.onReadyForSpeech = undefined;
48
47
  SpeechRecognizer.onRecordingStopped = undefined;
@@ -66,6 +65,7 @@ export const useRecognizer = (callbacks, destroyDeps = []) => {
66
65
  addAutoFinishTime: recognizerAddAutoFinishTime,
67
66
  updateConfig: recognizerUpdateConfig,
68
67
  getIsActive: recognizerGetIsActive,
68
+ getVoiceInputVolume: recognizerGetVoiceInputVolume,
69
69
  getSupportedLocalesIOS: recognizerGetSupportedLocalesIOS,
70
70
  };
71
71
  };
@@ -0,0 +1,25 @@
1
+ type OnActiveStateChange = (isActive: boolean) => void;
2
+ /**
3
+ * Returns true if the speech recognition session is active.
4
+ */
5
+ export declare const useRecognizerIsActive: () => boolean;
6
+ /**
7
+ * Direct access to default Speech Recognizer isActive state change handler.
8
+ *
9
+ * In case you use static Speech Recognizer:
10
+ *
11
+ * ```typescript
12
+ * import { speechRecognizerActiveStateHandler } from '@gmessier/nitro-speech'
13
+ *
14
+ * SpeechRecognizer.onReadyForSpeech = () => {
15
+ * speechRecognizerActiveStateHandler(true)
16
+ * }
17
+ * SpeechRecognizer.onRecordingStopped = () => {
18
+ * speechRecognizerActiveStateHandler(false)
19
+ * }
20
+ * ... // setup everything else
21
+ * SpeechRecognizer.startListening({ locale: 'en-US' })
22
+ * ```
23
+ */
24
+ export declare const speechRecognizerActiveStateHandler: OnActiveStateChange;
25
+ export {};
@@ -0,0 +1,40 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ const subscribers = new Set();
3
+ let recognizerIsActive = false;
4
+ const getSnapshot = () => {
5
+ return recognizerIsActive;
6
+ };
7
+ /**
8
+ * Returns true if the speech recognition session is active.
9
+ */
10
+ export const useRecognizerIsActive = () => {
11
+ return useSyncExternalStore((subscriber) => {
12
+ subscribers.add(subscriber);
13
+ return () => subscribers.delete(subscriber);
14
+ }, getSnapshot);
15
+ };
16
+ /**
17
+ * Direct access to default Speech Recognizer isActive state change handler.
18
+ *
19
+ * In case you use static Speech Recognizer:
20
+ *
21
+ * ```typescript
22
+ * import { speechRecognizerActiveStateHandler } from '@gmessier/nitro-speech'
23
+ *
24
+ * SpeechRecognizer.onReadyForSpeech = () => {
25
+ * speechRecognizerActiveStateHandler(true)
26
+ * }
27
+ * SpeechRecognizer.onRecordingStopped = () => {
28
+ * speechRecognizerActiveStateHandler(false)
29
+ * }
30
+ * ... // setup everything else
31
+ * SpeechRecognizer.startListening({ locale: 'en-US' })
32
+ * ```
33
+ */
34
+ export const speechRecognizerActiveStateHandler = (isActive) => {
35
+ if (isActive === recognizerIsActive) {
36
+ return;
37
+ }
38
+ recognizerIsActive = isActive;
39
+ subscribers.forEach((subscriber) => subscriber?.(isActive));
40
+ };
@@ -1,5 +1,5 @@
1
1
  import type { RecognizerSpec, VolumeChangeEvent } from './types';
2
- type OnVolumeChange = RecognizerSpec['onVolumeChange'];
2
+ type OnVolumeChange = Exclude<RecognizerSpec['onVolumeChange'], undefined>;
3
3
  /**
4
4
  * Subscription to the voice input volume changes
5
5
  *
package/lib/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- export * from './Recognizer/useRecognizer';
2
- export * from './Recognizer/useVoiceInputVolume';
3
- export * from './Recognizer/SpeechRecognizer';
4
- export * from './Recognizer/RecognizerRef';
5
1
  export * from './Recognizer/types';
6
- export * from './NitroSpeech';
2
+ export { useRecognizer } from './Recognizer/useRecognizer';
3
+ export { useVoiceInputVolume, speechRecognizerVolumeChangeHandler, } from './Recognizer/useVoiceInputVolume';
4
+ export { useRecognizerIsActive, speechRecognizerActiveStateHandler, } from './Recognizer/useRecognizerIsActive';
5
+ export { SpeechRecognizer } from './Recognizer/SpeechRecognizer';
6
+ export { RecognizerRef } from './Recognizer/RecognizerRef';
7
+ export { NitroSpeech } from './NitroSpeech';
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
- export * from './Recognizer/useRecognizer';
2
- export * from './Recognizer/useVoiceInputVolume';
3
- export * from './Recognizer/SpeechRecognizer';
4
- export * from './Recognizer/RecognizerRef';
5
1
  export * from './Recognizer/types';
6
- export * from './NitroSpeech';
2
+ export { useRecognizer } from './Recognizer/useRecognizer';
3
+ export { useVoiceInputVolume, speechRecognizerVolumeChangeHandler, } from './Recognizer/useVoiceInputVolume';
4
+ export { useRecognizerIsActive, speechRecognizerActiveStateHandler, } from './Recognizer/useRecognizerIsActive';
5
+ export { SpeechRecognizer } from './Recognizer/SpeechRecognizer';
6
+ export { RecognizerRef } from './Recognizer/RecognizerRef';
7
+ export { NitroSpeech } from './NitroSpeech';
@@ -52,6 +52,10 @@ export interface Recognizer extends HybridObject<{
52
52
  * Returns true if the speech recognition is active.
53
53
  */
54
54
  getIsActive(): boolean;
55
+ /**
56
+ * Returns the current voice input volume.
57
+ */
58
+ getVoiceInputVolume(): VolumeChangeEvent;
55
59
  /**
56
60
  * Returns a list of supported locales.
57
61
  *
@@ -67,11 +71,11 @@ export interface Recognizer extends HybridObject<{
67
71
  */
68
72
  onRecordingStopped?: () => void;
69
73
  /**
70
- * Called each time either a new batch has been added or the last batch has been updated.
74
+ * Fires each time either a new batch has been added or the last batch has been updated.
71
75
  */
72
76
  onResult?: (resultBatches: string[]) => void;
73
77
  /**
74
- * Called every {@linkcode SpeechRecognitionConfig.autoFinishProgressIntervalMs} or 1000ms
78
+ * Fires every {@linkcode SpeechRecognitionConfig.autoFinishProgressIntervalMs} or 1000ms
75
79
  *
76
80
  * Time left in milliseconds until the timer stops.
77
81
  *
@@ -87,9 +91,7 @@ export interface Recognizer extends HybridObject<{
87
91
  */
88
92
  onPermissionDenied?: () => void;
89
93
  /**
90
- * Called with high and arbitrary frequency (many times per second) while audio recording is active.
91
- *
92
- * @warning overriding it will disable the built-in `useVoiceInputVolume` hook.
94
+ * Fires with high and arbitrary frequency (many times per second) while audio recording is active.
93
95
  */
94
96
  onVolumeChange?: (event: VolumeChangeEvent) => void;
95
97
  }
@@ -233,6 +233,11 @@ namespace margelo::nitro::nitrospeech {
233
233
  auto __result = method(_javaPart);
234
234
  return static_cast<bool>(__result);
235
235
  }
236
+ VolumeChangeEvent JHybridRecognizerSpec::getVoiceInputVolume() {
237
+ static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JVolumeChangeEvent>()>("getVoiceInputVolume");
238
+ auto __result = method(_javaPart);
239
+ return __result->toCpp();
240
+ }
236
241
  std::vector<std::string> JHybridRecognizerSpec::getSupportedLocalesIOS() {
237
242
  static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JArrayClass<jni::JString>>()>("getSupportedLocalesIOS");
238
243
  auto __result = method(_javaPart);
@@ -74,6 +74,7 @@ namespace margelo::nitro::nitrospeech {
74
74
  void addAutoFinishTime(std::optional<double> additionalTimeMs) override;
75
75
  void updateConfig(const std::optional<MutableSpeechRecognitionConfig>& newConfig, std::optional<bool> resetAutoFinishTime) override;
76
76
  bool getIsActive() override;
77
+ VolumeChangeEvent getVoiceInputVolume() override;
77
78
  std::vector<std::string> getSupportedLocalesIOS() override;
78
79
 
79
80
  private:
@@ -153,6 +153,10 @@ abstract class HybridRecognizerSpec: HybridObject() {
153
153
  @Keep
154
154
  abstract fun getIsActive(): Boolean
155
155
 
156
+ @DoNotStrip
157
+ @Keep
158
+ abstract fun getVoiceInputVolume(): VolumeChangeEvent
159
+
156
160
  @DoNotStrip
157
161
  @Keep
158
162
  abstract fun getSupportedLocalesIOS(): Array<String>
@@ -454,6 +454,15 @@ namespace margelo::nitro::nitrospeech::bridge::swift {
454
454
  return Result<bool>::withError(error);
455
455
  }
456
456
 
457
+ // pragma MARK: Result<VolumeChangeEvent>
458
+ using Result_VolumeChangeEvent_ = Result<VolumeChangeEvent>;
459
+ inline Result_VolumeChangeEvent_ create_Result_VolumeChangeEvent_(const VolumeChangeEvent& value) noexcept {
460
+ return Result<VolumeChangeEvent>::withValue(value);
461
+ }
462
+ inline Result_VolumeChangeEvent_ create_Result_VolumeChangeEvent_(const std::exception_ptr& error) noexcept {
463
+ return Result<VolumeChangeEvent>::withError(error);
464
+ }
465
+
457
466
  // pragma MARK: Result<std::vector<std::string>>
458
467
  using Result_std__vector_std__string__ = Result<std::vector<std::string>>;
459
468
  inline Result_std__vector_std__string__ create_Result_std__vector_std__string__(const std::vector<std::string>& value) noexcept {
@@ -178,6 +178,14 @@ namespace margelo::nitro::nitrospeech {
178
178
  auto __value = std::move(__result.value());
179
179
  return __value;
180
180
  }
181
+ inline VolumeChangeEvent getVoiceInputVolume() override {
182
+ auto __result = _swiftPart.getVoiceInputVolume();
183
+ if (__result.hasError()) [[unlikely]] {
184
+ std::rethrow_exception(__result.error());
185
+ }
186
+ auto __value = std::move(__result.value());
187
+ return __value;
188
+ }
181
189
  inline std::vector<std::string> getSupportedLocalesIOS() override {
182
190
  auto __result = _swiftPart.getSupportedLocalesIOS();
183
191
  if (__result.hasError()) [[unlikely]] {
@@ -26,6 +26,7 @@ public protocol HybridRecognizerSpec_protocol: HybridObject {
26
26
  func addAutoFinishTime(additionalTimeMs: Double?) throws -> Void
27
27
  func updateConfig(newConfig: MutableSpeechRecognitionConfig?, resetAutoFinishTime: Bool?) throws -> Void
28
28
  func getIsActive() throws -> Bool
29
+ func getVoiceInputVolume() throws -> VolumeChangeEvent
29
30
  func getSupportedLocalesIOS() throws -> [String]
30
31
  }
31
32
 
@@ -452,6 +452,18 @@ open class HybridRecognizerSpec_cxx {
452
452
  }
453
453
  }
454
454
 
455
+ @inline(__always)
456
+ public final func getVoiceInputVolume() -> bridge.Result_VolumeChangeEvent_ {
457
+ do {
458
+ let __result = try self.__implementation.getVoiceInputVolume()
459
+ let __resultCpp = __result
460
+ return bridge.create_Result_VolumeChangeEvent_(__resultCpp)
461
+ } catch (let __error) {
462
+ let __exceptionPtr = __error.toCpp()
463
+ return bridge.create_Result_VolumeChangeEvent_(__exceptionPtr)
464
+ }
465
+ }
466
+
455
467
  @inline(__always)
456
468
  public final func getSupportedLocalesIOS() -> bridge.Result_std__vector_std__string__ {
457
469
  do {
@@ -35,6 +35,7 @@ namespace margelo::nitro::nitrospeech {
35
35
  prototype.registerHybridMethod("addAutoFinishTime", &HybridRecognizerSpec::addAutoFinishTime);
36
36
  prototype.registerHybridMethod("updateConfig", &HybridRecognizerSpec::updateConfig);
37
37
  prototype.registerHybridMethod("getIsActive", &HybridRecognizerSpec::getIsActive);
38
+ prototype.registerHybridMethod("getVoiceInputVolume", &HybridRecognizerSpec::getVoiceInputVolume);
38
39
  prototype.registerHybridMethod("getSupportedLocalesIOS", &HybridRecognizerSpec::getSupportedLocalesIOS);
39
40
  });
40
41
  }
@@ -80,6 +80,7 @@ namespace margelo::nitro::nitrospeech {
80
80
  virtual void addAutoFinishTime(std::optional<double> additionalTimeMs) = 0;
81
81
  virtual void updateConfig(const std::optional<MutableSpeechRecognitionConfig>& newConfig, std::optional<bool> resetAutoFinishTime) = 0;
82
82
  virtual bool getIsActive() = 0;
83
+ virtual VolumeChangeEvent getVoiceInputVolume() = 0;
83
84
  virtual std::vector<std::string> getSupportedLocalesIOS() = 0;
84
85
 
85
86
  protected:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmessier/nitro-speech",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "React Native Real-time Speech Recognition Library powered by Nitro Modules",
5
5
  "main": "./lib/index.js",
6
6
  "module": "./lib/index.js",
@@ -7,10 +7,13 @@ import {
7
7
  recognizerStartListening,
8
8
  recognizerStopListening,
9
9
  recognizerUpdateConfig,
10
+ recognizerGetVoiceInputVolume,
10
11
  } from './methods'
11
12
 
12
13
  /**
13
14
  * Safe cross-component reference to the Speech Recognizer methods.
15
+ *
16
+ * All methods support worklets and UI thread calls
14
17
  */
15
18
  export const RecognizerRef: RecognizerMethods = {
16
19
  startListening: recognizerStartListening,
@@ -19,5 +22,6 @@ export const RecognizerRef: RecognizerMethods = {
19
22
  addAutoFinishTime: recognizerAddAutoFinishTime,
20
23
  updateConfig: recognizerUpdateConfig,
21
24
  getIsActive: recognizerGetIsActive,
25
+ getVoiceInputVolume: recognizerGetVoiceInputVolume,
22
26
  getSupportedLocalesIOS: recognizerGetSupportedLocalesIOS,
23
27
  }
@@ -34,6 +34,11 @@ export const recognizerGetIsActive = () => {
34
34
  return SpeechRecognizer.getIsActive()
35
35
  }
36
36
 
37
+ export const recognizerGetVoiceInputVolume = () => {
38
+ 'worklet'
39
+ return SpeechRecognizer.getVoiceInputVolume()
40
+ }
41
+
37
42
  export const recognizerGetSupportedLocalesIOS = () => {
38
43
  'worklet'
39
44
  return SpeechRecognizer.getSupportedLocalesIOS().sort()
@@ -21,6 +21,7 @@ type RecognizerMethods = Pick<
21
21
  | 'addAutoFinishTime'
22
22
  | 'updateConfig'
23
23
  | 'getIsActive'
24
+ | 'getVoiceInputVolume'
24
25
  | 'getSupportedLocalesIOS'
25
26
  >
26
27
 
@@ -7,10 +7,12 @@ import {
7
7
  recognizerGetSupportedLocalesIOS,
8
8
  recognizerStartListening,
9
9
  recognizerStopListening,
10
+ recognizerGetVoiceInputVolume,
10
11
  } from './methods'
11
12
  import type { RecognizerCallbacks, RecognizerMethods } from './types'
12
13
  import { SpeechRecognizer } from './SpeechRecognizer'
13
14
  import { speechRecognizerVolumeChangeHandler } from './useVoiceInputVolume'
15
+ import { speechRecognizerActiveStateHandler } from './useRecognizerIsActive'
14
16
 
15
17
  /**
16
18
  * Safe, lifecycle-aware hook to use the recognizer.
@@ -30,17 +32,12 @@ export const useRecognizer = (
30
32
  destroyDeps: DependencyList = []
31
33
  ): RecognizerMethods => {
32
34
  useEffect(() => {
33
- if (callbacks.onVolumeChange) {
34
- SpeechRecognizer.onVolumeChange = (event) => {
35
- callbacks.onVolumeChange?.(event)
36
- }
37
- } else {
38
- SpeechRecognizer.onVolumeChange = speechRecognizerVolumeChangeHandler
39
- }
40
35
  SpeechRecognizer.onReadyForSpeech = () => {
36
+ speechRecognizerActiveStateHandler(true)
41
37
  callbacks.onReadyForSpeech?.()
42
38
  }
43
39
  SpeechRecognizer.onRecordingStopped = () => {
40
+ speechRecognizerActiveStateHandler(false)
44
41
  callbacks.onRecordingStopped?.()
45
42
  }
46
43
  SpeechRecognizer.onResult = (resultBatches: string[]) => {
@@ -55,6 +52,10 @@ export const useRecognizer = (
55
52
  SpeechRecognizer.onPermissionDenied = () => {
56
53
  callbacks.onPermissionDenied?.()
57
54
  }
55
+ SpeechRecognizer.onVolumeChange = (event) => {
56
+ speechRecognizerVolumeChangeHandler(event)
57
+ callbacks.onVolumeChange?.(event)
58
+ }
58
59
  return () => {
59
60
  SpeechRecognizer.onReadyForSpeech = undefined
60
61
  SpeechRecognizer.onRecordingStopped = undefined
@@ -80,6 +81,7 @@ export const useRecognizer = (
80
81
  addAutoFinishTime: recognizerAddAutoFinishTime,
81
82
  updateConfig: recognizerUpdateConfig,
82
83
  getIsActive: recognizerGetIsActive,
84
+ getVoiceInputVolume: recognizerGetVoiceInputVolume,
83
85
  getSupportedLocalesIOS: recognizerGetSupportedLocalesIOS,
84
86
  }
85
87
  }
@@ -0,0 +1,49 @@
1
+ import { useSyncExternalStore } from 'react'
2
+
3
+ type OnActiveStateChange = (isActive: boolean) => void
4
+
5
+ const subscribers = new Set<OnActiveStateChange>()
6
+
7
+ let recognizerIsActive = false
8
+
9
+ const getSnapshot = () => {
10
+ return recognizerIsActive
11
+ }
12
+
13
+ /**
14
+ * Returns true if the speech recognition session is active.
15
+ */
16
+ export const useRecognizerIsActive = () => {
17
+ return useSyncExternalStore((subscriber) => {
18
+ subscribers.add(subscriber)
19
+ return () => subscribers.delete(subscriber)
20
+ }, getSnapshot)
21
+ }
22
+
23
+ /**
24
+ * Direct access to default Speech Recognizer isActive state change handler.
25
+ *
26
+ * In case you use static Speech Recognizer:
27
+ *
28
+ * ```typescript
29
+ * import { speechRecognizerActiveStateHandler } from '@gmessier/nitro-speech'
30
+ *
31
+ * SpeechRecognizer.onReadyForSpeech = () => {
32
+ * speechRecognizerActiveStateHandler(true)
33
+ * }
34
+ * SpeechRecognizer.onRecordingStopped = () => {
35
+ * speechRecognizerActiveStateHandler(false)
36
+ * }
37
+ * ... // setup everything else
38
+ * SpeechRecognizer.startListening({ locale: 'en-US' })
39
+ * ```
40
+ */
41
+ export const speechRecognizerActiveStateHandler: OnActiveStateChange = (
42
+ isActive
43
+ ) => {
44
+ if (isActive === recognizerIsActive) {
45
+ return
46
+ }
47
+ recognizerIsActive = isActive
48
+ subscribers.forEach((subscriber) => subscriber?.(isActive))
49
+ }
@@ -1,7 +1,7 @@
1
1
  import { useSyncExternalStore } from 'react'
2
2
  import type { RecognizerSpec, VolumeChangeEvent } from './types'
3
3
 
4
- type OnVolumeChange = RecognizerSpec['onVolumeChange']
4
+ type OnVolumeChange = Exclude<RecognizerSpec['onVolumeChange'], undefined>
5
5
 
6
6
  const subscribers = new Set<OnVolumeChange>()
7
7
 
package/src/index.ts CHANGED
@@ -1,6 +1,13 @@
1
- export * from './Recognizer/useRecognizer'
2
- export * from './Recognizer/useVoiceInputVolume'
3
- export * from './Recognizer/SpeechRecognizer'
4
- export * from './Recognizer/RecognizerRef'
5
1
  export * from './Recognizer/types'
6
- export * from './NitroSpeech'
2
+ export { useRecognizer } from './Recognizer/useRecognizer'
3
+ export {
4
+ useVoiceInputVolume,
5
+ speechRecognizerVolumeChangeHandler,
6
+ } from './Recognizer/useVoiceInputVolume'
7
+ export {
8
+ useRecognizerIsActive,
9
+ speechRecognizerActiveStateHandler,
10
+ } from './Recognizer/useRecognizerIsActive'
11
+ export { SpeechRecognizer } from './Recognizer/SpeechRecognizer'
12
+ export { RecognizerRef } from './Recognizer/RecognizerRef'
13
+ export { NitroSpeech } from './NitroSpeech'
@@ -66,6 +66,11 @@ export interface Recognizer extends HybridObject<{
66
66
  */
67
67
  getIsActive(): boolean
68
68
 
69
+ /**
70
+ * Returns the current voice input volume.
71
+ */
72
+ getVoiceInputVolume(): VolumeChangeEvent
73
+
69
74
  /**
70
75
  * Returns a list of supported locales.
71
76
  *
@@ -82,11 +87,11 @@ export interface Recognizer extends HybridObject<{
82
87
  */
83
88
  onRecordingStopped?: () => void
84
89
  /**
85
- * Called each time either a new batch has been added or the last batch has been updated.
90
+ * Fires each time either a new batch has been added or the last batch has been updated.
86
91
  */
87
92
  onResult?: (resultBatches: string[]) => void
88
93
  /**
89
- * Called every {@linkcode SpeechRecognitionConfig.autoFinishProgressIntervalMs} or 1000ms
94
+ * Fires every {@linkcode SpeechRecognitionConfig.autoFinishProgressIntervalMs} or 1000ms
90
95
  *
91
96
  * Time left in milliseconds until the timer stops.
92
97
  *
@@ -102,9 +107,7 @@ export interface Recognizer extends HybridObject<{
102
107
  */
103
108
  onPermissionDenied?: () => void
104
109
  /**
105
- * Called with high and arbitrary frequency (many times per second) while audio recording is active.
106
- *
107
- * @warning overriding it will disable the built-in `useVoiceInputVolume` hook.
110
+ * Fires with high and arbitrary frequency (many times per second) while audio recording is active.
108
111
  */
109
112
  onVolumeChange?: (event: VolumeChangeEvent) => void
110
113
  }