@siteed/expo-audio-studio 2.5.0 → 2.6.1

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 (35) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +3 -3
  3. package/build/AudioDeviceManager.d.ts +1 -1
  4. package/build/AudioDeviceManager.js +1 -1
  5. package/build/AudioDeviceManager.js.map +1 -1
  6. package/build/ExpoAudioStream.types.d.ts +19 -1
  7. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  8. package/build/ExpoAudioStream.types.js.map +1 -1
  9. package/build/ExpoAudioStream.web.d.ts.map +1 -1
  10. package/build/ExpoAudioStream.web.js +80 -9
  11. package/build/ExpoAudioStream.web.js.map +1 -1
  12. package/build/WebRecorder.web.d.ts +14 -4
  13. package/build/WebRecorder.web.d.ts.map +1 -1
  14. package/build/WebRecorder.web.js +121 -14
  15. package/build/WebRecorder.web.js.map +1 -1
  16. package/build/useAudioRecorder.d.ts.map +1 -1
  17. package/build/useAudioRecorder.js +1 -1
  18. package/build/useAudioRecorder.js.map +1 -1
  19. package/build/utils/writeWavHeader.d.ts +3 -18
  20. package/build/utils/writeWavHeader.d.ts.map +1 -1
  21. package/build/utils/writeWavHeader.js +19 -26
  22. package/build/utils/writeWavHeader.js.map +1 -1
  23. package/ios/AudioDeviceManager.swift +65 -65
  24. package/ios/AudioProcessor.swift +32 -32
  25. package/ios/AudioStreamManager.swift +323 -158
  26. package/ios/ExpoAudioStreamModule.swift +92 -75
  27. package/ios/ISSUE_IOS.md +26 -3
  28. package/ios/Logger.swift +27 -7
  29. package/package.json +1 -1
  30. package/src/AudioDeviceManager.ts +1 -1
  31. package/src/ExpoAudioStream.types.ts +21 -1
  32. package/src/ExpoAudioStream.web.ts +99 -9
  33. package/src/WebRecorder.web.ts +146 -21
  34. package/src/useAudioRecorder.tsx +1 -2
  35. package/src/utils/writeWavHeader.ts +26 -25
@@ -39,12 +39,12 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
39
39
  ])
40
40
 
41
41
  OnCreate {
42
- Logger.debug("[ExpoAudioStreamModule] Module created, setting delegate and starting device monitoring.")
42
+ Logger.debug("ExpoAudioStreamModule", "Module created, setting delegate and starting device monitoring.")
43
43
  streamManager.delegate = self
44
44
  }
45
45
 
46
46
  OnDestroy {
47
- Logger.debug("[ExpoAudioStreamModule] Module destroyed, stopping device monitoring.")
47
+ Logger.debug("ExpoAudioStreamModule", "Module destroyed, stopping device monitoring.")
48
48
  _ = streamManager.stopRecording()
49
49
  }
50
50
 
@@ -58,10 +58,10 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
58
58
  /// - promise: A promise to resolve with the extracted audio analysis data or reject with an error.
59
59
  /// - Returns: Promise to be resolved with audio analysis data.
60
60
  AsyncFunction("extractAudioAnalysis") { (options: [String: Any], promise: Promise) in
61
- Logger.debug("[ExpoAudioStreamModule] extractAudioAnalysis called with options: \(options)")
61
+ Logger.debug("ExpoAudioStreamModule", "extractAudioAnalysis called with options: \(options)")
62
62
  guard let fileUri = options["fileUri"] as? String,
63
63
  let url = URL(string: fileUri) else {
64
- Logger.error("[ExpoAudioStreamModule] extractAudioAnalysis: Invalid file URI.")
64
+ Logger.error("ExpoAudioStreamModule", "extractAudioAnalysis: Invalid file URI.")
65
65
  promise.reject("INVALID_ARGUMENTS", "Invalid file URI provided")
66
66
  return
67
67
  }
@@ -106,11 +106,11 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
106
106
  effectiveLength = byteLength
107
107
  }
108
108
 
109
- Logger.debug("[ExpoAudioStreamModule] extractAudioAnalysis: Processing started for \(fileUri)")
109
+ Logger.debug("ExpoAudioStreamModule", "extractAudioAnalysis: Processing started for \(fileUri)")
110
110
  let audioProcessor = try AudioProcessor(url: url, resolve: { result in
111
- Logger.warn("[ExpoAudioStreamModule] extractAudioAnalysis: AudioProcessor resolve called unexpectedly.")
111
+ Logger.warn("ExpoAudioStreamModule", "extractAudioAnalysis: AudioProcessor resolve called unexpectedly.")
112
112
  }, reject: { code, message in
113
- Logger.warn("[ExpoAudioStreamModule] extractAudioAnalysis: AudioProcessor reject called unexpectedly: \(code) - \(message)")
113
+ Logger.warn("ExpoAudioStreamModule", "extractAudioAnalysis: AudioProcessor reject called unexpectedly: \(code) - \(message)")
114
114
  })
115
115
 
