@siteed/expo-audio-studio 2.18.2 → 2.18.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +19 -3
  3. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +142 -91
  4. package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +1 -0
  5. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +1 -1
  6. package/build/cjs/AudioAnalysis/extractAudioAnalysis.js +6 -1
  7. package/build/cjs/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
  8. package/build/cjs/AudioAnalysis/extractAudioData.js +10 -1
  9. package/build/cjs/AudioAnalysis/extractAudioData.js.map +1 -1
  10. package/build/cjs/AudioAnalysis/extractMelSpectrogram.js +5 -1
  11. package/build/cjs/AudioAnalysis/extractMelSpectrogram.js.map +1 -1
  12. package/build/cjs/trimAudio.js +3 -1
  13. package/build/cjs/trimAudio.js.map +1 -1
  14. package/build/cjs/useAudioRecorder.js +9 -38
  15. package/build/cjs/useAudioRecorder.js.map +1 -1
  16. package/build/cjs/utils/cleanNativeOptions.js +22 -0
  17. package/build/cjs/utils/cleanNativeOptions.js.map +1 -0
  18. package/build/esm/AudioAnalysis/extractAudioAnalysis.js +6 -1
  19. package/build/esm/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
  20. package/build/esm/AudioAnalysis/extractAudioData.js +10 -1
  21. package/build/esm/AudioAnalysis/extractAudioData.js.map +1 -1
  22. package/build/esm/AudioAnalysis/extractMelSpectrogram.js +5 -1
  23. package/build/esm/AudioAnalysis/extractMelSpectrogram.js.map +1 -1
  24. package/build/esm/trimAudio.js +3 -1
  25. package/build/esm/trimAudio.js.map +1 -1
  26. package/build/esm/useAudioRecorder.js +8 -4
  27. package/build/esm/useAudioRecorder.js.map +1 -1
  28. package/build/esm/utils/cleanNativeOptions.js +19 -0
  29. package/build/esm/utils/cleanNativeOptions.js.map +1 -0
  30. package/build/types/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -1
  31. package/build/types/AudioAnalysis/extractAudioData.d.ts.map +1 -1
  32. package/build/types/AudioAnalysis/extractMelSpectrogram.d.ts.map +1 -1
  33. package/build/types/trimAudio.d.ts.map +1 -1
  34. package/build/types/useAudioRecorder.d.ts.map +1 -1
  35. package/build/types/utils/cleanNativeOptions.d.ts +15 -0
  36. package/build/types/utils/cleanNativeOptions.d.ts.map +1 -0
  37. package/ios/AudioStreamManager.swift +76 -18
  38. package/ios/ExpoAudioStreamModule.swift +17 -19
  39. package/package.json +6 -7
  40. package/src/AudioAnalysis/extractAudioAnalysis.ts +12 -1
  41. package/src/AudioAnalysis/extractAudioData.ts +12 -1
  42. package/src/AudioAnalysis/extractMelSpectrogram.ts +11 -1
  43. package/src/trimAudio.ts +5 -1
  44. package/src/useAudioRecorder.tsx +8 -7
  45. package/src/utils/cleanNativeOptions.ts +18 -0
@@ -177,14 +177,14 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDev
177
177
  /// - `bitrate`: The compression bitrate in bps (default is 128000).
178
178
  /// - promise: A promise to resolve with the recording settings or reject with an error.
