@siteed/expo-audio-studio 2.6.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.
- package/CHANGELOG.md +5 -1
- package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +3 -3
- package/build/ExpoAudioStream.types.d.ts +5 -1
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/useAudioRecorder.d.ts.map +1 -1
- package/build/useAudioRecorder.js +1 -1
- package/build/useAudioRecorder.js.map +1 -1
- package/ios/AudioDeviceManager.swift +65 -65
- package/ios/AudioProcessor.swift +32 -32
- package/ios/AudioStreamManager.swift +323 -158
- package/ios/ExpoAudioStreamModule.swift +92 -75
- package/ios/ISSUE_IOS.md +26 -3
- package/ios/Logger.swift +27 -7
- package/package.json +1 -1
- package/src/ExpoAudioStream.types.ts +5 -1
- package/src/useAudioRecorder.tsx +1 -2
|
@@ -51,11 +51,14 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
51
51
|
var isPaused = false
|
|
52
52
|
var isPrepared = false // Add this new state flag
|
|
53
53
|
|
|
54
|
+
// Move static variables to class level
|
|
55
|
+
private var debugBufferCounter = 0
|
|
56
|
+
private var tapCallCount = 0
|
|
57
|
+
|
|
54
58
|
// Wake lock related properties
|
|
55
59
|
private var wasIdleTimerDisabled: Bool = false // Track previous idle timer state
|
|
56
60
|
private var isWakeLockEnabled: Bool = false // Track current wake lock state
|
|
57
61
|
|
|
58
|
-
|
|
59
62
|
// Data emission for onAudioStream
|
|
60
63
|
internal var lastEmissionTime: Date?
|
|
61
64
|
internal var lastEmittedSize: Int64 = 0
|
|
@@ -153,7 +156,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
153
156
|
|
|
154
157
|
switch type {
|
|
155
158
|
case .began:
|
|
156
|
-
Logger.debug("Audio session interruption began")
|
|
159
|
+
Logger.debug("AudioStreamManager", "Audio session interruption began")
|
|
157
160
|
// Store the pause start time if not already paused
|
|
158
161
|
if !wasSuspended {
|
|
159
162
|
currentPauseStart = Date()
|
|
@@ -170,17 +173,17 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
170
173
|
)
|
|
171
174
|
|
|
172
175
|
case .ended:
|
|
173
|
-
Logger.debug("Audio session interruption ended - autoResume: \(autoResumeAfterInterruption), wasSuspended: \(wasSuspended)")
|
|
176
|
+
Logger.debug("AudioStreamManager", "Audio session interruption ended - autoResume: \(autoResumeAfterInterruption), wasSuspended: \(wasSuspended)")
|
|
174
177
|
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
|
|
175
178
|
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
|
176
|
-
Logger.debug("Interruption options - shouldResume: \(options.contains(.shouldResume))")
|
|
179
|
+
Logger.debug("AudioStreamManager", "Interruption options - shouldResume: \(options.contains(.shouldResume))")
|
|
177
180
|
|
|
178
181
|
// Calculate pause duration if we have a pause start time
|
|
179
182
|
if let pauseStart = currentPauseStart {
|
|
180
183
|
let pauseDuration = Date().timeIntervalSince(pauseStart)
|
|
181
184
|
totalPausedDuration += pauseDuration
|
|
182
185
|
currentPauseStart = nil
|
|
183
|
-
Logger.debug("Added interruption pause duration: \(pauseDuration), total paused: \(totalPausedDuration)")
|
|
186
|
+
Logger.debug("AudioStreamManager", "Added interruption pause duration: \(pauseDuration), total paused: \(totalPausedDuration)")
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
// For phone calls, we should auto-resume if enabled, regardless of previous pause state
|
|
@@ -188,7 +191,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
188
191
|
// Add a longer delay for phone calls and ensure proper session setup
|
|
189
192
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
|
|
190
193
|
guard let self = self else { return }
|
|
191
|
-
Logger.debug("Attempting to auto-resume recording after phone call")
|
|
194
|
+
Logger.debug("AudioStreamManager", "Attempting to auto-resume recording after phone call")
|
|
192
195
|
|
|
193
196
|
// Configure audio session
|
|
194
197
|
do {
|
|
@@ -198,14 +201,14 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
198
201
|
|
|
199
202
|
// Resume if we're still recording and paused
|
|
200
203
|
if self.isRecording && self.isPaused {
|
|
201
|
-
Logger.debug("Resuming recording after phone call interruption")
|
|
204
|
+
Logger.debug("AudioStreamManager", "Resuming recording after phone call interruption")
|
|
202
205
|
self.audioEngine.prepare()
|
|
203
206
|
self.resumeRecording()
|
|
204
207
|
} else {
|
|
205
|
-
Logger.debug("Cannot resume - recording state invalid: isRecording=\(self.isRecording), isPaused=\(self.isPaused)")
|
|
208
|
+
Logger.debug("AudioStreamManager", "Cannot resume - recording state invalid: isRecording=\(self.isRecording), isPaused=\(self.isPaused)")
|
|
206
209
|
}
|
|
207
210
|
} catch {
|
|
208
|
-
Logger.debug("Failed to reactivate audio session: \(error)")
|
|
211
|
+
Logger.debug("AudioStreamManager", "Failed to reactivate audio session: \(error)")
|
|
209
212
|
self.delegate?.audioStreamManager(self, didFailWithError: "Failed to auto-resume: \(error.localizedDescription)")
|
|
210
213
|
}
|
|
211
214
|
}
|
|
@@ -233,7 +236,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
233
236
|
try audioSession?.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .mixWithOthers])
|
|
234
237
|
try audioSession?.setActive(true)
|
|
235
238
|
} catch {
|
|
236
|
-
Logger.debug("Failed to configure audio session: \(error)")
|
|
239
|
+
Logger.debug("AudioStreamManager", "Failed to configure audio session: \(error)")
|
|
237
240
|
}
|
|
238
241
|
|
|
239
242
|
// Setup Now Playing info
|
|
@@ -378,7 +381,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
378
381
|
let pauseDuration = Date().timeIntervalSince(pauseStart)
|
|
379
382
|
totalPausedDuration += pauseDuration
|
|
380
383
|
currentPauseStart = nil
|
|
381
|
-
Logger.debug("Added background pause duration: \(pauseDuration), total paused: \(totalPausedDuration)")
|
|
384
|
+
Logger.debug("AudioStreamManager", "Added background pause duration: \(pauseDuration), total paused: \(totalPausedDuration)")
|
|
382
385
|
}
|
|
383
386
|
|
|
384
387
|
notificationManager?.stopUpdates()
|
|
@@ -433,7 +436,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
433
436
|
self.wasIdleTimerDisabled = UIApplication.shared.isIdleTimerDisabled
|
|
434
437
|
UIApplication.shared.isIdleTimerDisabled = true
|
|
435
438
|
self.isWakeLockEnabled = true
|
|
436
|
-
Logger.debug("Wake lock enabled")
|
|
439
|
+
Logger.debug("AudioStreamManager", "Wake lock enabled")
|
|
437
440
|
}
|
|
438
441
|
}
|
|
439
442
|
|
|
@@ -447,7 +450,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
447
450
|
DispatchQueue.main.async {
|
|
448
451
|
UIApplication.shared.isIdleTimerDisabled = self.wasIdleTimerDisabled
|
|
449
452
|
self.isWakeLockEnabled = false
|
|
450
|
-
Logger.debug("Wake lock disabled")
|
|
453
|
+
Logger.debug("AudioStreamManager", "Wake lock disabled")
|
|
451
454
|
}
|
|
452
455
|
}
|
|
453
456
|
|
|
@@ -455,17 +458,17 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
455
458
|
/// - Returns: The URL of the newly created recording file, or nil if creation failed.
|
|
456
459
|
private func createRecordingFile(isCompressed: Bool = false) -> URL? {
|
|
457
460
|
// Add debug logging
|
|
458
|
-
Logger.debug("Creating recording file - settings filename: \(recordingSettings?.filename ?? "nil")")
|
|
461
|
+
Logger.debug("AudioStreamManager", "Creating recording file - settings filename: \(recordingSettings?.filename ?? "nil")")
|
|
459
462
|
|
|
460
463
|
// Get base directory - use default if no custom directory provided
|
|
461
464
|
let baseDirectory: URL
|
|
462
465
|
if let customDir = recordingSettings?.outputDirectory {
|
|
463
466
|
baseDirectory = URL(fileURLWithPath: customDir)
|
|
464
|
-
Logger.debug("Using custom directory: \(customDir)")
|
|
467
|
+
Logger.debug("AudioStreamManager", "Using custom directory: \(customDir)")
|
|
465
468
|
} else {
|
|
466
469
|
// Use existing default behavior
|
|
467
470
|
baseDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
468
|
-
Logger.debug("Using default directory: \(baseDirectory.path)")
|
|
471
|
+
Logger.debug("AudioStreamManager", "Using default directory: \(baseDirectory.path)")
|
|
469
472
|
}
|
|
470
473
|
|
|
471
474
|
// Generate or reuse UUID for filename
|
|
@@ -478,7 +481,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
478
481
|
recordingUUID = newUUID
|
|
479
482
|
baseFilename = newUUID.uuidString
|
|
480
483
|
}
|
|
481
|
-
Logger.debug("Using base filename: \(baseFilename)")
|
|
484
|
+
Logger.debug("AudioStreamManager", "Using base filename: \(baseFilename)")
|
|
482
485
|
|
|
483
486
|
// Remove any existing extension from the filename
|
|
484
487
|
let filenameWithoutExtension = baseFilename.replacingOccurrences(
|
|
@@ -496,19 +499,19 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
496
499
|
}
|
|
497
500
|
|
|
498
501
|
let fullFilename = "\(filenameWithoutExtension).\(fileExtension)"
|
|
499
|
-
Logger.debug("Full filename: \(fullFilename)")
|
|
502
|
+
Logger.debug("AudioStreamManager", "Full filename: \(fullFilename)")
|
|
500
503
|
|
|
501
504
|
let fileURL = baseDirectory.appendingPathComponent(fullFilename)
|
|
502
|
-
Logger.debug("Final file URL: \(fileURL.path)")
|
|
505
|
+
Logger.debug("AudioStreamManager", "Final file URL: \(fileURL.path)")
|
|
503
506
|
|
|
504
507
|
// Check if file already exists
|
|
505
508
|
if fileManager.fileExists(atPath: fileURL.path) {
|
|
506
|
-
Logger.debug("File already exists at: \(fileURL.path)")
|
|
509
|
+
Logger.debug("AudioStreamManager", "File already exists at: \(fileURL.path)")
|
|
507
510
|
return nil
|
|
508
511
|
}
|
|
509
512
|
|
|
510
513
|
if !fileManager.createFile(atPath: fileURL.path, contents: nil, attributes: nil) {
|
|
511
|
-
Logger.debug("Failed to create file at: \(fileURL.path)")
|
|
514
|
+
Logger.debug("AudioStreamManager", "Failed to create file at: \(fileURL.path)")
|
|
512
515
|
return nil
|
|
513
516
|
}
|
|
514
517
|
return fileURL
|
|
@@ -588,7 +591,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
588
591
|
do {
|
|
589
592
|
let compressedAttributes = try FileManager.default.attributesOfItem(atPath: compressedURL.path)
|
|
590
593
|
if let compressedSize = compressedAttributes[.size] as? Int64 {
|
|
591
|
-
Logger.debug("Compressed file status - Size: \(compressedSize)")
|
|
594
|
+
Logger.debug("AudioStreamManager", "Compressed file status - Size: \(compressedSize)")
|
|
592
595
|
let compressionBundle: [String: Any] = [
|
|
593
596
|
"fileUri": compressedURL.absoluteString,
|
|
594
597
|
"mimeType": compressedFormat == "aac" ? "audio/aac" : "audio/opus",
|
|
@@ -599,7 +602,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
599
602
|
status["compression"] = compressionBundle
|
|
600
603
|
}
|
|
601
604
|
} catch {
|
|
602
|
-
Logger.debug("Error getting compressed file attributes: \(error)")
|
|
605
|
+
Logger.debug("AudioStreamManager", "Error getting compressed file attributes: \(error)")
|
|
603
606
|
}
|
|
604
607
|
}
|
|
605
608
|
|
|
@@ -616,6 +619,52 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
616
619
|
audioSession.currentRoute.outputs.contains { $0.portType == .builtInReceiver }
|
|
617
620
|
}
|
|
618
621
|
|
|
622
|
+
/// Installs the audio tap with the hardware-compatible format
|
|
623
|
+
/// - Parameters:
|
|
624
|
+
/// - customTapBlock: Optional custom tap block for specialized processing (like in fallback)
|
|
625
|
+
/// - prepareEngine: Whether to call prepare() on the engine after installing the tap (default: true)
|
|
626
|
+
/// - Returns: The hardware input format that was used for the tap
|
|
627
|
+
private func installTapWithHardwareFormat(
|
|
628
|
+
customTapBlock: ((AVAudioPCMBuffer, AVAudioTime) -> Void)? = nil,
|
|
629
|
+
prepareEngine: Bool = true
|
|
630
|
+
) -> AVAudioFormat {
|
|
631
|
+
// Get the hardware input format
|
|
632
|
+
let inputNode = audioEngine.inputNode
|
|
633
|
+
let inputHardwareFormat = inputNode.inputFormat(forBus: 0)
|
|
634
|
+
let nodeOutputFormat = inputNode.outputFormat(forBus: 0)
|
|
635
|
+
|
|
636
|
+
// Log format information for diagnostic purposes
|
|
637
|
+
Logger.debug("AudioStreamManager", "Installing tap - Hardware input format: \(describeAudioFormat(inputHardwareFormat))")
|
|
638
|
+
Logger.debug("AudioStreamManager", "Node output format: \(describeAudioFormat(nodeOutputFormat))")
|
|
639
|
+
|
|
640
|
+
// Remove any existing tap
|
|
641
|
+
inputNode.removeTap(onBus: 0)
|
|
642
|
+
|
|
643
|
+
// Create the default tap block if none provided
|
|
644
|
+
let tapBlock = customTapBlock ?? { [weak self] (buffer, time) in
|
|
645
|
+
guard let self = self,
|
|
646
|
+
let fileURL = self.recordingFileURL,
|
|
647
|
+
self.isRecording else {
|
|
648
|
+
return
|
|
649
|
+
}
|
|
650
|
+
// processAudioBuffer will handle resampling if needed
|
|
651
|
+
self.processAudioBuffer(buffer, fileURL: fileURL)
|
|
652
|
+
self.lastBufferTime = time
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Install the tap with hardware format
|
|
656
|
+
inputNode.installTap(onBus: 0, bufferSize: 1024, format: inputHardwareFormat, block: tapBlock)
|
|
657
|
+
Logger.debug("AudioStreamManager", "Tap installed with hardware-compatible format")
|
|
658
|
+
|
|
659
|
+
// Prepare the engine if requested
|
|
660
|
+
if prepareEngine {
|
|
661
|
+
audioEngine.prepare()
|
|
662
|
+
Logger.debug("AudioStreamManager", "Engine prepared after tap installation")
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return inputHardwareFormat
|
|
666
|
+
}
|
|
667
|
+
|
|
619
668
|
/// Prepares the audio recording with the specified settings without starting it.
|
|
620
669
|
/// This reduces latency when startRecording is called later.
|
|
621
670
|
/// - Parameters:
|
|
@@ -627,13 +676,13 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
627
676
|
|
|
628
677
|
// Skip if already prepared or recording
|
|
629
678
|
guard !isPrepared && !isRecording else {
|
|
630
|
-
Logger.debug("Already prepared or recording in progress.")
|
|
679
|
+
Logger.debug("AudioStreamManager", "Already prepared or recording in progress.")
|
|
631
680
|
return isPrepared
|
|
632
681
|
}
|
|
633
682
|
|
|
634
683
|
// Check for active call using the new method
|
|
635
684
|
if isPhoneCallActive() {
|
|
636
|
-
Logger.debug("Cannot prepare recording during an active phone call")
|
|
685
|
+
Logger.debug("AudioStreamManager", "Cannot prepare recording during an active phone call")
|
|
637
686
|
delegate?.audioStreamManager(self, didFailWithError: "Cannot prepare recording during an active phone call")
|
|
638
687
|
return false
|
|
639
688
|
}
|
|
@@ -645,7 +694,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
645
694
|
Thread.sleep(forTimeInterval: 0.1) // Brief pause to ensure clean state
|
|
646
695
|
try session.setActive(true, options: .notifyOthersOnDeactivation)
|
|
647
696
|
} catch {
|
|
648
|
-
Logger.debug("Failed to reset audio session: \(error)")
|
|
697
|
+
Logger.debug("AudioStreamManager", "Failed to reset audio session: \(error)")
|
|
649
698
|
delegate?.audioStreamManager(self, didFailWithError: "Failed to reset audio session: \(error.localizedDescription)")
|
|
650
699
|
return false
|
|
651
700
|
}
|
|
@@ -682,14 +731,14 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
682
731
|
let header = createWavHeader(dataSize: 0)
|
|
683
732
|
self.fileHandle?.write(header)
|
|
684
733
|
self.totalDataSize = Int64(WAV_HEADER_SIZE) // Initialize size with header size
|
|
685
|
-
Logger.debug("File handle opened and initial header written for \(url.path). Initial size: \(self.totalDataSize)")
|
|
734
|
+
Logger.debug("AudioStreamManager", "File handle opened and initial header written for \(url.path). Initial size: \(self.totalDataSize)")
|
|
686
735
|
} catch {
|
|
687
|
-
Logger.debug("Error creating/opening file handle: \(error.localizedDescription)")
|
|
736
|
+
Logger.debug("AudioStreamManager", "Error creating/opening file handle: \(error.localizedDescription)")
|
|
688
737
|
// No need to call cleanupPreparation here, return false will handle it
|
|
689
738
|
return false
|
|
690
739
|
}
|
|
691
740
|
} else {
|
|
692
|
-
Logger.debug("Error: Failed to create recording file URL.")
|
|
741
|
+
Logger.debug("AudioStreamManager", "Error: Failed to create recording file URL.")
|
|
693
742
|
return false
|
|
694
743
|
}
|
|
695
744
|
|
|
@@ -697,11 +746,11 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
697
746
|
|
|
698
747
|
// Then set up audio session and tap
|
|
699
748
|
do {
|
|
700
|
-
Logger.debug("Configuring audio session with sample rate: \(settings.sampleRate) Hz")
|
|
749
|
+
Logger.debug("AudioStreamManager", "Configuring audio session with sample rate: \(settings.sampleRate) Hz")
|
|
701
750
|
|
|
702
751
|
let session = AVAudioSession.sharedInstance()
|
|
703
752
|
if let currentRoute = session.currentRoute.outputs.first {
|
|
704
|
-
Logger.debug("Current audio output: \(currentRoute.portType)")
|
|
753
|
+
Logger.debug("AudioStreamManager", "Current audio output: \(currentRoute.portType)")
|
|
705
754
|
newSettings.sampleRate = settings.sampleRate // Keep original sample rate
|
|
706
755
|
}
|
|
707
756
|
|
|
@@ -718,12 +767,12 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
718
767
|
|
|
719
768
|
// Append necessary options for background recording if keepAwake is enabled
|
|
720
769
|
if settings.keepAwake {
|
|
721
|
-
Logger.debug("keepAwake enabled - configuring for background recording")
|
|
770
|
+
Logger.debug("AudioStreamManager", "keepAwake enabled - configuring for background recording")
|
|
722
771
|
// Add background audio option
|
|
723
772
|
options.insert(.mixWithOthers)
|
|
724
773
|
try session.setActive(true, options: .notifyOthersOnDeactivation)
|
|
725
774
|
} else {
|
|
726
|
-
Logger.debug("keepAwake disabled - using standard session configuration")
|
|
775
|
+
Logger.debug("AudioStreamManager", "keepAwake disabled - using standard session configuration")
|
|
727
776
|
// If keepAwake is false, don't add background audio options
|
|
728
777
|
try session.setActive(true)
|
|
729
778
|
}
|
|
@@ -739,61 +788,30 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
739
788
|
try session.setActive(true, options: .notifyOthersOnDeactivation)
|
|
740
789
|
|
|
741
790
|
// Log session config details as single lines for clarity
|
|
742
|
-
Logger.debug("Audio session configured:")
|
|
743
|
-
Logger.debug(" - category: \(category)")
|
|
744
|
-
Logger.debug(" - mode: \(mode)")
|
|
745
|
-
Logger.debug(" - options: \(options)")
|
|
746
|
-
Logger.debug(" - keepAwake: \(settings.keepAwake)")
|
|
747
|
-
Logger.debug(" - emission interval: \(emissionInterval * 1000)ms")
|
|
748
|
-
Logger.debug(" - analysis interval: \(emissionIntervalAnalysis * 1000)ms")
|
|
749
|
-
Logger.debug(" - requested sample rate: \(settings.sampleRate)Hz")
|
|
750
|
-
Logger.debug(" - actual session sample rate: \(session.sampleRate)Hz") // Log actual rate
|
|
751
|
-
Logger.debug(" - channels: \(settings.numberOfChannels)")
|
|
752
|
-
Logger.debug(" - bit depth: \(settings.bitDepth)-bit")
|
|
753
|
-
Logger.debug(" - compression enabled: \(settings.enableCompressedOutput)")
|
|
754
|
-
|
|
755
|
-
//
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
Logger.debug("
|
|
761
|
-
Logger.debug("
|
|
762
|
-
|
|
763
|
-
// Create the tap format using the ACTUAL session sample rate, but node's channel count/type.
|
|
764
|
-
// This aims to match the hardware stream (like 16kHz HFP) more reliably.
|
|
765
|
-
guard let tapFormat = AVAudioFormat(
|
|
766
|
-
commonFormat: nodeFormat.commonFormat, // Keep node's format (e.g., Float32)
|
|
767
|
-
sampleRate: actualSessionRate, // Use ACTUAL session rate
|
|
768
|
-
channels: nodeFormat.channelCount, // Use node's channel count
|
|
769
|
-
interleaved: nodeFormat.isInterleaved // Use node's interleaving
|
|
770
|
-
) else {
|
|
771
|
-
Logger.debug("Failed to create tap format with session rate \(actualSessionRate) and node details.")
|
|
772
|
-
// Throw an error to prevent proceeding with invalid setup
|
|
773
|
-
throw NSError(domain: "AudioStreamManager", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to create tap format for installation."])
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// Log tap config details as single lines
|
|
777
|
-
Logger.debug("Final Tap Configuration (Using Session Rate):")
|
|
778
|
-
Logger.debug(" - Tap Format: \(describeAudioFormat(tapFormat))")
|
|
779
|
-
Logger.debug(" - Node Format Was: \(describeAudioFormat(nodeFormat))")
|
|
780
|
-
Logger.debug(" - Requested Output Format: \(settings.bitDepth)-bit at \(settings.sampleRate)Hz")
|
|
791
|
+
Logger.debug("AudioStreamManager", "Audio session configured:")
|
|
792
|
+
Logger.debug("AudioStreamManager", " - category: \(category)")
|
|
793
|
+
Logger.debug("AudioStreamManager", " - mode: \(mode)")
|
|
794
|
+
Logger.debug("AudioStreamManager", " - options: \(options)")
|
|
795
|
+
Logger.debug("AudioStreamManager", " - keepAwake: \(settings.keepAwake)")
|
|
796
|
+
Logger.debug("AudioStreamManager", " - emission interval: \(emissionInterval * 1000)ms")
|
|
797
|
+
Logger.debug("AudioStreamManager", " - analysis interval: \(emissionIntervalAnalysis * 1000)ms")
|
|
798
|
+
Logger.debug("AudioStreamManager", " - requested sample rate: \(settings.sampleRate)Hz")
|
|
799
|
+
Logger.debug("AudioStreamManager", " - actual session sample rate: \(session.sampleRate)Hz") // Log actual rate
|
|
800
|
+
Logger.debug("AudioStreamManager", " - channels: \(settings.numberOfChannels)")
|
|
801
|
+
Logger.debug("AudioStreamManager", " - bit depth: \(settings.bitDepth)-bit")
|
|
802
|
+
Logger.debug("AudioStreamManager", " - compression enabled: \(settings.enableCompressedOutput)")
|
|
803
|
+
|
|
804
|
+
// Use our shared tap installation method
|
|
805
|
+
let tapFormat = installTapWithHardwareFormat()
|
|
806
|
+
|
|
807
|
+
// Log tap configuration
|
|
808
|
+
Logger.debug("AudioStreamManager", "Final Tap Configuration (Using Hardware Format):")
|
|
809
|
+
Logger.debug("AudioStreamManager", " - Tap Format: \(describeAudioFormat(tapFormat))")
|
|
810
|
+
Logger.debug("AudioStreamManager", " - Session Rate: \(session.sampleRate) Hz")
|
|
811
|
+
Logger.debug("AudioStreamManager", " - Requested Output Format: \(settings.bitDepth)-bit at \(settings.sampleRate)Hz")
|
|
781
812
|
|
|
782
813
|
recordingSettings = newSettings // Keep original settings with desired sample rate
|
|
783
814
|
|
|
784
|
-
// Install tap with the format derived from session sample rate
|
|
785
|
-
audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: tapFormat) { [weak self] (buffer, time) in // Use newly constructed tapFormat
|
|
786
|
-
guard let self = self,
|
|
787
|
-
let fileURL = self.recordingFileURL,
|
|
788
|
-
self.isRecording else { // Only process buffer if actually recording
|
|
789
|
-
// Logger.debug("Tap received buffer but self, fileURL, or isRecording is invalid. Ignoring.")
|
|
790
|
-
return
|
|
791
|
-
}
|
|
792
|
-
// processAudioBuffer will handle resampling if tapFormat.sampleRate != settings.sampleRate
|
|
793
|
-
self.processAudioBuffer(buffer, fileURL: fileURL)
|
|
794
|
-
self.lastBufferTime = time
|
|
795
|
-
}
|
|
796
|
-
|
|
797
815
|
audioEngine.prepare() // Prepare the engine without starting it
|
|
798
816
|
|
|
799
817
|
// Setup compressed recording if enabled
|
|
@@ -808,13 +826,14 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
808
826
|
AVEncoderBitDepthHintKey: settings.bitDepth
|
|
809
827
|
]
|
|
810
828
|
|
|
811
|
-
|
|
829
|
+
|
|
830
|
+
Logger.debug("AudioStreamManager", "Initializing compressed recording with settings: \(compressedSettings)")
|
|
812
831
|
|
|
813
832
|
// Create file for compressed recording
|
|
814
833
|
compressedFileURL = createRecordingFile(isCompressed: true)
|
|
815
834
|
|
|
816
835
|
if let url = compressedFileURL {
|
|
817
|
-
Logger.debug("Using compressed file URL: \(url.path)")
|
|
836
|
+
Logger.debug("AudioStreamManager", "Using compressed file URL: \(url.path)")
|
|
818
837
|
|
|
819
838
|
// Initialize recorder with proper error handling
|
|
820
839
|
do {
|
|
@@ -823,28 +842,28 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
823
842
|
recorder.delegate = self
|
|
824
843
|
|
|
825
844
|
if !recorder.prepareToRecord() {
|
|
826
|
-
Logger.debug("Failed to prepare recorder")
|
|
845
|
+
Logger.debug("AudioStreamManager", "Failed to prepare recorder")
|
|
827
846
|
compressedFileURL = nil
|
|
828
847
|
compressedRecorder = nil
|
|
829
848
|
} else {
|
|
830
849
|
// Note: We don't start the recorder yet, just prepare it
|
|
831
|
-
Logger.debug("Compressed recording prepared successfully")
|
|
850
|
+
Logger.debug("AudioStreamManager", "Compressed recording prepared successfully")
|
|
832
851
|
compressedFormat = settings.compressedFormat
|
|
833
852
|
compressedBitRate = settings.compressedBitRate
|
|
834
853
|
}
|
|
835
854
|
}
|
|
836
855
|
} catch {
|
|
837
|
-
Logger.debug("Failed to initialize compressed recorder: \(error)")
|
|
856
|
+
Logger.debug("AudioStreamManager", "Failed to initialize compressed recorder: \(error)")
|
|
838
857
|
compressedFileURL = nil
|
|
839
858
|
compressedRecorder = nil
|
|
840
859
|
}
|
|
841
860
|
} else {
|
|
842
|
-
Logger.debug("Failed to create compressed recording file")
|
|
861
|
+
Logger.debug("AudioStreamManager", "Failed to create compressed recording file")
|
|
843
862
|
}
|
|
844
863
|
}
|
|
845
864
|
|
|
846
865
|
} catch {
|
|
847
|
-
Logger.debug("Error: Failed to set up audio session with preferred settings: \(error.localizedDescription)")
|
|
866
|
+
Logger.debug("AudioStreamManager", "Error: Failed to set up audio session with preferred settings: \(error.localizedDescription)")
|
|
848
867
|
return false
|
|
849
868
|
}
|
|
850
869
|
|
|
@@ -857,7 +876,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
857
876
|
}, reject: { code, message in
|
|
858
877
|
// Handle the rejection here if needed
|
|
859
878
|
})
|
|
860
|
-
Logger.debug("AudioProcessor activated successfully.")
|
|
879
|
+
Logger.debug("AudioStreamManager", "AudioProcessor activated successfully.")
|
|
861
880
|
}
|
|
862
881
|
|
|
863
882
|
// Prepare notifications if enabled but don't show yet
|
|
@@ -880,6 +899,10 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
880
899
|
// If already prepared, use the prepared state
|
|
881
900
|
if isPrepared {
|
|
882
901
|
Logger.debug("Using prepared recording state")
|
|
902
|
+
|
|
903
|
+
// Install tap with hardware format
|
|
904
|
+
_ = installTapWithHardwareFormat()
|
|
905
|
+
Logger.debug("Tap was reinstalled during recording start")
|
|
883
906
|
} else {
|
|
884
907
|
// If not prepared, prepare now
|
|
885
908
|
Logger.debug("Not prepared, preparing recording first")
|
|
@@ -889,6 +912,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
889
912
|
}
|
|
890
913
|
}
|
|
891
914
|
|
|
915
|
+
// Rest of the method remains unchanged
|
|
892
916
|
// Check for active phone call again, in case one started after preparation
|
|
893
917
|
if isPhoneCallActive() {
|
|
894
918
|
Logger.debug("Cannot start recording during an active phone call")
|
|
@@ -1078,6 +1102,10 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1078
1102
|
}
|
|
1079
1103
|
|
|
1080
1104
|
do {
|
|
1105
|
+
// Check and reinstall tap with hardware format
|
|
1106
|
+
_ = installTapWithHardwareFormat()
|
|
1107
|
+
Logger.debug("Tap reinstalled for resume")
|
|
1108
|
+
|
|
1081
1109
|
// Try to restart the engine
|
|
1082
1110
|
try audioEngine.start()
|
|
1083
1111
|
|
|
@@ -1095,10 +1123,14 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1095
1123
|
// Clear the stored valid duration
|
|
1096
1124
|
lastValidDuration = nil
|
|
1097
1125
|
|
|
1126
|
+
// Reset emission timers to ensure emission starts immediately after resume
|
|
1127
|
+
lastEmissionTime = Date()
|
|
1128
|
+
lastEmissionTimeAnalysis = Date()
|
|
1129
|
+
|
|
1098
1130
|
// Notify delegate
|
|
1099
1131
|
delegate?.audioStreamManager(self, didResumeRecording: Date())
|
|
1100
1132
|
|
|
1101
|
-
Logger.debug("Recording resumed")
|
|
1133
|
+
Logger.debug("Recording resumed successfully")
|
|
1102
1134
|
|
|
1103
1135
|
} catch {
|
|
1104
1136
|
Logger.debug("Failed to resume recording: \(error.localizedDescription)")
|
|
@@ -1285,6 +1317,14 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1285
1317
|
return
|
|
1286
1318
|
}
|
|
1287
1319
|
|
|
1320
|
+
// DEBUG: Add tap and buffer info
|
|
1321
|
+
debugBufferCounter += 1
|
|
1322
|
+
|
|
1323
|
+
// Log every 10th buffer to avoid excessive logs
|
|
1324
|
+
if debugBufferCounter % 10 == 0 {
|
|
1325
|
+
Logger.debug("BUFFER DEBUG: Processing buffer #\(debugBufferCounter), channelCount: \(buffer.format.channelCount), frameLength: \(buffer.frameLength)")
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1288
1328
|
// targetSampleRate and targetFormat remain the user's requested final format
|
|
1289
1329
|
let targetSampleRate = Double(settings.sampleRate)
|
|
1290
1330
|
let targetFormat: AVAudioCommonFormat = settings.bitDepth == 32 ? .pcmFormatFloat32 : .pcmFormatInt16
|
|
@@ -1360,24 +1400,37 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1360
1400
|
let currentTotalSize = self.totalDataSize // Use the most up-to-date size for events
|
|
1361
1401
|
|
|
1362
1402
|
// Emit AudioData event
|
|
1363
|
-
if let lastEmission = self.lastEmissionTime
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
compressionInfo:
|
|
1379
|
-
|
|
1380
|
-
|
|
1403
|
+
if let lastEmission = self.lastEmissionTime {
|
|
1404
|
+
// Log emission evaluation every 10th buffer
|
|
1405
|
+
if debugBufferCounter % 10 == 0 {
|
|
1406
|
+
let timeGap = currentTime.timeIntervalSince(lastEmission)
|
|
1407
|
+
let isTimeReady = timeGap >= emissionInterval
|
|
1408
|
+
Logger.debug("EMISSION DEBUG: Time since last: \(timeGap)s, Threshold: \(emissionInterval)s, Ready: \(isTimeReady), DataSize: \(accumulatedData.count) bytes")
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
if currentTime.timeIntervalSince(lastEmission) >= emissionInterval,
|
|
1412
|
+
!accumulatedData.isEmpty {
|
|
1413
|
+
let dataToEmit = accumulatedData
|
|
1414
|
+
let recordingTime = currentRecordingDuration()
|
|
1415
|
+
self.lastEmissionTime = currentTime
|
|
1416
|
+
self.lastEmittedSize = currentTotalSize
|
|
1417
|
+
accumulatedData.removeAll()
|
|
1418
|
+
var compressionInfo: [String: Any]? = nil
|
|
1419
|
+
|
|
1420
|
+
Logger.debug("EMISSION SUCCESS: Emitting \(dataToEmit.count) bytes at recording time \(recordingTime)s")
|
|
1421
|
+
|
|
1422
|
+
delegate?.audioStreamManager(
|
|
1423
|
+
self,
|
|
1424
|
+
didReceiveAudioData: dataToEmit,
|
|
1425
|
+
recordingTime: recordingTime,
|
|
1426
|
+
totalDataSize: currentTotalSize,
|
|
1427
|
+
compressionInfo: compressionInfo
|
|
1428
|
+
)
|
|
1429
|
+
}
|
|
1430
|
+
} else {
|
|
1431
|
+
// This case occurs when lastEmissionTime is nil (either first run or after reset)
|
|
1432
|
+
Logger.debug("EMISSION DEBUG: lastEmissionTime is nil, setting to current time")
|
|
1433
|
+
lastEmissionTime = currentTime
|
|
1381
1434
|
}
|
|
1382
1435
|
|
|
1383
1436
|
// Dispatch analysis task
|
|
@@ -1768,12 +1821,14 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1768
1821
|
// Switch on the *enum* value
|
|
1769
1822
|
switch behavior {
|
|
1770
1823
|
case .PAUSE:
|
|
1824
|
+
Logger.debug("Device disconnect behavior set to PAUSE. Pausing recording.")
|
|
1771
1825
|
performPauseAction(reason: .deviceDisconnected)
|
|
1772
1826
|
|
|
1773
1827
|
case .FALLBACK:
|
|
1774
|
-
|
|
1828
|
+
Logger.debug("Device disconnect behavior set to FALLBACK. Attempting to switch to default device.")
|
|
1829
|
+
Task {
|
|
1775
1830
|
await performFallbackAction()
|
|
1776
|
-
|
|
1831
|
+
}
|
|
1777
1832
|
}
|
|
1778
1833
|
} else {
|
|
1779
1834
|
Logger.debug("A different device disconnected (\(normalizedDisconnectedId)). Current recording device (\(normalizedCurrentId)) is still active. Ignoring.")
|
|
@@ -1807,13 +1862,26 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1807
1862
|
}
|
|
1808
1863
|
Logger.debug("Found default device for fallback: \(defaultDevice.name) (ID: \(defaultDevice.id))")
|
|
1809
1864
|
|
|
1810
|
-
//
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
audioEngine.pause()
|
|
1814
|
-
}
|
|
1865
|
+
// CRITICAL: Complete engine reset - stronger than just pausing
|
|
1866
|
+
audioEngine.stop()
|
|
1867
|
+
audioEngine.reset() // Reset the entire engine
|
|
1815
1868
|
audioEngine.inputNode.removeTap(onBus: 0)
|
|
1816
|
-
|
|
1869
|
+
|
|
1870
|
+
// More aggressive session reset
|
|
1871
|
+
do {
|
|
1872
|
+
let session = AVAudioSession.sharedInstance()
|
|
1873
|
+
try session.setActive(false, options: .notifyOthersOnDeactivation)
|
|
1874
|
+
Thread.sleep(forTimeInterval: 0.2) // Give system time to release resources
|
|
1875
|
+
|
|
1876
|
+
// Reconfigure the session completely
|
|
1877
|
+
try session.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .mixWithOthers])
|
|
1878
|
+
try session.setActive(true, options: .notifyOthersOnDeactivation)
|
|
1879
|
+
Thread.sleep(forTimeInterval: 0.1) // Allow the session to activate fully
|
|
1880
|
+
} catch {
|
|
1881
|
+
Logger.debug("Session reset error: \(error.localizedDescription)")
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
let wasManuallyPaused = isPaused
|
|
1817
1885
|
|
|
1818
1886
|
// 3. Update settings and select the new device in the session
|
|
1819
1887
|
recordingSettings?.deviceId = defaultDevice.id // Update setting
|
|
@@ -1824,56 +1892,153 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1824
1892
|
return
|
|
1825
1893
|
}
|
|
1826
1894
|
Logger.debug("Successfully selected default device \(defaultDevice.id) in session.")
|
|
1827
|
-
|
|
1828
|
-
// --- Reinstall Tap ---
|
|
1829
|
-
// 4. Get the tap format for the *new* device
|
|
1830
|
-
let session = AVAudioSession.sharedInstance()
|
|
1831
|
-
let nodeFormat = audioEngine.inputNode.outputFormat(forBus: 0)
|
|
1832
|
-
let actualSessionRate = session.sampleRate
|
|
1833
|
-
Logger.debug("Fallback: New device node format: \(describeAudioFormat(nodeFormat))")
|
|
1834
|
-
Logger.debug("Fallback: New device session rate: \(actualSessionRate) Hz")
|
|
1835
1895
|
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
channels: nodeFormat.channelCount,
|
|
1840
|
-
interleaved: nodeFormat.isInterleaved
|
|
1841
|
-
) else {
|
|
1842
|
-
Logger.debug("Fallback failed: Could not create tap format for new device.")
|
|
1843
|
-
performPauseAction(reason: .deviceSwitchFailed)
|
|
1844
|
-
return
|
|
1845
|
-
}
|
|
1846
|
-
Logger.debug("Fallback: Determined new tap format: \(describeAudioFormat(newTapFormat))")
|
|
1896
|
+
// Additional forced reset of engine to ensure clean state
|
|
1897
|
+
audioEngine.reset()
|
|
1898
|
+
audioEngine.prepare()
|
|
1847
1899
|
|
|
1848
|
-
//
|
|
1849
|
-
|
|
1900
|
+
// Create a counter for tracking buffers since fallback
|
|
1901
|
+
var buffersSinceFallback = 0
|
|
1902
|
+
|
|
1903
|
+
// Create a specialized tap block for fallback with aggressive emission
|
|
1904
|
+
let fallbackTapBlock = { [weak self] (buffer: AVAudioPCMBuffer, time: AVAudioTime) -> Void in
|
|
1850
1905
|
guard let self = self, self.isRecording else { return }
|
|
1906
|
+
|
|
1907
|
+
// Process the buffer and ensure it's written to file
|
|
1851
1908
|
self.processAudioBuffer(buffer, fileURL: self.recordingFileURL!)
|
|
1852
1909
|
self.lastBufferTime = time
|
|
1910
|
+
|
|
1911
|
+
// Special handling for fallback: force emission regularly to restart flow
|
|
1912
|
+
let audioData = buffer.audioBufferList.pointee.mBuffers
|
|
1913
|
+
guard let bufferData = audioData.mData else { return }
|
|
1914
|
+
|
|
1915
|
+
let dataToAdd = Data(bytes: bufferData, count: Int(audioData.mDataByteSize))
|
|
1916
|
+
if !dataToAdd.isEmpty {
|
|
1917
|
+
// Force emission every few buffers regardless of timing during recovery period
|
|
1918
|
+
buffersSinceFallback += 1
|
|
1919
|
+
|
|
1920
|
+
// MORE AGGRESSIVE: Force emission every 2 buffers for the first 30 buffers
|
|
1921
|
+
if buffersSinceFallback <= 30 && buffersSinceFallback % 2 == 0 {
|
|
1922
|
+
DispatchQueue.main.async {
|
|
1923
|
+
// Bypass normal timing checks to ensure data flows
|
|
1924
|
+
let recordingTime = self.currentRecordingDuration()
|
|
1925
|
+
let totalSize = self.totalDataSize // Make sure we use the current value
|
|
1926
|
+
Logger.debug("FALLBACK FORCE EMIT: Forcing emission after fallback (buffer #\(buffersSinceFallback), size: \(dataToAdd.count) bytes, totalSize: \(totalSize))")
|
|
1927
|
+
|
|
1928
|
+
self.delegate?.audioStreamManager(
|
|
1929
|
+
self,
|
|
1930
|
+
didReceiveAudioData: dataToAdd,
|
|
1931
|
+
recordingTime: recordingTime,
|
|
1932
|
+
totalDataSize: totalSize,
|
|
1933
|
+
compressionInfo: nil
|
|
1934
|
+
)
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1853
1938
|
}
|
|
1854
|
-
Logger.debug("Fallback: Re-installed tap with new format.")
|
|
1855
1939
|
|
|
1856
|
-
//
|
|
1940
|
+
// Use our shared tap installation method with the custom block
|
|
1941
|
+
installTapWithHardwareFormat(customTapBlock: fallbackTapBlock)
|
|
1942
|
+
Logger.debug("Fallback: Re-installed tap with enhanced emission handling")
|
|
1943
|
+
|
|
1944
|
+
// Force prepare engine again to ensure it's ready
|
|
1857
1945
|
audioEngine.prepare()
|
|
1858
1946
|
Logger.debug("Fallback: Prepared audio engine.")
|
|
1859
1947
|
|
|
1860
1948
|
if !wasManuallyPaused {
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1949
|
+
// Only start if it's not running (it should have been paused earlier)
|
|
1950
|
+
if !audioEngine.isRunning {
|
|
1951
|
+
do {
|
|
1952
|
+
try audioEngine.start()
|
|
1953
|
+
Logger.debug("Audio engine restarted for fallback.")
|
|
1954
|
+
} catch {
|
|
1955
|
+
// Try ONE more time with delay
|
|
1956
|
+
Thread.sleep(forTimeInterval: 0.2)
|
|
1957
|
+
do {
|
|
1958
|
+
try audioEngine.start()
|
|
1959
|
+
Logger.debug("Audio engine restarted on second attempt after fallback.")
|
|
1960
|
+
} catch {
|
|
1961
|
+
Logger.debug("Fallback failed: Could not restart audio engine after tap reinstall. Pausing. Error: \(error)")
|
|
1962
|
+
performPauseAction(reason: .deviceSwitchFailed)
|
|
1963
|
+
return
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
} else {
|
|
1872
1967
|
Logger.debug("Audio engine was already running during fallback attempt? Unexpected state.")
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1968
|
+
}
|
|
1969
|
+
} else {
|
|
1970
|
+
Logger.debug("Recording was manually paused, leaving engine paused after fallback.")
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// Emit any remaining audio data from the previous device before resetting timers
|
|
1974
|
+
if !accumulatedData.isEmpty {
|
|
1975
|
+
Logger.debug("Emitting final audio chunk of \(accumulatedData.count) bytes from previous device")
|
|
1976
|
+
let recordingTime = currentRecordingDuration()
|
|
1977
|
+
let finalTotalSize = self.totalDataSize
|
|
1978
|
+
|
|
1979
|
+
// Create a copy of accumulated data to avoid race conditions
|
|
1980
|
+
let finalData = accumulatedData
|
|
1981
|
+
|
|
1982
|
+
// Notify delegate with final audio data from previous device
|
|
1983
|
+
delegate?.audioStreamManager(
|
|
1984
|
+
self,
|
|
1985
|
+
didReceiveAudioData: finalData,
|
|
1986
|
+
recordingTime: recordingTime,
|
|
1987
|
+
totalDataSize: finalTotalSize,
|
|
1988
|
+
compressionInfo: nil
|
|
1989
|
+
)
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// Reset emission timers to force new data emission with the fallback device
|
|
1993
|
+
lastEmissionTime = Date() // Reset to force immediate emission
|
|
1994
|
+
lastEmissionTimeAnalysis = Date() // Reset analysis timer too
|
|
1995
|
+
|
|
1996
|
+
// Important: Do not reset totalDataSize here - it needs to be maintained
|
|
1997
|
+
// We only clear the buffers to start accumulating new data from the fallback device
|
|
1998
|
+
accumulatedData.removeAll() // Clear any partial data from previous device
|
|
1999
|
+
accumulatedAnalysisData.removeAll() // Clear analysis data buffer
|
|
2000
|
+
Logger.debug("Emission timers reset. Current totalDataSize: \(totalDataSize)")
|
|
2001
|
+
|
|
2002
|
+
// CRITICAL: Multiple scheduled recovery attempts
|
|
2003
|
+
for delaySeconds in [0.5, 1.0, 2.0, 3.0] {
|
|
2004
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) { [weak self] in
|
|
2005
|
+
guard let self = self, self.isRecording, !self.isPaused else { return }
|
|
2006
|
+
Logger.debug("FALLBACK RECOVERY: Checking for data at \(delaySeconds)s")
|
|
2007
|
+
|
|
2008
|
+
// Force an immediate emission if data is being received but not emitted
|
|
2009
|
+
if !self.accumulatedData.isEmpty {
|
|
2010
|
+
Logger.debug("FALLBACK RECOVERY: Forcing emission from accumulated data after \(delaySeconds)s (total size: \(self.totalDataSize))")
|
|
2011
|
+
let dataToEmit = self.accumulatedData
|
|
2012
|
+
let recordingTime = self.currentRecordingDuration()
|
|
2013
|
+
let totalSize = self.totalDataSize
|
|
2014
|
+
|
|
2015
|
+
self.lastEmissionTime = Date() // Reset the emission timer
|
|
2016
|
+
self.accumulatedData.removeAll() // Clear the buffer
|
|
2017
|
+
|
|
2018
|
+
// Direct delegate call with accumulated data
|
|
2019
|
+
self.delegate?.audioStreamManager(
|
|
2020
|
+
self,
|
|
2021
|
+
didReceiveAudioData: dataToEmit,
|
|
2022
|
+
recordingTime: recordingTime,
|
|
2023
|
+
totalDataSize: totalSize,
|
|
2024
|
+
compressionInfo: nil
|
|
2025
|
+
)
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
// If we're at the 3-second mark and the engine appears to not be running, attempt restart
|
|
2029
|
+
if delaySeconds >= 3.0 && (!self.audioEngine.isRunning || self.lastEmissionTime!.timeIntervalSinceNow < -3) {
|
|
2030
|
+
Logger.debug("FALLBACK RECOVERY: Emergency engine restart attempt")
|
|
2031
|
+
do {
|
|
2032
|
+
self.audioEngine.reset()
|
|
2033
|
+
self.audioEngine.prepare()
|
|
2034
|
+
try self.audioEngine.start()
|
|
2035
|
+
self.lastEmissionTime = Date()
|
|
2036
|
+
} catch {
|
|
2037
|
+
Logger.debug("Emergency restart failed: \(error)")
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
1877
2042
|
|
|
1878
2043
|
// 7. Notify JS about successful fallback
|
|
1879
2044
|
delegate?.audioStreamManager(self, didReceiveInterruption: [
|