116
116
  if let result = audioProcessor.processAudioData(
@@ -124,14 +124,14 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
124
124
  position: effectivePosition,
125
125
  byteLength: effectiveLength
126
126
  ) {
127
- Logger.debug("[ExpoAudioStreamModule] extractAudioAnalysis: Processing successful for \(fileUri)")
127
+ Logger.debug("ExpoAudioStreamModule", "extractAudioAnalysis: Processing successful for \(fileUri)")
128
128
  promise.resolve(result.toDictionary())
129
129
  } else {
130
- Logger.error("[ExpoAudioStreamModule] extractAudioAnalysis: audioProcessor.processAudioData returned nil for \(fileUri)")
130
+ Logger.error("ExpoAudioStreamModule", "extractAudioAnalysis: audioProcessor.processAudioData returned nil for \(fileUri)")
131
131
  promise.reject("PROCESSING_ERROR", "Failed to process audio data")
132
132
  }
133
133
  } catch {
134
- Logger.error("[ExpoAudioStreamModule] extractAudioAnalysis: Error initializing AudioProcessor for \(fileUri): \(error.localizedDescription)")
134
+ Logger.error("ExpoAudioStreamModule", "extractAudioAnalysis: Error initializing AudioProcessor for \(fileUri): \(error.localizedDescription)")
135
135
  promise.reject("PROCESSING_ERROR", "Failed to initialize audio processor: \(error.localizedDescription)")
136
136
  }
137
137
  })
@@ -158,31 +158,48 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
158
158
  /// - `bitrate`: The compression bitrate in bps (default is 128000).
159
159
  /// - promise: A promise to resolve with the recording settings or reject with an error.
160
160
  AsyncFunction("startRecording") { (options: [String: Any], promise: Promise) in
161
- Logger.debug("[ExpoAudioStreamModule] startRecording called with options: \(options)")
161
+ Logger.debug("ExpoAudioStreamModule", "startRecording called with options: \(options)")
162
162
  self.checkMicrophonePermission { granted in
163
163
  guard granted else {
164
- Logger.warn("[ExpoAudioStreamModule] startRecording: Permission denied.")
164
+ Logger.warn("ExpoAudioStreamModule", "startRecording: Permission denied.")
165
165
  promise.reject("PERMISSION_DENIED", "Recording permission has not been granted")
166
166
  return
167
167
  }
168
168
 
169
- // Create settings with validation
170
- let settingsResult = RecordingSettings.fromDictionary(options)
169
+ // Check if compression is enabled and format is Opus
170
+ var modifiedOptions = options
171
+ if let compression = options["compression"] as? [String: Any],
172
+ let enabled = compression["enabled"] as? Bool, enabled,
173
+ let format = compression["format"] as? String,
174
+ format.lowercased() == "opus" {
175
+
176
+ // Create a mutable copy of the compression dictionary
177
+ var modifiedCompression = compression
178
+
179
+ // Change format to AAC and log warning
180
+ modifiedCompression["format"] = "aac"
181
+ modifiedOptions["compression"] = modifiedCompression
182
+
183
+ Logger.warn("ExpoAudioStreamModule", "startRecording: Opus format is not supported on iOS. Falling back to AAC format.")
184
+ }
185
+
186
+ // Create settings with validation using the potentially modified options
187
+ let settingsResult = RecordingSettings.fromDictionary(modifiedOptions)
171
188
 
172
189
  switch settingsResult {
173
190
  case .success(let settings):
174
- Logger.debug("[ExpoAudioStreamModule] startRecording: Settings parsed successfully. ShowNotification=\(settings.showNotification)")
191
+ Logger.debug("ExpoAudioStreamModule", "startRecording: Settings parsed successfully. ShowNotification=\(settings.showNotification)")
175
192
  // Initialize notification if enabled
176
193
  if settings.showNotification {
177
194
  Task {
178
195
  let notificationGranted = await self.requestNotificationPermissions()
179
196
  if !notificationGranted {
180
- Logger.debug("Notification permissions not granted")
197
+ Logger.debug("ExpoAudioStreamModule", "Notification permissions not granted")
181
198
  }
182
199
  }
183
200
  }
184
201
 
185
- Logger.debug("[ExpoAudioStreamModule] startRecording: Calling streamManager.startRecording")
202
+ Logger.debug("ExpoAudioStreamModule", "startRecording: Calling streamManager.startRecording")
186
203
  if let result = self.streamManager.startRecording(settings: settings) {
187
204
  var resultDict: [String: Any] = [
188
205
  "fileUri": result.fileUri,
@@ -202,15 +219,15 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
202
219
  ]
203
220
  }
204
221
 
205
- Logger.info("[ExpoAudioStreamModule] startRecording: Recording started successfully. fileUri: \(result.fileUri)")
222
+ Logger.info("ExpoAudioStreamModule", "startRecording: Recording started successfully. fileUri: \(result.fileUri)")
206
223
  promise.resolve(resultDict)
207
224
  } else {
208
- Logger.error("[ExpoAudioStreamModule] startRecording: streamManager.startRecording returned nil.")
225
+ Logger.error("ExpoAudioStreamModule", "startRecording: streamManager.startRecording returned nil.")
209
226
  promise.reject("ERROR", "Failed to start recording.")
210
227
  }
211
228
 
212
229
  case .failure(let error):
213
- Logger.error("[ExpoAudioStreamModule] startRecording: Invalid settings - \(error.localizedDescription)")
230
+ Logger.error("ExpoAudioStreamModule", "startRecording: Invalid settings - \(error.localizedDescription)")
214
231
  promise.reject("INVALID_SETTINGS", error.localizedDescription)
215
232
  }
216
233
  }