179
179
  AsyncFunction("startRecording") { (options: [String: Any], promise: Promise) in
180
- Logger.debug("ExpoAudioStreamModule", "startRecording called with options: \(options)")
180
+ Logger.debug("ExpoAudioStreamModule", "startRecording called")
181
181
  self.checkMicrophonePermission { granted in
182
182
  guard granted else {
183
183
  Logger.warn("ExpoAudioStreamModule", "startRecording: Permission denied.")
184
184
  promise.reject("PERMISSION_DENIED", "Recording permission has not been granted")
185
185
  return
186
186
  }
187
-
187
+
188
188
  // Check if output.compressed is enabled and format is Opus
189
189
  var modifiedOptions = options
190
190
  if let output = options["output"] as? [String: Any],
@@ -192,25 +192,24 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDev
192
192
  let enabled = compressed["enabled"] as? Bool, enabled,
193
193
  let format = compressed["format"] as? String,
194
194
  format.lowercased() == "opus" {
195
-
195
+
196
196
  // Create mutable copies
197
197
  var modifiedOutput = output
198
198
  var modifiedCompressed = compressed
199
-
199
+
200
200
  // Change format to AAC and log warning
201
201
  modifiedCompressed["format"] = "aac"
202
202
  modifiedOutput["compressed"] = modifiedCompressed
203
203
  modifiedOptions["output"] = modifiedOutput
204
-
204
+
205
205
  Logger.warn("ExpoAudioStreamModule", "startRecording: Opus format is not supported on iOS. Falling back to AAC format.")
206
206
  }
207
-
207
+
208
208
  // Create settings with validation using the potentially modified options
209
209
  let settingsResult = RecordingSettings.fromDictionary(modifiedOptions)
210
-
210
+
211
211
  switch settingsResult {
212
212
  case .success(let settings):
213
- Logger.debug("ExpoAudioStreamModule", "startRecording: Settings parsed successfully. ShowNotification=\(settings.showNotification)")
214
213
  // Initialize notification if enabled
215
214
  if settings.showNotification {
216
215
  Task {
@@ -220,8 +219,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDev
220
219
  }
221
220
  }
222
221
  }
223
-
224
- Logger.debug("ExpoAudioStreamModule", "startRecording: Calling streamManager.startRecording")
222
+
225
223
  if let result = self.streamManager.startRecording(settings: settings) {
226
224
  var resultDict: [String: Any] = [
227
225
  "fileUri": result.fileUri,
@@ -230,7 +228,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDev
230
228
  "sampleRate": result.sampleRate,
231
229
  "mimeType": result.mimeType,
232
230
  ]
233
-
231
+
234
232
  // Add compression info if available
235
233
  if let compression = result.compression {
236
234
  resultDict["compression"] = [
@@ -240,16 +238,16 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDev
240
238
  "format": compression.format
241
239
  ]
242
240
  }
243
-
244
- Logger.info("ExpoAudioStreamModule", "startRecording: Recording started successfully. fileUri: \(result.fileUri)")
241
+
242
+ Logger.info("ExpoAudioStreamModule", "Recording started successfully")
245
243
  promise.resolve(resultDict)
246
244
  } else {
247
- Logger.error("ExpoAudioStreamModule", "startRecording: streamManager.startRecording returned nil.")
245
+ Logger.error("ExpoAudioStreamModule", "Failed to start recording")
248
246
  promise.reject("ERROR", "Failed to start recording.")
249
247
  }
250
-
248
+
251
249
  case .failure(let error):
252
- Logger.error("ExpoAudioStreamModule", "startRecording: Invalid settings - \(error.localizedDescription)")
250
+ Logger.error("ExpoAudioStreamModule", "Invalid settings - \(error.localizedDescription)")
253
251
  promise.reject("INVALID_SETTINGS", error.localizedDescription)
254
252
  }
255
253
  }
@@ -918,9 +916,9 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDev
918
916
  private func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
919
917
  switch AVAudioSession.sharedInstance().recordPermission {
920
918
  case .granted:
921
- completion(true)
919
+ DispatchQueue.main.async { completion(true) }
922
920
  case .denied:
923
- completion(false)
921
+ DispatchQueue.main.async { completion(false) }
924
922
  case .undetermined:
925
923
  AVAudioSession.sharedInstance().requestRecordPermission { granted in
926
924
  DispatchQueue.main.async {
@@ -928,7 +926,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDev
928
926
  }
929
927
  }
930
928
  @unknown default:
931
- completion(false)
929
+ DispatchQueue.main.async { completion(false) }
932
930
  }
933
931
  }
934
932
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteed/expo-audio-studio",
3
- "version": "2.18.2",
3
+ "version": "2.18.5",
4
4
  "description": "Comprehensive audio processing library for React Native and Expo with recording, analysis, visualization, and streaming capabilities across iOS, Android, and web",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -96,13 +96,12 @@
96
96
  "open:android": "open -a \"Android Studio\" ../../apps/playground/android",
97
97
  "size": "bundle-size && size-limit",
98
98
  "release": "./publish.sh",
99
- "agent:validate": "cd ../../apps/playground && yarn agent:validate",
100
99
  "agent:test:unit": "yarn test:android:unit",
101
100
  "agent:test:integration": "yarn test:android:instrumented",
102
101
  "agent:compilation:check": "yarn typecheck && yarn build"
103
102
  },
