@qafka/react-native 2.3.3 → 2.3.5

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.
@@ -1,8 +1,11 @@
1
1
  package com.qafka.audio
2
2
 
3
3
  import android.Manifest
4
+ import android.content.Context
4
5
  import android.content.pm.PackageManager
6
+ import android.media.AudioAttributes
5
7
  import android.media.AudioFormat
8
+ import android.media.AudioManager
6
9
  import android.media.AudioRecord
7
10
  import android.media.AudioTrack
8
11
  import android.media.MediaRecorder
@@ -33,6 +36,11 @@ class QafkaAudioModule(reactContext: ReactApplicationContext) :
33
36
  private var noiseSuppressor: NoiseSuppressor? = null
34
37
  private var automaticGainControl: AutomaticGainControl? = null
35
38
 
39
+ // Audio-mode state saved on startCapture and restored on stopCapture so the
40
+ // host app's audio routing is left exactly as we found it.
41
+ private var previousAudioMode: Int? = null
42
+ private var previousSpeakerphoneOn: Boolean? = null
43
+
36
44
  companion object {
37
45
  private const val TAG = "QafkaAudio"
38
46
  private const val CAPTURE_SAMPLE_RATE = 16000
@@ -110,6 +118,24 @@ class QafkaAudioModule(reactContext: ReactApplicationContext) :
110
118
 
111
119
  val bufferSize = maxOf(minBufferSize, 4096)
112
120
 
121
+ // iOS VPIO equivalent (part 1): put the audio system into communication
122
+ // mode and route both capture and playback through the voice path.
123
+ // Without this the AudioTrack below plays on the media path, which the
124
+ // VOICE_COMMUNICATION capture's echo canceler has no reference to — so
125
+ // the AI's own playback leaks back into the mic and it talks to itself.
126
+ try {
127
+ val audioManager =
128
+ reactApplicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
129
+ previousAudioMode = audioManager.mode
130
+ previousSpeakerphoneOn = audioManager.isSpeakerphoneOn
131
+ audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
132
+ @Suppress("DEPRECATION")
133
+ audioManager.isSpeakerphoneOn = true
134
+ dlogI("✅ AudioManager mode=IN_COMMUNICATION, speakerphone=on")
135
+ } catch (e: Exception) {
136
+ dlogW("⚠️ Failed to set communication audio mode: ${e.message}")
137
+ }
138
+
113
139
  try {
114
140
  val record = AudioRecord(
115
141
  MediaRecorder.AudioSource.VOICE_COMMUNICATION,
@@ -158,13 +184,29 @@ class QafkaAudioModule(reactContext: ReactApplicationContext) :
158
184
  dlogW("⚠️ AutomaticGainControl NOT available on this device")
159
185
  }
160
186
 
161
- // Initialize AudioTrack for playback
187
+ // Initialize AudioTrack for playback.
188
+ // The voice-communication output path favors low latency with a
189
+ // small default buffer; that underruns on jittery streamed network
190
+ // audio and makes playback choppy ("skipping"). Size the buffer for
191
+ // ~0.4s of audio to absorb jitter — smoothness matters more than
192
+ // latency for an assistant reply.
162
193
  val playMinBuffer = AudioTrack.getMinBufferSize(
163
194
  PLAYBACK_SAMPLE_RATE, CHANNEL_OUT, AUDIO_ENCODING
164
195
  )
165
- val playBufferSize = maxOf(playMinBuffer, 4096)
196
+ // bytes/sec = sampleRate * 2 (16-bit mono); * 4 / 10 ≈ 0.4s
197
+ val jitterBufferBytes = PLAYBACK_SAMPLE_RATE * 2 * 4 / 10
198
+ val playBufferSize = maxOf(playMinBuffer * 2, jitterBufferBytes)
166
199
 
200
+ // iOS VPIO equivalent (part 2): play on the voice-communication path
201
+ // (USAGE_VOICE_COMMUNICATION + CONTENT_TYPE_SPEECH) so the echo
202
+ // canceler bound to the capture session can reference and cancel it.
167
203
  val track = AudioTrack.Builder()
204
+ .setAudioAttributes(
205
+ AudioAttributes.Builder()
206
+ .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
207
+ .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
208
+ .build()
209
+ )
168
210
  .setAudioFormat(
169
211
  AudioFormat.Builder()
170
212
  .setSampleRate(PLAYBACK_SAMPLE_RATE)
@@ -247,6 +289,19 @@ class QafkaAudioModule(reactContext: ReactApplicationContext) :
247
289
  } catch (_: Exception) {}
248
290
  audioTrack = null
249
291
 
292
+ // Restore the host app's audio mode / routing exactly as we found it.
293
+ try {
294
+ val audioManager =
295
+ reactApplicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
296
+ previousAudioMode?.let { audioManager.mode = it }
297
+ previousSpeakerphoneOn?.let {
298
+ @Suppress("DEPRECATION")
299
+ audioManager.isSpeakerphoneOn = it
300
+ }
301
+ } catch (_: Exception) {}
302
+ previousAudioMode = null
303
+ previousSpeakerphoneOn = null
304
+
250
305
  promise.resolve(true)
251
306
  }
252
307
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qafka/react-native",
3
- "version": "2.3.3",
3
+ "version": "2.3.5",
4
4
  "description": "Drop-in AI assistant for React Native: chat, voice, and tool execution with screen-aware navigation.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",