@@ -221,7 +238,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
221
238
  /// - Returns: The current status of the audio stream.Ï
222
239
  Function("status") {
223
240
  let currentStatus = self.streamManager.getStatus()
224
- Logger.debug("[ExpoAudioStreamModule] status requested: isRecording=\(currentStatus["isRecording"] ?? false), isPaused=\(currentStatus["isPaused"] ?? false)")
241
+ Logger.debug("ExpoAudioStreamModule", "status requested: isRecording=\(currentStatus["isRecording"] ?? false), isPaused=\(currentStatus["isPaused"] ?? false)")
225
242
  return currentStatus
226
243
  }
227
244
 
@@ -232,7 +249,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
232
249
  /// - promise: A promise to resolve with true if preparation was successful.
233
250
  /// - Returns: A promise that resolves with a boolean indicating success.
234
251
  AsyncFunction("prepareRecording") { (options: [String: Any], promise: Promise) in
235
- Logger.debug("[ExpoAudioStreamModule] prepareRecording called with options: \(options)")
252
+ Logger.debug("ExpoAudioStreamModule", "prepareRecording called with options: \(options)")
236
253
  self.checkMicrophonePermission { granted in
237
254
  guard granted else {
238
255
  promise.reject("PERMISSION_DENIED", "Recording permission has not been granted")
@@ -244,12 +261,12 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
244
261
 
245
262
  switch settingsResult {
246
263
  case .success(let settings):
247
- Logger.debug("[ExpoAudioStreamModule] prepareRecording: Settings parsed successfully. Calling streamManager.prepareRecording")
264
+ Logger.debug("ExpoAudioStreamModule", "prepareRecording: Settings parsed successfully. Calling streamManager.prepareRecording")
248
265
  if self.streamManager.prepareRecording(settings: settings) {
249
- Logger.info("[ExpoAudioStreamModule] prepareRecording: Preparation successful.")
266
+ Logger.info("ExpoAudioStreamModule", "prepareRecording: Preparation successful.")
250
267
  promise.resolve(true)
251
268
  } else {
252
- Logger.error("[ExpoAudioStreamModule] prepareRecording: streamManager.prepareRecording returned false.")
269
+ Logger.error("ExpoAudioStreamModule", "prepareRecording: streamManager.prepareRecording returned false.")
253
270
  promise.reject("ERROR", "Failed to prepare recording.")
254
271
  }
255
272
 
@@ -261,13 +278,13 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
261
278
 
262
279
  /// Pauses audio recording.
263
280
  Function("pauseRecording") {
264
- Logger.debug("[ExpoAudioStreamModule] pauseRecording called.")
281
+ Logger.debug("ExpoAudioStreamModule", "pauseRecording called.")
265
282
  self.streamManager.pauseRecording()
266
283
  }
267
284
 
268
285
  /// Resumes audio recording.
269
286
  Function("resumeRecording") {
270
- Logger.debug("[ExpoAudioStreamModule] resumeRecording called.")
287
+ Logger.debug("ExpoAudioStreamModule", "resumeRecording called.")
271
288
  self.streamManager.resumeRecording()
272
289
  }
273
290
 
@@ -276,7 +293,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
276
293
  /// - Parameters:
277
294
  /// - promise: A promise to resolve with the recording result or reject with an error.
278
295
  AsyncFunction("stopRecording") { (promise: Promise) in
279
- Logger.debug("[ExpoAudioStreamModule] stopRecording called.")
296
+ Logger.debug("ExpoAudioStreamModule", "stopRecording called.")
280
297
  if let recordingResult = self.streamManager.stopRecording() {
281
298
  var resultDict: [String: Any] = [
282
299
  "fileUri": recordingResult.fileUri,
@@ -301,10 +318,10 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
301
318
  ]
302
319
  }
303
320
 
304
- Logger.info("[ExpoAudioStreamModule] stopRecording: Recording stopped successfully. fileUri: \(recordingResult.fileUri), size: \(recordingResult.size)")
321
+ Logger.info("ExpoAudioStreamModule", "stopRecording: Recording stopped successfully. fileUri: \(recordingResult.fileUri), size: \(recordingResult.size)")
305
322
  promise.resolve(resultDict)
306
323
  } else {
307
- Logger.error("[ExpoAudioStreamModule] stopRecording: streamManager.stopRecording returned nil.")
324
+ Logger.error("ExpoAudioStreamModule", "stopRecording: streamManager.stopRecording returned nil.")
308
325
  promise.reject("ERROR", "Failed to stop recording or no recording in progress.")
309
326
  }
310
327
  }
@@ -315,15 +332,15 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
315
332
  /// - promise: A promise to resolve with the list of audio file URIs or reject with an error.
316
333
  /// - Returns: A promise that resolves with the list of audio file URIs or rejects with an error.
317
334
  AsyncFunction("listAudioFiles") { (promise: Promise) in
318
- Logger.debug("[ExpoAudioStreamModule] listAudioFiles called.")
335
+ Logger.debug("ExpoAudioStreamModule", "listAudioFiles called.")
319
336
  let files = listAudioFiles()
320
- Logger.debug("[ExpoAudioStreamModule] listAudioFiles returning \(files.count) files.")
337
+ Logger.debug("ExpoAudioStreamModule", "listAudioFiles returning \(files.count) files.")
321
338
  promise.resolve(files)
322
339
  }
323
340
 
324
341
  /// Clears all audio files stored in the document directory.
325
342
  Function("clearAudioFiles") {
326
- Logger.debug("[ExpoAudioStreamModule] clearAudioFiles called.")
343
+ Logger.debug("ExpoAudioStreamModule", "clearAudioFiles called.")
327
344
  clearAudioFiles()
328
345
  }
329
346
 
@@ -415,14 +432,14 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
415
432
  let decodingOptions = options["decodingOptions"] as? [String: Any] ?? [:]
416
433
 
417
434
  // Add detailed logging for filename and format options
418
- Logger.debug("Trim audio request:")
419
- Logger.debug("- Input file: \(fileUri)")
420
- Logger.debug("- Mode: \(mode)")
421
- Logger.debug("- Output filename: \(outputFileName ?? "not specified (will generate UUID)")")
435
+ Logger.debug("ExpoAudioStreamModule", "Trim audio request:")
436
+ Logger.debug("ExpoAudioStreamModule", "- Input file: \(fileUri)")
437
+ Logger.debug("ExpoAudioStreamModule", "- Mode: \(mode)")
438
+ Logger.debug("ExpoAudioStreamModule", "- Output filename: \(outputFileName ?? "not specified (will generate UUID)")")
422
439
  if let format = outputFormat?["format"] as? String {
423
- Logger.debug("- Output format: \(format)")
440
+ Logger.debug("ExpoAudioStreamModule", "- Output format: \(format)")
424
441
  } else {
425
- Logger.debug("- Output format: not specified (will use default)")
442
+ Logger.debug("ExpoAudioStreamModule", "- Output format: not specified (will use default)")
426
443
  }
427
444
 
428
445
  // Input validation based on mode
@@ -478,28 +495,28 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
478
495
  resultDict["processingInfo"] = ["durationMs": processingTimeMs]
479
496
 
480
497
  let uri = result.uri
481
- Logger.debug("Trim completed successfully in \(processingTimeMs)ms")
482
- Logger.debug("Output file URI: \(uri)")
498
+ Logger.debug("ExpoAudioStreamModule", "Trim completed successfully in \(processingTimeMs)ms")
499
+ Logger.debug("ExpoAudioStreamModule", "Output file URI: \(uri)")
483
500
 
484
501
  // Verify file exists
485
502
  let fileManager = FileManager.default
486
503
  if let url = URL(string: uri) {
487
504
  let exists = fileManager.fileExists(atPath: url.path)
488
- Logger.debug("File exists at path \(url.path): \(exists)")
505
+ Logger.debug("ExpoAudioStreamModule", "File exists at path \(url.path): \(exists)")
489
506
 
490
507
  // Log filename details
491
- Logger.debug("Filename: \(url.lastPathComponent)")
492
- Logger.debug("File extension: \(url.pathExtension.lowercased())")
508
+ Logger.debug("ExpoAudioStreamModule", "Filename: \(url.lastPathComponent)")
509
+ Logger.debug("ExpoAudioStreamModule", "File extension: \(url.pathExtension.lowercased())")
493
510
 
494
511
  // If format is AAC, ensure we're using the correct extension and MIME type
495
512
  if let format = outputFormat?["format"] as? String,
496
513
  format.lowercased() == "aac" {
497
514
 
498
- Logger.debug("AAC format detected - ensuring correct metadata")
515
+ Logger.debug("ExpoAudioStreamModule", "AAC format detected - ensuring correct metadata")
499
516
 
500
517
  // For AAC format, ensure we're using the correct extension and MIME type
501
518
  if url.pathExtension.lowercased() == "m4a" {
502
- Logger.debug("File has correct m4a extension for AAC audio")
519
+ Logger.debug("ExpoAudioStreamModule", "File has correct m4a extension for AAC audio")
503
520
 
504
521
  // Just update the MIME type in the result to ensure correct playback
505
522
  if var compression = resultDict["compression"] as? [String: Any] {
@@ -510,18 +527,18 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
510
527
  resultDict["mimeType"] = "audio/mp4"
511
528
  resultDict["actualFormat"] = "m4a"
512
529
  } else {
513
- Logger.debug("Warning: AAC format should use .m4a extension, but found .\(url.pathExtension.lowercased())")
530
+ Logger.debug("ExpoAudioStreamModule", "Warning: AAC format should use .m4a extension, but found .\(url.pathExtension.lowercased())")
514
531
  }
515
532
  }
516
533
  }
517
534
 
518
535
  promise.resolve(resultDict)
519
536
  } else {
520
- Logger.debug("Failed to trim audio")
537
+ Logger.debug("ExpoAudioStreamModule", "Failed to trim audio")
521
538
  promise.reject("TRIM_ERROR", "Failed to trim audio")
522
539
  }
523
540
  } catch {
524
- Logger.debug("Failed to initialize audio processor: \(error.localizedDescription)")
541
+ Logger.debug("ExpoAudioStreamModule", "Failed to initialize audio processor: \(error.localizedDescription)")
525
542
  promise.reject("PROCESSING_ERROR", "Failed to initialize audio processor: \(error.localizedDescription)")
526
543
  }
527
544
  }
@@ -649,7 +666,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
649
666
  let checksum = calculateCRC32(data: pcmData)
650
667
  resultDict["checksum"] = Int(checksum)
651
668
 
652
- Logger.debug("Computed CRC32 checksum: \(checksum)")
669
+ Logger.debug("ExpoAudioStreamModule", "Computed CRC32 checksum: \(checksum)")
653
670
  }