104
103
  "devDependencies": {
105
- "@expo/config-plugins": "~10.0.0",
104
+ "@expo/config-plugins": "~54.0.0",
106
105
  "@expo/npm-proofread": "^1.0.1",
107
106
  "@siteed/publisher": "^0.4.18",
108
107
  "@size-limit/preset-big-lib": "^11.1.4",
@@ -119,13 +118,13 @@
119
118
  "eslint-plugin-prettier": "^5.1.3",
120
119
  "eslint-plugin-promise": "^6.1.1",
121
120
  "eslint-plugin-react": "^7.34.1",
122
- "expo": "^53.0.9",
121
+ "expo": "^54.0.0",
123
122
  "expo-module-scripts": "^4.1.7",
124
- "expo-modules-core": "2.4.0",
123
+ "expo-modules-core": "~3.0.0",
125
124
  "jest": "^29.7.0",
126
125
  "prettier": "^3.2.5",
127
- "react": "19.0.0",
128
- "react-native": "0.79.3",
126
+ "react": "19.1.0",
127
+ "react-native": "0.81.5",
129
128
  "rimraf": "^6.0.1",
130
129
  "size-limit": "^11.1.4",
131
130
  "ts-node": "^10.9.2",
@@ -15,6 +15,7 @@ import {
15
15
  DecodingConfig,
16
16
  } from './AudioAnalysis.types'
17
17
  import { processAudioBuffer } from '../utils/audioProcessing'
18
+ import { cleanNativeOptions } from '../utils/cleanNativeOptions'
18
19
  import { convertPCMToFloat32 } from '../utils/convertPCMToFloat32'
19
20
  import crc32 from '../utils/crc32'
20
21
  import { getWavFileInfo, WavFileInfo } from '../utils/getWavFileInfo'
@@ -209,7 +210,17 @@ export async function extractAudioAnalysis(
209
210
  throw error
210
211
  }
211
212
  } else {
212
- return await ExpoAudioStreamModule.extractAudioAnalysis(props)
213
+ // Strip non-serializable fields — logger and arrayBuffer cause
214
+ // "Cannot convert '[object Object]' to a Kotlin type" on Android.
215
+ const {
216
+ logger: _logger,
217
+ arrayBuffer: _arrayBuffer,
218
+ ...nativeOptions
219
+ } = props
220
+ // Clean undefined values to avoid Android Kotlin bridge crash
221
+ return await ExpoAudioStreamModule.extractAudioAnalysis(
222
+ cleanNativeOptions(nativeOptions)
223
+ )
213
224
  }
214
225
  }
215
226
 
@@ -1,6 +1,17 @@
1
1
  import { ExtractAudioDataOptions } from '../ExpoAudioStream.types'
2
2
  import ExpoAudioStreamModule from '../ExpoAudioStreamModule'
3
+ import { isWeb } from '../constants'
4
+ import { cleanNativeOptions } from '../utils/cleanNativeOptions'
3
5
 
4
6
  export const extractAudioData = async (props: ExtractAudioDataOptions) => {
5
- return await ExpoAudioStreamModule.extractAudioData(props)
7
+ if (isWeb) {
8
+ // Web implementation handles logger natively in ExpoAudioStreamModule.ts
9
+ return await ExpoAudioStreamModule.extractAudioData(props)
10
+ }
11
+ // Native: only pass serializable fields — logger causes crash on Android
12
+ const { logger: _logger, ...nativeOptions } = props
13
+ // Clean undefined values to avoid Android Kotlin bridge crash
14
+ return await ExpoAudioStreamModule.extractAudioData(
15
+ cleanNativeOptions(nativeOptions)
16
+ )
6
17
  }
@@ -13,6 +13,7 @@ import {
13
13
  processAudioBuffer,
14
14
  ProcessedAudioData,
15
15
  } from '../utils/audioProcessing'
16
+ import { cleanNativeOptions } from '../utils/cleanNativeOptions'
16
17
 
