@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
|
-
|
|
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