654
671
 
655
672
  if let includeBase64Data = options["includeBase64Data"] as? Bool, includeBase64Data {
@@ -685,9 +702,9 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
685
702
  /// - options: Optional dictionary containing a refresh parameter
686
703
  /// - promise: A promise to resolve with a list of available audio input devices
687
704
  AsyncFunction("getAvailableInputDevices") { (options: [String: Any]?, promise: Promise) in
688
- Logger.debug("[ExpoAudioStreamModule] getAvailableInputDevices called. Refresh: \(options?["refresh"] ?? false)")
705
+ Logger.debug("ExpoAudioStreamModule", "getAvailableInputDevices called. Refresh: \(options?["refresh"] ?? false)")
689
706
  if let options = options, let refresh = options["refresh"] as? Bool, refresh {
690
- Logger.debug("Forcing refresh of audio devices")
707
+ Logger.debug("ExpoAudioStreamModule", "Forcing refresh of audio devices")
691
708
  self.deviceManager.forceRefreshAudioSession()
692
709
  }
693
710
 
@@ -698,9 +715,9 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
698
715
  /// Refreshes the audio session to detect newly connected devices
699
716
  /// - Returns: Boolean indicating success
700
717
  Function("refreshAudioDevices") {
701
- Logger.debug("[ExpoAudioStreamModule] refreshAudioDevices called.")
718
+ Logger.debug("ExpoAudioStreamModule", "refreshAudioDevices called.")
702
719
  let success = self.deviceManager.forceRefreshAudioSession()
703
- Logger.debug("[ExpoAudioStreamModule] refreshAudioDevices result: \(success)")
720
+ Logger.debug("ExpoAudioStreamModule", "refreshAudioDevices result: \(success)")
704
721
  return ["success": success]
705
722
  }
706
723
 
@@ -709,7 +726,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
709
726
  /// - Parameters:
710
727
  /// - promise: A promise to resolve with the currently selected audio input device
711
728
  AsyncFunction("getCurrentInputDevice") { (promise: Promise) in
712
- Logger.debug("[ExpoAudioStreamModule] getCurrentInputDevice called.")
729
+ Logger.debug("ExpoAudioStreamModule", "getCurrentInputDevice called.")
713
730
  self.deviceManager.getCurrentInputDevice(promise: promise)
714
731
  }
715
732
 
@@ -719,14 +736,14 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
719
736
  /// - deviceId: The ID of the device to select
720
737
  /// - promise: A promise to resolve with boolean indicating success
721
738
  AsyncFunction("selectInputDevice") { (deviceId: String, promise: Promise) in
722
- Logger.debug("[ExpoAudioStreamModule] selectInputDevice called with ID: \(deviceId)")
739
+ Logger.debug("ExpoAudioStreamModule", "selectInputDevice called with ID: \(deviceId)")
723
740
  self.deviceManager.selectInputDevice(deviceId, promise: promise)
724
741
  // Update the audio recorder if recording is in progress or prepared
725
742
  if self.streamManager.isRecording || self.streamManager.isPrepared {
726
- Logger.debug("[ExpoAudioStreamModule] selectInputDevice: Calling updateAudioSessionWithCurrentSettings because recording/prepared.")
743
+ Logger.debug("ExpoAudioStreamModule", "selectInputDevice: Calling updateAudioSessionWithCurrentSettings because recording/prepared.")
727
744
  self.streamManager.updateAudioSessionWithCurrentSettings()
728
745
  } else {
729
- Logger.debug("[ExpoAudioStreamModule] selectInputDevice: Not calling updateAudioSessionWithCurrentSettings because not recording/prepared.")
746
+ Logger.debug("ExpoAudioStreamModule", "selectInputDevice: Not calling updateAudioSessionWithCurrentSettings because not recording/prepared.")
730
747
  }
731
748
  }
732
749
 
@@ -735,18 +752,18 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
735
752
  /// - Parameters:
736
753
  /// - promise: A promise to resolve with boolean indicating success
737
754
  AsyncFunction("resetToDefaultDevice") { (promise: Promise) in
738
- Logger.debug("[ExpoAudioStreamModule] resetToDefaultDevice called.")
755
+ Logger.debug("ExpoAudioStreamModule", "resetToDefaultDevice called.")
739
756
  self.deviceManager.resetToDefaultDevice { success, error in
740
757
  if success {
741
758
  if self.streamManager.isRecording || self.streamManager.isPrepared {
742
- Logger.debug("[ExpoAudioStreamModule] resetToDefaultDevice: Calling updateAudioSessionWithCurrentSettings because recording/prepared.")
759
+ Logger.debug("ExpoAudioStreamModule", "resetToDefaultDevice: Calling updateAudioSessionWithCurrentSettings because recording/prepared.")
743
760
  self.streamManager.updateAudioSessionWithCurrentSettings()
744
761
  } else {
745
- Logger.debug("[ExpoAudioStreamModule] resetToDefaultDevice: Not calling updateAudioSessionWithCurrentSettings because not recording/prepared.")
762
+ Logger.debug("ExpoAudioStreamModule", "resetToDefaultDevice: Not calling updateAudioSessionWithCurrentSettings because not recording/prepared.")
746
763
  }
747
764
  promise.resolve(true)
748
765
  } else {
749
- Logger.error("[ExpoAudioStreamModule] resetToDefaultDevice failed: \(error?.localizedDescription ?? "Unknown error")")
766
+ Logger.error("ExpoAudioStreamModule", "resetToDefaultDevice failed: \(error?.localizedDescription ?? "Unknown error")")
750
767
  promise.reject("DEVICE_ERROR", "Failed to reset to default device: \(error?.localizedDescription ?? "Unknown error")")
751
768
  }
752
769
  }
@@ -754,7 +771,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
754
771
  }
755
772
 
756
773
  func audioStreamManager(_ manager: AudioStreamManager, didReceiveInterruption info: [String: Any]) {
757
- Logger.debug("[ExpoAudioStreamModule] Delegate: didReceiveInterruption: \(info)")
774
+ Logger.debug("ExpoAudioStreamModule", "Delegate: didReceiveInterruption: \(info)")
758
775
  // Convert iOS interruption events to match the TypeScript types
759
776
  var reason: String
760
777
  var isPaused: Bool = true
@@ -791,7 +808,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
791
808
  }
792
809
 
793
810
  func audioStreamManager(_ manager: AudioStreamManager, didPauseRecording pauseTime: Date) {
794
- Logger.debug("[ExpoAudioStreamModule] Delegate: didPauseRecording")
811
+ Logger.debug("ExpoAudioStreamModule", "Delegate: didPauseRecording")
795
812
  sendEvent(recordingInterruptedEvent, [
796
813
  "reason": "userPaused",
797
814
  "isPaused": true,
@@ -800,7 +817,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
800
817
  }
801
818
 
802
819
  func audioStreamManager(_ manager: AudioStreamManager, didResumeRecording resumeTime: Date) {
803
- Logger.debug("[ExpoAudioStreamModule] Delegate: didResumeRecording")
820
+ Logger.debug("ExpoAudioStreamModule", "Delegate: didResumeRecording")
804
821
  sendEvent(recordingInterruptedEvent, [
805
822
  "reason": "userResumed",
806
823
  "isPaused": false,
@@ -809,7 +826,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
809
826
  }
810
827
 
811
828
  func audioStreamManager(_ manager: AudioStreamManager, didUpdateNotificationState isPaused: Bool) {
812
- Logger.debug("[ExpoAudioStreamModule] Delegate: didUpdateNotificationState: isPaused=\(isPaused)")
829
+ Logger.debug("ExpoAudioStreamModule", "Delegate: didUpdateNotificationState: isPaused=\(isPaused)")
813
830
  sendEvent(recordingInterruptedEvent, [
814
831
  "reason": "notification",
815
832
  "isPaused": isPaused,
@@ -856,16 +873,16 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
856
873
  let options: UNAuthorizationOptions = [.alert, .sound]
857
874
  return try await notificationCenter.requestAuthorization(options: options)
858
875
  } catch {
859
- Logger.debug("Failed to request notification permissions: \(error)")
876
+ Logger.debug("ExpoAudioStreamModule", "Failed to request notification permissions: \(error)")
860
877
  return false
861
878
  }
862
879
  }
863
880
 
864
881
  func audioStreamManager(_ manager: AudioStreamManager, didReceiveProcessingResult result: AudioAnalysisData?) {
865
882
  if let data = result {
866
- Logger.debug("[ExpoAudioStreamModule] Delegate: didReceiveProcessingResult: Received \(data.dataPoints.count) data points.")
883
+ Logger.debug("ExpoAudioStreamModule", "Delegate: didReceiveProcessingResult: Received \(data.dataPoints.count) data points.")
867
884
  } else {
868
- Logger.debug("[ExpoAudioStreamModule] Delegate: didReceiveProcessingResult: Received nil result.")
885
+ Logger.debug("ExpoAudioStreamModule", "Delegate: didReceiveProcessingResult: Received nil result.")
869
886
  }
870
887
  let resultDict = result?.toDictionary() ?? [:]
871
888
  sendEvent(audioAnalysisEvent, resultDict)
@@ -899,12 +916,12 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
899
916
  if let fileURL = URL(string: fileURLString) {
900
917
  do {
901
918
  try FileManager.default.removeItem(at: fileURL)
902
- print("Removed file at:", fileURL.path)
919
+ print("ExpoAudioStreamModule", "Removed file at:", fileURL.path)
903
920
  } catch {
904
- print("Error removing file at \(fileURL.path):", error.localizedDescription)
921
+ print("ExpoAudioStreamModule", "Error removing file at \(fileURL.path):", error.localizedDescription)
905
922
  }
906
923
  } else {
907
- print("Invalid URL string: \(fileURLString)")
924
+ print("ExpoAudioStreamModule", "Invalid URL string: \(fileURLString)")
908
925
  }
909
926
  }
910
927
  }
@@ -941,7 +958,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
941
958
  /// - Returns: An array of file URIs as strings.
942
959
  func listAudioFiles() -> [String] {
943
960
  guard let documentDirectory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) else {
944
- print("Failed to access document directory.")
961
+ print("ExpoAudioStreamModule", "Failed to access document directory.")
945
962
  return []
946
963
  }
947
964
 
@@ -950,13 +967,13 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
950
967
  let audioFiles = files.filter { $0.pathExtension == "wav" }.map { $0.absoluteString }
951
968
  return audioFiles
952
969
  } catch {
953
- print("Error listing audio files:", error.localizedDescription)
970
+ print("ExpoAudioStreamModule", "Error listing audio files:", error.localizedDescription)
954
971
  return []
955
972
  }
956
973
  }
957
974
 
958
975
  func audioStreamManager(_ manager: AudioStreamManager, didFailWithError error: String) {
959
- Logger.error("[ExpoAudioStreamModule] Delegate: didFailWithError: \(error)")
976
+ Logger.error("ExpoAudioStreamModule", "Delegate: didFailWithError: \(error)")
960
977
  sendEvent(errorEvent, [ "message": error ])
961
978
  }
962
979
  }
package/ios/ISSUE_IOS.md CHANGED
@@ -21,6 +21,7 @@ Parallel compressed recording (e.g., AAC) functioned correctly even when the WAV
21
21
  5. **Background File I/O Refactor:** Improved WAV file writing by keeping the `FileHandle` open during recording instead of opening/closing for each buffer in the background queue. This fixed the partial file writing issue but didn't solve the core "no buffers received at 16kHz" problem.
22
22
  6. **Removing `setPreferredSampleRate`:** Tried removing the `session.setPreferredSampleRate` call, hoping the session would default to the hardware rate, allowing the 48kHz tap (based on `inputNode.outputFormat`) to receive buffers. This **worked** for the internal microphone but caused crashes with Bluetooth headsets, as the Bluetooth hardware *actually* operated at 16kHz, creating a new format mismatch.
23
23
  7. **Using `session.sampleRate`:** Attempted to use `session.sampleRate` *after* session activation to determine the tap format. This also proved unreliable, sometimes reporting 16kHz for the session while `inputNode.outputFormat` still reported 48kHz, leading back to the format mismatch crash.
24
+ 8. **Using `inputNode.inputFormat`:** After further analysis, discovered that `inputNode.inputFormat(forBus: 0)` gives the actual hardware input format, which may differ from the output format. This is the format that iOS strictly requires for a tap.
24
25
 
25
26
  ## Root Cause
26
27
 
@@ -28,6 +29,7 @@ The core issue stems from the unreliability and potential inconsistency between:
28
29
 
29
30
  * `AVAudioSession.sampleRate` (especially after `setPreferredSampleRate` or device changes).
30
31
  * `audioEngine.inputNode.outputFormat(forBus: 0)` (which might not immediately reflect the true hardware format).
32
+ * `audioEngine.inputNode.inputFormat(forBus: 0)` (which provides the hardware's actual input format).
31
33
  * The actual sample rate the hardware is delivering to the audio engine.
32
34
 
33
35
  Attempting to force a specific sample rate via `setPreferredSampleRate` or relying solely on `session.sampleRate` post-activation can lead to situations where the format used to install the tap does not match the format the audio engine expects/receives from the hardware input, causing either a crash (`Format mismatch`) or the tap simply not receiving any buffers.
@@ -38,8 +40,29 @@ The most robust solution was found to be:
38
40
 
39
41
  1. **Configure Session:** Set up the `AVAudioSession` category, mode, and options as required. **Do not** call `setPreferredSampleRate`. Let the session negotiate the rate with the hardware.
40
42
  2. **Activate Session:** Activate the audio session.
41
- 3. **Query Input Node Format (Just-In-Time):** Immediately **before** calling `installTap`, query the input node's expected format using `audioEngine.inputNode.outputFormat(forBus: 0)`. This appears to provide the most accurate format that the node *will accept* for the tap at that specific moment.
42
- 4. **Install Tap:** Install the tap using the exact `AVAudioFormat` obtained from `inputNode.outputFormat(forBus: 0)` in the previous step.
43
+ 3. **Query Hardware Input Format (Just-In-Time):** Immediately **before** calling `installTap`, query the hardware's input format using `audioEngine.inputNode.inputFormat(forBus: 0)`. This provides the actual format that the hardware is delivering and that iOS requires for the tap.
44
+ 4. **Install Tap:** Install the tap using the exact `AVAudioFormat` obtained from `inputNode.inputFormat(forBus: 0)` in the previous step.
43
45
  5. **Resample in Tap:** Inside the tap's processing closure (`processAudioBuffer`), check if the received `buffer.format.sampleRate` differs from the `settings.sampleRate` requested by the user. If they differ, perform the resampling explicitly using `AVAudioConverter` (or a similar method) before writing the WAV data or performing analysis.
44
46
 
45
- This approach ensures the tap format always matches what the input node requires at the time of installation, regardless of the device (internal mic, Bluetooth) or the requested output sample rate. Subsequent resampling handles the conversion to the user's desired format.
47
+ This approach ensures the tap format always matches what the hardware requires, regardless of the device (internal mic, Bluetooth) or the requested output sample rate. Subsequent resampling handles the conversion to the user's desired format.
48
+
49
+ ## Additional Findings: Device Disconnection Handling
50
+
51
+ During testing, we discovered an issue with device disconnection (particularly Bluetooth headsets):
52
+
53
+ 1. When a recording device disconnects, iOS reports a route change notification.
54
+ 2. If we attempt to resume recording after the device disconnection or switch to a new device, the app would crash with `Format mismatch: input hw <AVAudioFormat: 1 ch, 16000 Hz, Float32>, client format <AVAudioFormat: 1 ch, 48000 Hz, Float32>`.
55
+
56
+ We implemented a robust solution with the following components:
57
+
58
+ 1. **Hardware Format Detection:** Created a shared `installTapWithHardwareFormat` method that always queries the current hardware input format using `inputNode.inputFormat(forBus: 0)` before installing a tap.
59
+
60
+ 2. **Format Verification on Resume:** When resuming recording after a pause (potentially due to device disconnect), we reinstall the tap with the current hardware format.
61
+
62
+ 3. **Fallback Device Handling:** Implemented a configurable device disconnection behavior:
63
+ - `pause`: Pause recording when the current device disconnects (default)
64
+ - `fallback`: Automatically switch to the default device (built-in mic) and continue recording
65
+
66
+ 4. **Size Tracking Preservation:** Ensured that during device transitions, the total audio data size is preserved to maintain continuity in the recording.
67
+
68
+ These improvements ensure that when audio devices change during a recording session, the app can either pause gracefully or continue recording with a fallback device, without data loss or crashes due to format mismatches.
package/ios/Logger.swift CHANGED
@@ -1,19 +1,39 @@
1
1
  class Logger {
2
- static func debug(_ message: @autoclosure () -> String) {
2
+ // Similar to Android's TAG_PREFIX for consistent cross-platform logging
3
+ private static let TAG_PREFIX = "ExpoAudioStudio"
4
+
5
+ static func debug(_ className: String, _ message: @autoclosure () -> String) {
3
6
  #if DEBUG
4
- print("[DEBUG] \(message())")
7
+ print("[\(TAG_PREFIX):\(className)] [DEBUG] \(message())")
5
8
  #endif
6
9
  }
7
10
 
8
- static func info(_ message: @autoclosure () -> String) {
9
- print("[INFO] \(message())")
11
+ static func info(_ className: String, _ message: @autoclosure () -> String) {
12
+ print("[\(TAG_PREFIX):\(className)] [INFO] \(message())")
10
13
  }
11
14
 
12
- static func warn(_ message: @autoclosure () -> String) {
13
- print("[WARN] ⚠️ \(message())")
15
+ static func warn(_ className: String, _ message: @autoclosure () -> String) {
16
+ print("[\(TAG_PREFIX):\(className)] [WARN] ⚠️ \(message())")
14
17
  }
15
18
 
19
+ static func error(_ className: String, _ message: @autoclosure () -> String) {
20
+ print("[\(TAG_PREFIX):\(className)] [ERROR] 🛑 \(message())")
21
+ }
22
+
23
+ // For backward compatibility with code that doesn't specify a class name
24
+ static func debug(_ message: @autoclosure () -> String) {
25
+ debug("General", message())
26
+ }
27
+
28
+ static func info(_ message: @autoclosure () -> String) {
29
+ info("General", message())
30
+ }
31
+
32
+ static func warn(_ message: @autoclosure () -> String) {
33
+ warn("General", message())
34
+ }
35
+
16
36
  static func error(_ message: @autoclosure () -> String) {
17
- print("[ERROR] 🛑 \(message())")
37
+ error("General", message())
18
38
  }
19
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteed/expo-audio-studio",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
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
  "main": "build/index.js",
@@ -130,7 +130,7 @@ export class AudioDeviceManager {
130
130
 
131
131
  /**
132
132
  * Get all available audio input devices
133
- * @param options Optional settings { refresh: boolean } to force refresh the device list
133
+ * @param options Optional settings to force refresh the device list. Can include a refresh flag.
134
134
  * @returns Promise resolving to an array of audio devices conforming to AudioDevice interface
135
135
  */
136
136
  async getAvailableDevices(options?: {