17
18
  /**
18
19
  * Extracts a mel spectrogram from audio data
@@ -100,7 +101,16 @@ export async function extractMelSpectrogram(
100
101
  await audioContext.close()
101
102
  }
102
103
  }
103
- return ExpoAudioStreamModule.extractMelSpectrogram(options)
104
+ // Strip logger/arrayBuffer (non-serializable) then clean undefined values
105
+ // to avoid Android "Cannot convert '[object Object]' to Kotlin type" crash
106
+ const {
107
+ logger: _logger,
108
+ arrayBuffer: _arrayBuffer,
109
+ ...nativeOptions
110
+ } = options
111
+ return ExpoAudioStreamModule.extractMelSpectrogram(
112
+ cleanNativeOptions(nativeOptions)
113
+ )
104
114
  }
105
115
 
106
116
  /**
package/src/trimAudio.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  TrimProgressEvent,
7
7
  } from './ExpoAudioStream.types'
8
8
  import ExpoAudioStreamModule from './ExpoAudioStreamModule'
9
+ import { cleanNativeOptions } from './utils/cleanNativeOptions'
9
10
 
10
11
  // Create a single emitter instance
11
12
  const emitter = new LegacyEventEmitter(ExpoAudioStreamModule)
@@ -63,7 +64,10 @@ export async function trimAudio(
63
64
  }
64
65
 
65
66
  try {
66
- const result = await ExpoAudioStreamModule.trimAudio(options)
67
+ // Clean non-serializable/undefined values to avoid Android Kotlin bridge crash
68
+ const result = await ExpoAudioStreamModule.trimAudio(
69
+ cleanNativeOptions(options)
70
+ )
67
71
  return result
68
72
  } finally {
69
73
  if (subscription) {
@@ -14,12 +14,14 @@ import {
14
14
  StartRecordingResult,
15
15
  } from './ExpoAudioStream.types'
16
16
  import ExpoAudioStreamModule from './ExpoAudioStreamModule'
17
+ import { validateRecordingConfig } from './constants/platformLimitations'
17
18
  import {
18
19
  addAudioAnalysisListener,
19
20
  addAudioEventListener,
20
21
  AudioEventPayload,
21
22
  addRecordingInterruptionListener,
22
23
  } from './events'
24
+ import { cleanNativeOptions } from './utils/cleanNativeOptions'
23
25
 
24
26
  export interface UseAudioRecorderProps {
25
27
  logger?: ConsoleLike
@@ -473,11 +475,6 @@ export function useAudioRecorder({
473
475
 
474
476
  const startRecording = useCallback(
475
477
  async (recordingOptions: RecordingConfig) => {
476
- // Import validation function
477
- const { validateRecordingConfig } = await import(
478
- './constants/platformLimitations'
479
- )
480
-
481
478
  // Validate the encoding configuration
482
479
  const validationResult = validateRecordingConfig({
483
480
  encoding: recordingOptions.encoding,
@@ -519,8 +516,10 @@ export function useAudioRecorder({
519
516
  logger?.warn(`onAudioStream is not a function`, onAudioStream)
520
517
  onAudioStreamRef.current = null
521
518
  }
519
+ // Strip undefined values and functions that can't cross the native bridge
520
+ const cleanOptions = cleanNativeOptions(options)
522
521
  const startResult: StartRecordingResult =
523
- await ExpoAudioStream.startRecording(options)
522
+ await ExpoAudioStream.startRecording(cleanOptions)
524
523
  dispatch({ type: 'START' })
525
524
 
526
525
  startResultRef.current = startResult
@@ -573,8 +572,10 @@ export function useAudioRecorder({
573
572
  onAudioStreamRef.current = null
574
573
  }
575
574
 
575
+ // Strip undefined values and functions that can't cross the native bridge
576
+ const cleanOptions = cleanNativeOptions(options)
576
577
  // Call the native prepareRecording method
577
- await ExpoAudioStream.prepareRecording(options)
578
+ await ExpoAudioStream.prepareRecording(cleanOptions)
578
579
  logger?.debug(`recording prepared successfully`)
579
580
  },
580
581
  []
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Strips non-serializable values (functions, ArrayBuffer, undefined) from
3
+ * option objects before passing them to Expo native modules.
4
+ *
5
+ * Android's Kotlin bridge crashes with "Cannot convert '[object Object]' to a
6
+ * Kotlin type" when it receives non-plain values such as `logger`, `ArrayBuffer`,
7
+ * or `undefined` fields. The JSON round-trip removes all of these safely.
8
+ *
9
+ * Only use this for small config objects (never for large audio buffers).
10
+ *
11
+ * NOTE: structuredClone() is intentionally NOT used here — it preserves
12
+ * undefined values and non-JSON types, which is exactly what we need to strip.
13
+ */
14
+ export function cleanNativeOptions<T>(options: T): T {
15
+ // NOSONAR: JSON round-trip is deliberate — it strips undefined, functions,
16
+ // and non-serializable values that structuredClone would preserve.
17
+ return JSON.parse(JSON.stringify(options)) // NOSONAR
18
+ }