@edkimmel/expo-audio-stream 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -36,6 +36,7 @@ class AudioRecorderManager(
36
36
 
37
37
  // Flag to control whether actual audio data or silence is sent
38
38
  private var isSilent = false
39
+ @Volatile private var micGain: Float = 1.0f
39
40
  private var frequencyBandAnalyzer: FrequencyBandAnalyzer? = null
40
41
  private val gainNormalizer = GainNormalizer()
41
42
 
@@ -154,9 +155,10 @@ class AudioRecorderManager(
154
155
  // Generate a unique ID for this recording stream
155
156
  streamUuid = java.util.UUID.randomUUID().toString()
156
157
 
157
- audioRecord?.startRecording()
158
- // Apply audio effects after starting recording using the manager
158
+ // AEC and other effects must be attached before startRecording() so the
159
+ // hardware echo canceller processes audio from the very first captured frame.
159
160
  audioRecord?.let { audioEffectsManager.setupAudioEffects(it) }
161
+ audioRecord?.startRecording()
160
162
 
161
163
  isPaused.set(false)
162
164
  isRecording.set(true)
@@ -385,8 +387,11 @@ class AudioRecorderManager(
385
387
  }
386
388
 
387
389
  private fun emitAudioData(audioData: ByteArray, length: Int) {
388
- // If silent mode is active, replace audioData with zeros (using concise expression)
389
- val dataToEncode = if (isSilent) ByteArray(length) else audioData
390
+ val dataToEncode = when {
391
+ isSilent -> ByteArray(length)
392
+ micGain != 1.0f -> applyMicGain(audioData, length, micGain)
393
+ else -> audioData
394
+ }
390
395
 
391
396
  val encodedBuffer = audioDataEncoder.encodeToBase64(dataToEncode)
392
397
 
@@ -471,6 +476,25 @@ class AudioRecorderManager(
471
476
  /**
472
477
  * Toggles between sending actual audio data and silence
473
478
  */
479
+ fun setMicrophoneGain(gain: Float) {
480
+ micGain = gain.coerceIn(0.0f, 1.0f)
481
+ }
482
+
483
+ private fun applyMicGain(data: ByteArray, length: Int, gain: Float): ByteArray {
484
+ val result = ByteArray(length)
485
+ var i = 0
486
+ while (i + 1 < length) {
487
+ val lo = data[i].toInt() and 0xFF
488
+ val hi = data[i + 1].toInt() // sign-extends
489
+ val sample = (hi shl 8) or lo // little-endian Int16
490
+ val scaled = (sample * gain).toInt().coerceIn(-32768, 32767)
491
+ result[i] = (scaled and 0xFF).toByte()
492
+ result[i + 1] = (scaled shr 8).toByte()
493
+ i += 2
494
+ }
495
+ return result
496
+ }
497
+
474
498
  fun toggleSilence(isSilent: Boolean) {
475
499
  this.isSilent = isSilent
476
500
  Log.d(Constants.TAG, "Silence mode toggled: $isSilent")
@@ -168,10 +168,13 @@ class ExpoPlayAudioStreamModule : Module(), EventSender {
168
168
  }
169
169
 
170
170
  Function("toggleSilence") { isSilent: Boolean ->
171
- // Just toggle silence without returning any value
172
171
  audioRecorderManager.toggleSilence(isSilent)
173
172
  }
174
173
 
174
+ Function("setMicrophoneGain") { gain: Double ->
175
+ audioRecorderManager.setMicrophoneGain(gain.toFloat())
176
+ }
177
+
175
178
  // ── Native Audio Pipeline V3 ────────────────────────────────────
176
179
 
177
180
  AsyncFunction("connectPipeline") { options: Map<String, Any?>, promise: Promise ->
package/build/index.d.ts CHANGED
@@ -50,6 +50,11 @@ export declare class ExpoPlayAudioStream {
50
50
  * @throws {Error} If the microphone fails to toggle silence.
51
51
  */
52
52
  static toggleSilence(isSilent: boolean): void;
53
+ /**
54
+ * Sets the microphone capture gain (0.0 = silent, 1.0 = full).
55
+ * Takes effect immediately on the next captured audio chunk.
56
+ */
57
+ static setMicrophoneGain(gain: number): void;
53
58
  /**
54
59
  * Requests microphone permission from the user.
55
60
  * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission result.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/build/index.js CHANGED
@@ -124,6 +124,13 @@ export class ExpoPlayAudioStream {
124
124
  static toggleSilence(isSilent) {
125
125
  ExpoPlayAudioStreamModule.toggleSilence(isSilent);
126
126
  }
127
+ /**
128
+ * Sets the microphone capture gain (0.0 = silent, 1.0 = full).
129
+ * Takes effect immediately on the next captured audio chunk.
130
+ */
131
+ static setMicrophoneGain(gain) {
132
+ ExpoPlayAudioStreamModule.setMicrophoneGain(Math.max(0, Math.min(1, gain)));
133
+ }
127
134
  /**
128
135
  * Requests microphone permission from the user.
129
136
  * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission result.
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { EventSubscription } from \"expo-modules-core\";\nimport ExpoPlayAudioStreamModule from \"./ExpoPlayAudioStreamModule\";\n\n// Type alias for backwards compatibility\ntype Subscription = EventSubscription;\nimport {\n AudioDataEvent,\n AudioRecording,\n RecordingConfig,\n StartRecordingResult,\n PlaybackMode,\n Encoding,\n EncodingTypes,\n FrequencyBands,\n PlaybackModes,\n // Audio jitter buffer types\n IAudioBufferConfig,\n IAudioPlayPayload,\n IAudioFrame,\n BufferHealthState,\n IBufferHealthMetrics,\n IAudioBufferManager,\n IFrameProcessor,\n IQualityMonitor,\n BufferedStreamConfig,\n SmartBufferConfig,\n SmartBufferMode,\n NetworkConditions,\n} from \"./types\";\n\nimport {\n addAudioEventListener,\n addMicrophoneErrorListener,\n AudioEventPayload,\n AudioEvents,\n subscribeToEvent,\n DeviceReconnectedReason,\n DeviceReconnectedEventPayload,\n} from \"./events\";\n\nexport class ExpoPlayAudioStream {\n /**\n * Destroys the audio stream module, cleaning up all resources.\n * This should be called when the module is no longer needed.\n * It will reset all internal state and release audio resources.\n */\n static async destroy() {\n await ExpoPlayAudioStreamModule.destroy();\n }\n\n /**\n * Starts microphone streaming.\n * @param {RecordingConfig} recordingConfig - The recording configuration.\n * @returns {Promise<{recordingResult: StartRecordingResult, subscription: Subscription}>} A promise that resolves to an object containing the recording result and a subscription to audio events.\n * @throws {Error} If the recording fails to start.\n */\n static async startMicrophone(recordingConfig: RecordingConfig): Promise<{\n recordingResult: StartRecordingResult;\n subscription?: Subscription;\n }> {\n let subscription: Subscription | undefined;\n try {\n const { onAudioStream, onError, ...options } = recordingConfig;\n\n const subscriptions: EventSubscription[] = [];\n\n if (onAudioStream && typeof onAudioStream === \"function\") {\n subscriptions.push(\n addAudioEventListener(async (event: AudioEventPayload) => {\n const { fileUri, deltaSize, totalSize, position, encoded, soundLevel, frequencyBands, error } = event;\n // Only suppress AudioData error events when onError is wired up — the\n // MicrophoneError subscription will handle them. Without onError (old\n // integration or old native binary), fall through so the missing-encoded\n // path below preserves the pre-v0.6.0 behavior.\n if (error && onError) return;\n if (!encoded) {\n console.error(`[ExpoPlayAudioStream] Encoded audio data is missing`);\n throw new Error(\"Encoded audio data is missing\");\n }\n onAudioStream({\n data: encoded,\n position,\n fileUri,\n eventDataSize: deltaSize,\n totalSize,\n soundLevel,\n frequencyBands,\n });\n })\n );\n }\n\n if (onError && typeof onError === \"function\") {\n subscriptions.push(\n addMicrophoneErrorListener((event) => {\n onError(event);\n })\n );\n }\n\n if (subscriptions.length > 0) {\n subscription = { remove: () => subscriptions.forEach((s) => s.remove()) };\n }\n\n const result = await ExpoPlayAudioStreamModule.startMicrophone(options);\n\n return { recordingResult: result, subscription };\n } catch (error) {\n console.error(error);\n subscription?.remove();\n throw new Error(`Failed to start recording: ${error}`);\n }\n }\n\n /**\n * Stops the current microphone streaming.\n * @returns {Promise<void>}\n * @throws {Error} If the microphone streaming fails to stop.\n */\n static async stopMicrophone(): Promise<AudioRecording | null> {\n try {\n return await ExpoPlayAudioStreamModule.stopMicrophone();\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to stop mic stream: ${error}`);\n }\n }\n\n /**\n * Subscribes to audio events emitted during recording/streaming.\n * @param onMicrophoneStream - Callback function that will be called when audio data is received.\n * @returns {Subscription} A subscription object that can be used to unsubscribe from the events\n */\n static subscribeToAudioEvents(\n onMicrophoneStream: (event: AudioDataEvent) => Promise<void>\n ): Subscription {\n return addAudioEventListener(async (event: AudioEventPayload) => {\n const { fileUri, deltaSize, totalSize, position, encoded, soundLevel, frequencyBands } =\n event;\n if (!encoded) {\n console.error(`[ExpoPlayAudioStream] Encoded audio data is missing`);\n throw new Error(\"Encoded audio data is missing\");\n }\n onMicrophoneStream?.({\n data: encoded,\n position,\n fileUri,\n eventDataSize: deltaSize,\n totalSize,\n soundLevel,\n frequencyBands,\n });\n });\n }\n\n /**\n * Subscribes to events emitted by the audio stream module, for advanced use cases.\n * @param eventName - The name of the event to subscribe to.\n * @param onEvent - Callback function that will be called when the event is emitted.\n * @returns {Subscription} A subscription object that can be used to unsubscribe from the events.\n */\n static subscribe<T extends unknown>(\n eventName: string,\n onEvent: (event: T | undefined) => Promise<void>\n ): Subscription {\n return subscribeToEvent(eventName, onEvent);\n }\n\n /**\n * Prompts the user to select the microphone mode.\n * @returns {Promise<void>}\n * @throws {Error} If the microphone mode fails to prompt.\n */\n static promptMicrophoneModes() {\n ExpoPlayAudioStreamModule.promptMicrophoneModes();\n }\n\n /**\n * Toggles the silence state of the microphone.\n * @returns {Promise<void>}\n * @throws {Error} If the microphone fails to toggle silence.\n */\n static toggleSilence(isSilent: boolean) {\n ExpoPlayAudioStreamModule.toggleSilence(isSilent);\n }\n\n /**\n * Requests microphone permission from the user.\n * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission result.\n */\n static async requestPermissionsAsync(): Promise<{\n granted: boolean;\n canAskAgain?: boolean;\n status?: string;\n }> {\n try {\n return await ExpoPlayAudioStreamModule.requestPermissionsAsync();\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to request permissions: ${error}`);\n }\n }\n\n /**\n * Gets the current microphone permission status.\n * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission status.\n */\n static async getPermissionsAsync(): Promise<{\n granted: boolean;\n canAskAgain?: boolean;\n status?: string;\n }> {\n try {\n return await ExpoPlayAudioStreamModule.getPermissionsAsync();\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to get permissions: ${error}`);\n }\n }\n}\n\nexport {\n AudioDataEvent,\n DeviceReconnectedReason,\n DeviceReconnectedEventPayload,\n AudioRecording,\n RecordingConfig,\n StartRecordingResult,\n AudioEvents,\n PlaybackMode,\n Encoding,\n EncodingTypes,\n FrequencyBands,\n PlaybackModes,\n // Audio jitter buffer types\n IAudioBufferConfig,\n IAudioPlayPayload,\n IAudioFrame,\n BufferHealthState,\n IBufferHealthMetrics,\n IAudioBufferManager,\n IFrameProcessor,\n IQualityMonitor,\n BufferedStreamConfig,\n SmartBufferConfig,\n SmartBufferMode,\n NetworkConditions,\n};\n\n// Re-export Subscription type for backwards compatibility\nexport type { EventSubscription } from \"expo-modules-core\";\nexport type { Subscription } from \"./events\";\nexport { addMicrophoneErrorListener } from \"./events\";\nexport type { MicrophoneErrorEventPayload } from \"./events\";\n\n// Export native audio pipeline V3\nexport { Pipeline } from \"./pipeline\";\nexport type {\n ConnectPipelineOptions,\n ConnectPipelineResult,\n PushPipelineAudioOptions,\n InvalidatePipelineTurnOptions,\n PipelineState,\n PipelineEventMap,\n PipelineEventName,\n PipelineBufferTelemetry,\n PipelineTelemetry,\n PipelineStateChangedEvent,\n PipelinePlaybackStartedEvent,\n PipelineErrorEvent,\n PipelineZombieDetectedEvent,\n PipelineUnderrunEvent,\n PipelineDrainedEvent,\n PipelinePlaybackStoppedEvent,\n PipelineAudioFocusLostEvent,\n PipelineAudioFocusResumedEvent,\n} from \"./pipeline\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { EventSubscription } from \"expo-modules-core\";\nimport ExpoPlayAudioStreamModule from \"./ExpoPlayAudioStreamModule\";\n\n// Type alias for backwards compatibility\ntype Subscription = EventSubscription;\nimport {\n AudioDataEvent,\n AudioRecording,\n RecordingConfig,\n StartRecordingResult,\n PlaybackMode,\n Encoding,\n EncodingTypes,\n FrequencyBands,\n PlaybackModes,\n // Audio jitter buffer types\n IAudioBufferConfig,\n IAudioPlayPayload,\n IAudioFrame,\n BufferHealthState,\n IBufferHealthMetrics,\n IAudioBufferManager,\n IFrameProcessor,\n IQualityMonitor,\n BufferedStreamConfig,\n SmartBufferConfig,\n SmartBufferMode,\n NetworkConditions,\n} from \"./types\";\n\nimport {\n addAudioEventListener,\n addMicrophoneErrorListener,\n AudioEventPayload,\n AudioEvents,\n subscribeToEvent,\n DeviceReconnectedReason,\n DeviceReconnectedEventPayload,\n} from \"./events\";\n\nexport class ExpoPlayAudioStream {\n /**\n * Destroys the audio stream module, cleaning up all resources.\n * This should be called when the module is no longer needed.\n * It will reset all internal state and release audio resources.\n */\n static async destroy() {\n await ExpoPlayAudioStreamModule.destroy();\n }\n\n /**\n * Starts microphone streaming.\n * @param {RecordingConfig} recordingConfig - The recording configuration.\n * @returns {Promise<{recordingResult: StartRecordingResult, subscription: Subscription}>} A promise that resolves to an object containing the recording result and a subscription to audio events.\n * @throws {Error} If the recording fails to start.\n */\n static async startMicrophone(recordingConfig: RecordingConfig): Promise<{\n recordingResult: StartRecordingResult;\n subscription?: Subscription;\n }> {\n let subscription: Subscription | undefined;\n try {\n const { onAudioStream, onError, ...options } = recordingConfig;\n\n const subscriptions: EventSubscription[] = [];\n\n if (onAudioStream && typeof onAudioStream === \"function\") {\n subscriptions.push(\n addAudioEventListener(async (event: AudioEventPayload) => {\n const { fileUri, deltaSize, totalSize, position, encoded, soundLevel, frequencyBands, error } = event;\n // Only suppress AudioData error events when onError is wired up — the\n // MicrophoneError subscription will handle them. Without onError (old\n // integration or old native binary), fall through so the missing-encoded\n // path below preserves the pre-v0.6.0 behavior.\n if (error && onError) return;\n if (!encoded) {\n console.error(`[ExpoPlayAudioStream] Encoded audio data is missing`);\n throw new Error(\"Encoded audio data is missing\");\n }\n onAudioStream({\n data: encoded,\n position,\n fileUri,\n eventDataSize: deltaSize,\n totalSize,\n soundLevel,\n frequencyBands,\n });\n })\n );\n }\n\n if (onError && typeof onError === \"function\") {\n subscriptions.push(\n addMicrophoneErrorListener((event) => {\n onError(event);\n })\n );\n }\n\n if (subscriptions.length > 0) {\n subscription = { remove: () => subscriptions.forEach((s) => s.remove()) };\n }\n\n const result = await ExpoPlayAudioStreamModule.startMicrophone(options);\n\n return { recordingResult: result, subscription };\n } catch (error) {\n console.error(error);\n subscription?.remove();\n throw new Error(`Failed to start recording: ${error}`);\n }\n }\n\n /**\n * Stops the current microphone streaming.\n * @returns {Promise<void>}\n * @throws {Error} If the microphone streaming fails to stop.\n */\n static async stopMicrophone(): Promise<AudioRecording | null> {\n try {\n return await ExpoPlayAudioStreamModule.stopMicrophone();\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to stop mic stream: ${error}`);\n }\n }\n\n /**\n * Subscribes to audio events emitted during recording/streaming.\n * @param onMicrophoneStream - Callback function that will be called when audio data is received.\n * @returns {Subscription} A subscription object that can be used to unsubscribe from the events\n */\n static subscribeToAudioEvents(\n onMicrophoneStream: (event: AudioDataEvent) => Promise<void>\n ): Subscription {\n return addAudioEventListener(async (event: AudioEventPayload) => {\n const { fileUri, deltaSize, totalSize, position, encoded, soundLevel, frequencyBands } =\n event;\n if (!encoded) {\n console.error(`[ExpoPlayAudioStream] Encoded audio data is missing`);\n throw new Error(\"Encoded audio data is missing\");\n }\n onMicrophoneStream?.({\n data: encoded,\n position,\n fileUri,\n eventDataSize: deltaSize,\n totalSize,\n soundLevel,\n frequencyBands,\n });\n });\n }\n\n /**\n * Subscribes to events emitted by the audio stream module, for advanced use cases.\n * @param eventName - The name of the event to subscribe to.\n * @param onEvent - Callback function that will be called when the event is emitted.\n * @returns {Subscription} A subscription object that can be used to unsubscribe from the events.\n */\n static subscribe<T extends unknown>(\n eventName: string,\n onEvent: (event: T | undefined) => Promise<void>\n ): Subscription {\n return subscribeToEvent(eventName, onEvent);\n }\n\n /**\n * Prompts the user to select the microphone mode.\n * @returns {Promise<void>}\n * @throws {Error} If the microphone mode fails to prompt.\n */\n static promptMicrophoneModes() {\n ExpoPlayAudioStreamModule.promptMicrophoneModes();\n }\n\n /**\n * Toggles the silence state of the microphone.\n * @returns {Promise<void>}\n * @throws {Error} If the microphone fails to toggle silence.\n */\n static toggleSilence(isSilent: boolean) {\n ExpoPlayAudioStreamModule.toggleSilence(isSilent);\n }\n\n /**\n * Sets the microphone capture gain (0.0 = silent, 1.0 = full).\n * Takes effect immediately on the next captured audio chunk.\n */\n static setMicrophoneGain(gain: number) {\n ExpoPlayAudioStreamModule.setMicrophoneGain(Math.max(0, Math.min(1, gain)));\n }\n\n /**\n * Requests microphone permission from the user.\n * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission result.\n */\n static async requestPermissionsAsync(): Promise<{\n granted: boolean;\n canAskAgain?: boolean;\n status?: string;\n }> {\n try {\n return await ExpoPlayAudioStreamModule.requestPermissionsAsync();\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to request permissions: ${error}`);\n }\n }\n\n /**\n * Gets the current microphone permission status.\n * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission status.\n */\n static async getPermissionsAsync(): Promise<{\n granted: boolean;\n canAskAgain?: boolean;\n status?: string;\n }> {\n try {\n return await ExpoPlayAudioStreamModule.getPermissionsAsync();\n } catch (error) {\n console.error(error);\n throw new Error(`Failed to get permissions: ${error}`);\n }\n }\n}\n\nexport {\n AudioDataEvent,\n DeviceReconnectedReason,\n DeviceReconnectedEventPayload,\n AudioRecording,\n RecordingConfig,\n StartRecordingResult,\n AudioEvents,\n PlaybackMode,\n Encoding,\n EncodingTypes,\n FrequencyBands,\n PlaybackModes,\n // Audio jitter buffer types\n IAudioBufferConfig,\n IAudioPlayPayload,\n IAudioFrame,\n BufferHealthState,\n IBufferHealthMetrics,\n IAudioBufferManager,\n IFrameProcessor,\n IQualityMonitor,\n BufferedStreamConfig,\n SmartBufferConfig,\n SmartBufferMode,\n NetworkConditions,\n};\n\n// Re-export Subscription type for backwards compatibility\nexport type { EventSubscription } from \"expo-modules-core\";\nexport type { Subscription } from \"./events\";\nexport { addMicrophoneErrorListener } from \"./events\";\nexport type { MicrophoneErrorEventPayload } from \"./events\";\n\n// Export native audio pipeline V3\nexport { Pipeline } from \"./pipeline\";\nexport type {\n ConnectPipelineOptions,\n ConnectPipelineResult,\n PushPipelineAudioOptions,\n InvalidatePipelineTurnOptions,\n PipelineState,\n PipelineEventMap,\n PipelineEventName,\n PipelineBufferTelemetry,\n PipelineTelemetry,\n PipelineStateChangedEvent,\n PipelinePlaybackStartedEvent,\n PipelineErrorEvent,\n PipelineZombieDetectedEvent,\n PipelineUnderrunEvent,\n PipelineDrainedEvent,\n PipelinePlaybackStoppedEvent,\n PipelineAudioFocusLostEvent,\n PipelineAudioFocusResumedEvent,\n} from \"./pipeline\";\n"]}
@@ -69,9 +69,19 @@ class AudioPipeline: SharedAudioEngineDelegate {
69
69
  private var outputFormat: AVAudioFormat?
70
70
  private var jitterBuffer: JitterBuffer?
71
71
 
72
- /// Number of interleaved Int16 samples per scheduled buffer.
72
+ /// Number of interleaved Int16 samples read from the JitterBuffer per scheduled buffer
73
+ /// (at pipeline sample rate, e.g. 16 kHz).
73
74
  let frameSizeSamples: Int
74
75
 
76
+ /// Hardware output sample rate — read at connect time from the engine's output node.
77
+ /// The player node connects at this rate so AVAudioEngine injects no hidden resampler,
78
+ /// keeping the AEC echo reference deterministically timed.
79
+ private var hardwareSampleRate: Double = 48000
80
+
81
+ /// Number of frames written to the hardware-rate PCM buffer per scheduled buffer.
82
+ /// Always >= frameSizeSamples; computed as hardwareSampleRate / 50 (20 ms chunks).
83
+ private var hwFramesPerBuffer: Int = 960
84
+
75
85
  // ── Threading / state ───────────────────────────────────────────────
76
86
  private var running = false
77
87
  private let turnLock = NSLock()
@@ -178,10 +188,21 @@ class AudioPipeline: SharedAudioEngineDelegate {
178
188
  // (ensureAudioSessionInitialized). Just ensure it's active.
179
189
  try AVAudioSession.sharedInstance().setActive(true)
180
190
 
181
- // ── 4. Create format and attach player node to shared engine ─
191
+ // ── 4. Read hardware rate and create hardware-rate format ───
192
+ // Connecting at the native hardware rate prevents AVAudioEngine from
193
+ // injecting a hidden resampler between the player node and the hardware
194
+ // output. A hidden resampler shifts buffer timestamps and desynchronises
195
+ // the AEC echo reference, causing the adaptive filter to converge slowly
196
+ // or not at all. The scheduling loop handles the 16kHz→48kHz conversion
197
+ // via linear interpolation before handing buffers to the player node.
198
+ let rawHwRate = sharedEngine.engine?.outputNode.outputFormat(forBus: 0).sampleRate ?? 0
199
+ let resolvedHwRate = rawHwRate > 0 ? rawHwRate : 48000
200
+ hardwareSampleRate = resolvedHwRate
201
+ hwFramesPerBuffer = max(1, Int(resolvedHwRate / 50.0))
202
+
182
203
  guard let format = AVAudioFormat(
183
204
  commonFormat: .pcmFormatFloat32,
184
- sampleRate: Double(sampleRate),
205
+ sampleRate: resolvedHwRate,
185
206
  channels: AVAudioChannelCount(channelCount),
186
207
  interleaved: false
187
208
  ) else {
@@ -219,8 +240,9 @@ class AudioPipeline: SharedAudioEngineDelegate {
219
240
  startFrequencyBandTimer()
220
241
 
221
242
  setState(.idle)
222
- Logger.debug("[\(AudioPipeline.TAG)] Connected — sampleRate=\(sampleRate) " +
223
- "ch=\(channelCount) frameSamples=\(frameSizeSamples) " +
243
+ Logger.debug("[\(AudioPipeline.TAG)] Connected — pipelineRate=\(sampleRate) " +
244
+ "hwRate=\(Int(resolvedHwRate)) ch=\(channelCount) " +
245
+ "pipelineFrames=\(frameSizeSamples) hwFrames=\(hwFramesPerBuffer) " +
224
246
  "targetBuffer=\(targetBufferMs)ms")
225
247
  }
226
248
 
@@ -311,14 +333,27 @@ class AudioPipeline: SharedAudioEngineDelegate {
311
333
  // Old node is invalid (detached during teardown). Create a fresh one.
312
334
  scheduleGeneration += 1
313
335
 
314
- guard let sharedEngine = sharedEngine,
315
- let format = AVAudioFormat(
316
- commonFormat: .pcmFormatFloat32,
317
- sampleRate: Double(sampleRate),
318
- channels: AVAudioChannelCount(channelCount),
319
- interleaved: false
320
- ) else {
321
- Logger.debug("[\(AudioPipeline.TAG)] engineDidRebuild — cannot create format or engine missing, treating as dead")
336
+ guard let sharedEngine = sharedEngine else {
337
+ Logger.debug("[\(AudioPipeline.TAG)] engineDidRebuild engine missing, treating as dead")
338
+ running = false
339
+ setState(.error)
340
+ listener?.onError(code: "ENGINE_DIED", message: "Failed to recreate audio node after engine rebuild")
341
+ return
342
+ }
343
+
344
+ // Re-read hardware rate from the new engine (route may have changed).
345
+ let rawHwRate = sharedEngine.engine?.outputNode.outputFormat(forBus: 0).sampleRate ?? 0
346
+ let resolvedHwRate = rawHwRate > 0 ? rawHwRate : hardwareSampleRate
347
+ hardwareSampleRate = resolvedHwRate
348
+ hwFramesPerBuffer = max(1, Int(resolvedHwRate / 50.0))
349
+
350
+ guard let format = AVAudioFormat(
351
+ commonFormat: .pcmFormatFloat32,
352
+ sampleRate: resolvedHwRate,
353
+ channels: AVAudioChannelCount(channelCount),
354
+ interleaved: false
355
+ ) else {
356
+ Logger.debug("[\(AudioPipeline.TAG)] engineDidRebuild — cannot create format, treating as dead")
322
357
  running = false
323
358
  setState(.error)
324
359
  listener?.onError(code: "ENGINE_DIED", message: "Failed to recreate audio node after engine rebuild")
@@ -540,28 +575,35 @@ class AudioPipeline: SharedAudioEngineDelegate {
540
575
  }
541
576
  }
542
577
 
543
- // Convert to non-interleaved Float32 for AVAudioEngine
544
- let framesPerBuffer = frameSizeSamples / channelCount
578
+ // Build a hardware-rate PCM buffer. The scheduling loop resamples from
579
+ // pipeline rate (e.g. 16 kHz) to hardware rate (e.g. 48 kHz) so the
580
+ // player node connects at the native rate and the engine never inserts a
581
+ // hidden resampler that would desynchronise the AEC echo reference.
582
+ let pipelineFrames = frameSizeSamples / channelCount // frames at pipeline rate
583
+ let hwFrames = hwFramesPerBuffer // frames at hardware rate
545
584
  guard let pcmBuffer = AVAudioPCMBuffer(
546
585
  pcmFormat: format,
547
- frameCapacity: AVAudioFrameCount(framesPerBuffer)
586
+ frameCapacity: AVAudioFrameCount(hwFrames)
548
587
  ) else { return }
549
- pcmBuffer.frameLength = AVAudioFrameCount(framesPerBuffer)
588
+ pcmBuffer.frameLength = AVAudioFrameCount(hwFrames)
550
589
 
551
590
  if let channelData = pcmBuffer.floatChannelData {
552
591
  if isInterrupted {
553
- // Write silence during interruption
554
592
  for ch in 0..<channelCount {
555
- for i in 0..<framesPerBuffer {
556
- channelData[ch][i] = 0
557
- }
593
+ for i in 0..<hwFrames { channelData[ch][i] = 0 }
558
594
  }
559
595
  } else {
560
- // De-interleave Int16non-interleaved Float32
561
- for frame in 0..<framesPerBuffer {
596
+ // Linear interpolation: pipeline rate hardware rate
597
+ let ratio = hardwareSampleRate / Double(sampleRate)
598
+ for i in 0..<hwFrames {
599
+ let virtualIndex = Double(i) / ratio
600
+ let indexLow = Int(virtualIndex)
601
+ let indexHigh = min(indexLow + 1, pipelineFrames - 1)
602
+ let weight = Float(virtualIndex - Double(indexLow))
562
603
  for ch in 0..<channelCount {
563
- let sampleIndex = frame * channelCount + ch
564
- channelData[ch][frame] = Float(renderSamples[sampleIndex]) / 32768.0
604
+ let sampleLow = Float(renderSamples[indexLow * channelCount + ch]) / 32768.0
605
+ let sampleHigh = Float(renderSamples[indexHigh * channelCount + ch]) / 32768.0
606
+ channelData[ch][i] = sampleLow + weight * (sampleHigh - sampleLow)
565
607
  }
566
608
  }
567
609
  }
@@ -31,6 +31,7 @@ public class ExpoPlayAudioStreamModule: Module, MicrophoneDataDelegate, Pipeline
31
31
  }
32
32
 
33
33
  private var isAudioSessionInitialized: Bool = false
34
+ private var micTotalDataSize: Int = 0
34
35
 
35
36
  // ── PipelineEventSender conformance ───────────────────────────────
36
37
  func sendPipelineEvent(_ eventName: String, _ params: [String: Any]) {
@@ -150,6 +151,7 @@ public class ExpoPlayAudioStreamModule: Module, MicrophoneDataDelegate, Pipeline
150
151
  "sampleRate": result.sampleRate ?? 48000,
151
152
  "mimeType": result.mimeType ?? "",
152
153
  ]
154
+ micTotalDataSize = 0
153
155
  promise.resolve(resultDict)
154
156
  }
155
157
  } else {
@@ -165,6 +167,10 @@ public class ExpoPlayAudioStreamModule: Module, MicrophoneDataDelegate, Pipeline
165
167
  microphone.toggleSilence(isSilent: isSilent)
166
168
  }
167
169
 
170
+ Function("setMicrophoneGain") { (gain: Double) in
171
+ microphone.setMicrophoneGain(Float(gain))
172
+ }
173
+
168
174
  // ── Pipeline functions ────────────────────────────────────────────
169
175
 
170
176
  AsyncFunction("connectPipeline") { (options: [String: Any], promise: Promise) in
@@ -324,13 +330,15 @@ public class ExpoPlayAudioStreamModule: Module, MicrophoneDataDelegate, Pipeline
324
330
 
325
331
  func onMicrophoneData(_ microphoneData: Data, _ soundLevel: Float?, _ frequencyBands: FrequencyBands?) {
326
332
  let encodedData = microphoneData.base64EncodedString()
333
+ let deltaSize = microphoneData.count
334
+ micTotalDataSize += deltaSize
327
335
  var eventBody: [String: Any] = [
328
336
  "fileUri": "",
329
337
  "lastEmittedSize": 0,
330
338
  "position": 0,
331
339
  "encoded": encodedData,
332
- "deltaSize": 0,
333
- "totalSize": 0,
340
+ "deltaSize": deltaSize,
341
+ "totalSize": micTotalDataSize,
334
342
  "mimeType": "",
335
343
  "soundLevel": soundLevel ?? -160
336
344
  ]
@@ -30,6 +30,9 @@ class Microphone: SharedAudioEngineDelegate {
30
30
 
31
31
  private var isRecording: Bool = false
32
32
  private var isSilent: Bool = false
33
+ // Read on the audio render thread, written from the JS thread.
34
+ // Float is 4 bytes / naturally aligned — atomic on ARM64, no lock needed.
35
+ private var micGain: Float = 1.0
33
36
  private var frequencyBandAnalyzer: FrequencyBandAnalyzer?
34
37
  private var frequencyBandConfig: (lowCrossoverHz: Float, highCrossoverHz: Float)?
35
38
 
@@ -124,6 +127,10 @@ class Microphone: SharedAudioEngineDelegate {
124
127
  self.isSilent = isSilent
125
128
  }
126
129
 
130
+ func setMicrophoneGain(_ gain: Float) {
131
+ micGain = max(0.0, min(1.0, gain))
132
+ }
133
+
127
134
  func startRecording(settings: RecordingSettings, intervalMilliseconds: Int,
128
135
  frequencyBandConfig: (lowCrossoverHz: Float, highCrossoverHz: Float)? = nil) -> StartRecordingResult? {
129
136
  guard !isRecording else {
@@ -213,12 +220,27 @@ class Microphone: SharedAudioEngineDelegate {
213
220
  let hardwareFormat = inputNode.inputFormat(forBus: 0)
214
221
  recordingSettings?.sampleRate = hardwareFormat.sampleRate
215
222
 
223
+ // outputFormat is what the tap actually receives — under VoiceProcessingIO
224
+ // this may differ from inputFormat (VP may output at a different rate and
225
+ // adds internal metadata channels beyond channel 0).
226
+ let nodeOutputFormat = inputNode.outputFormat(forBus: 0)
227
+
216
228
  let intervalSamples = AVAudioFrameCount(
217
- Double(intervalMilliseconds) / 1000.0 * hardwareFormat.sampleRate
229
+ Double(intervalMilliseconds) / 1000.0 * nodeOutputFormat.sampleRate
218
230
  )
219
231
  let tapBufferSize = max(intervalSamples, 256)
220
232
 
221
- inputNode.installTap(onBus: 0, bufferSize: tapBufferSize, format: nil) { [weak self] (buffer, time) in
233
+ // Explicit mono Float32 format strips VP metadata channels. Passing format: nil
234
+ // here would deliver the raw VP output channels, corrupting the audio stream
235
+ // with echo-tracking metadata that bleeds into downstream processing.
236
+ let tapFormat = AVAudioFormat(
237
+ commonFormat: .pcmFormatFloat32,
238
+ sampleRate: nodeOutputFormat.sampleRate,
239
+ channels: 1,
240
+ interleaved: false
241
+ )
242
+
243
+ inputNode.installTap(onBus: 0, bufferSize: tapBufferSize, format: tapFormat) { [weak self] (buffer, time) in
222
244
  guard let self = self else { return }
223
245
  guard buffer.frameLength > 0 else {
224
246
  Logger.debug("[Microphone] Received empty buffer in tap callback")
@@ -279,7 +301,8 @@ class Microphone: SharedAudioEngineDelegate {
279
301
 
280
302
  let data: Data
281
303
  if isSilent {
282
- let byteCount = Int(currentBuffer.frameCapacity) * Int(currentBuffer.format.streamDescription.pointee.mBytesPerFrame)
304
+ let bytesPerSample = targetBitDepth / 8
305
+ let byteCount = Int(currentBuffer.frameLength) * Int(currentBuffer.format.channelCount) * bytesPerSample
283
306
  data = Data(repeating: 0, count: byteCount)
284
307
  } else if targetBitDepth == 16 && currentBuffer.format.commonFormat == .pcmFormatFloat32,
285
308
  let floatData = currentBuffer.floatChannelData {
@@ -288,7 +311,7 @@ class Microphone: SharedAudioEngineDelegate {
288
311
  var int16Data = Data(capacity: frameCount * channelCount * 2)
289
312
  for frame in 0..<frameCount {
290
313
  for ch in 0..<channelCount {
291
- let sample = max(-1.0, min(1.0, floatData[ch][frame]))
314
+ let sample = max(-1.0, min(1.0, floatData[ch][frame] * micGain))
292
315
  var int16Sample = Int16(sample * 32767.0)
293
316
  int16Data.append(Data(bytes: &int16Sample, count: 2))
294
317
  }
@@ -130,10 +130,15 @@ class SharedAudioEngine {
130
130
  }
131
131
 
132
132
  private func configureOnQueue(playbackMode: PlaybackMode) throws {
133
- if isConfigured && self.playbackMode == playbackMode && engine?.isRunning == true {
133
+ let needsVP = playbackMode == .conversation || playbackMode == .voiceProcessing
134
+ let vpActive = !needsVP || (engine?.inputNode.isVoiceProcessingEnabled == true)
135
+ if isConfigured && self.playbackMode == playbackMode && engine?.isRunning == true && vpActive {
134
136
  Logger.debug("[\(SharedAudioEngine.TAG)] Already configured for \(playbackMode) and engine running, skipping")
135
137
  return
136
138
  }
139
+ if isConfigured && !vpActive {
140
+ Logger.debug("[\(SharedAudioEngine.TAG)] Voice processing dropped (setCategory side-effect) — forcing reconfiguration")
141
+ }
137
142
 
138
143
  if isConfigured && engine?.isRunning != true {
139
144
  Logger.debug("[\(SharedAudioEngine.TAG)] Engine marked configured but not running — forcing reconfiguration")
@@ -151,7 +156,6 @@ class SharedAudioEngine {
151
156
  // Done before connecting nodes so the audio graph incorporates VP from the start.
152
157
  if playbackMode == .conversation || playbackMode == .voiceProcessing {
153
158
  try engine.inputNode.setVoiceProcessingEnabled(true)
154
- try engine.outputNode.setVoiceProcessingEnabled(true)
155
159
  Logger.debug("[\(SharedAudioEngine.TAG)] Voice processing enabled")
156
160
  }
157
161
 
@@ -270,7 +274,6 @@ class SharedAudioEngine {
270
274
  if playbackMode == .conversation || playbackMode == .voiceProcessing {
271
275
  if let engine = engine {
272
276
  try? engine.inputNode.setVoiceProcessingEnabled(false)
273
- try? engine.outputNode.setVoiceProcessingEnabled(false)
274
277
  }
275
278
  }
276
279
 
@@ -350,7 +353,6 @@ class SharedAudioEngine {
350
353
  if playbackMode == .conversation || playbackMode == .voiceProcessing {
351
354
  do {
352
355
  try engine.inputNode.setVoiceProcessingEnabled(true)
353
- try engine.outputNode.setVoiceProcessingEnabled(true)
354
356
  } catch {
355
357
  Logger.debug("[\(SharedAudioEngine.TAG)] Voice processing re-enable failed: \(error)")
356
358
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edkimmel/expo-audio-stream",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Expo Play Audio Stream module",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
package/src/index.ts CHANGED
@@ -184,6 +184,14 @@ export class ExpoPlayAudioStream {
184
184
  ExpoPlayAudioStreamModule.toggleSilence(isSilent);
185
185
  }
186
186
 
187
+ /**
188
+ * Sets the microphone capture gain (0.0 = silent, 1.0 = full).
189
+ * Takes effect immediately on the next captured audio chunk.
190
+ */
191
+ static setMicrophoneGain(gain: number) {
192
+ ExpoPlayAudioStreamModule.setMicrophoneGain(Math.max(0, Math.min(1, gain)));
193
+ }
194
+
187
195
  /**
188
196
  * Requests microphone permission from the user.
189
197
  * @returns {Promise<{granted: boolean, canAskAgain?: boolean, status?: string}>} A promise that resolves to the permission result.
File without changes
@@ -1,2 +0,0 @@
1
- #Thu Jun 04 15:44:34 EDT 2026
2
- gradle.version=8.9
File without changes