@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.
- package/android/src/main/java/expo/modules/audiostream/AudioRecorderManager.kt +28 -4
- package/android/src/main/java/expo/modules/audiostream/ExpoPlayAudioStreamModule.kt +4 -1
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +7 -0
- package/build/index.js.map +1 -1
- package/ios/AudioPipeline.swift +67 -25
- package/ios/ExpoPlayAudioStreamModule.swift +10 -2
- package/ios/Microphone.swift +27 -4
- package/ios/SharedAudioEngine.swift +6 -4
- package/package.json +1 -1
- package/src/index.ts +8 -0
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
- package/android/.gradle/vcs-1/gc.properties +0 -0
|
@@ -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
|
-
|
|
158
|
-
//
|
|
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
|
-
|
|
389
|
-
|
|
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.
|
package/build/index.d.ts.map
CHANGED
|
@@ -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.
|
package/build/index.js.map
CHANGED
|
@@ -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"]}
|
package/ios/AudioPipeline.swift
CHANGED
|
@@ -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.
|
|
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:
|
|
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 —
|
|
223
|
-
"
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
//
|
|
544
|
-
|
|
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(
|
|
586
|
+
frameCapacity: AVAudioFrameCount(hwFrames)
|
|
548
587
|
) else { return }
|
|
549
|
-
pcmBuffer.frameLength = AVAudioFrameCount(
|
|
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..<
|
|
556
|
-
channelData[ch][i] = 0
|
|
557
|
-
}
|
|
593
|
+
for i in 0..<hwFrames { channelData[ch][i] = 0 }
|
|
558
594
|
}
|
|
559
595
|
} else {
|
|
560
|
-
//
|
|
561
|
-
|
|
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
|
|
564
|
-
|
|
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":
|
|
333
|
-
"totalSize":
|
|
340
|
+
"deltaSize": deltaSize,
|
|
341
|
+
"totalSize": micTotalDataSize,
|
|
334
342
|
"mimeType": "",
|
|
335
343
|
"soundLevel": soundLevel ?? -160
|
|
336
344
|
]
|
package/ios/Microphone.swift
CHANGED
|
@@ -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 *
|
|
229
|
+
Double(intervalMilliseconds) / 1000.0 * nodeOutputFormat.sampleRate
|
|
218
230
|
)
|
|
219
231
|
let tapBufferSize = max(intervalSamples, 256)
|
|
220
232
|
|
|
221
|
-
|
|
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
|
|
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
|
-
|
|
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
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.
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
File without changes
|