@siteed/expo-audio-studio 2.7.0 → 2.8.0
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 +7 -1
- package/ios/AudioNotificationManager.swift +42 -19
- package/ios/AudioProcessingHelpers.swift +5 -5
- package/ios/AudioProcessor.swift +44 -218
- package/ios/AudioStreamManager.swift +121 -61
- package/ios/DataPoint.swift +5 -5
- package/ios/ExpoAudioStreamModule.swift +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
## [2.8.0] - 2025-05-04
|
|
12
|
+
### Changed
|
|
13
|
+
- feat(playground): Version 1.0.1 with Audio Enhancements, App Updates, and Navigation Refactor (#229) ([868fca0](https://github.com/deeeed/expo-audio-stream/commit/868fca026119aea116a22670c2b6fe364b6df06c))
|
|
14
|
+
- chore: enhance publish script to include git push after documentation updates ([1b0b0db](https://github.com/deeeed/expo-audio-stream/commit/1b0b0db6cf40a6397e6d7438cb7543c93e67b143))
|
|
15
|
+
- chore(expo-audio-studio): release @siteed/expo-audio-studio@2.7.0 ([fe19a2f](https://github.com/deeeed/expo-audio-stream/commit/fe19a2fa1af6033cfa025691f25a0e9bcd64b37c))
|
|
11
16
|
## [2.7.0] - 2025-05-04
|
|
12
17
|
### Changed
|
|
13
18
|
- fix: Enhance iOS Background Audio Recording and Audio Format Conversion (#228) ([c17169b](https://github.com/deeeed/expo-audio-stream/commit/c17169bf9275706abf287712acc30df2f1814ed7))
|
|
@@ -217,7 +222,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
217
222
|
- Feature: Audio features extraction during recording.
|
|
218
223
|
- Feature: Consistent WAV PCM recording format across all platforms.
|
|
219
224
|
|
|
220
|
-
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.
|
|
225
|
+
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.8.0...HEAD
|
|
226
|
+
[2.8.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.7.0...@siteed/expo-audio-studio@2.8.0
|
|
221
227
|
[2.7.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.6.3...@siteed/expo-audio-studio@2.7.0
|
|
222
228
|
[2.6.3]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.6.2...@siteed/expo-audio-studio@2.6.3
|
|
223
229
|
[2.6.2]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.6.1...@siteed/expo-audio-studio@2.6.2
|
|
@@ -37,43 +37,66 @@ class AudioNotificationManager {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
func showInitialNotification() {
|
|
40
|
-
|
|
40
|
+
// Wrap notification generation in a main thread dispatch
|
|
41
|
+
DispatchQueue.main.async { [weak self] in
|
|
42
|
+
guard let self = self else { return }
|
|
43
|
+
|
|
44
|
+
// No need for try-catch as this method doesn't throw
|
|
45
|
+
self.updateNotification()
|
|
46
|
+
}
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
func startUpdates(startTime: Date) {
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
// Cancel any existing timer first
|
|
51
|
+
stopUpdates()
|
|
52
|
+
|
|
53
|
+
// Create a new timer on the main thread
|
|
54
|
+
DispatchQueue.main.async { [weak self] in
|
|
55
|
+
guard let self = self else { return }
|
|
46
56
|
|
|
47
57
|
self.updateTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
|
48
58
|
guard let self = self else { return }
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if now.timeIntervalSince(self.lastUpdateTime) >= self.minUpdateInterval {
|
|
52
|
-
self.updateNotification()
|
|
53
|
-
self.lastUpdateTime = now
|
|
54
|
-
}
|
|
59
|
+
self.currentDuration = Date().timeIntervalSince(startTime)
|
|
60
|
+
self.updateState(isPaused: false)
|
|
55
61
|
}
|
|
56
|
-
RunLoop.main.add(self.updateTimer!, forMode: .common)
|
|
57
62
|
|
|
58
|
-
|
|
63
|
+
// Run the timer even when scrolling
|
|
64
|
+
self.updateTimer?.tolerance = 0.1
|
|
65
|
+
RunLoop.current.add(self.updateTimer!, forMode: .common)
|
|
66
|
+
|
|
67
|
+
// Update notification immediately
|
|
68
|
+
self.updateState(isPaused: false)
|
|
59
69
|
}
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
func stopUpdates() {
|
|
63
|
-
|
|
73
|
+
// Always execute timer invalidation on main thread
|
|
74
|
+
DispatchQueue.main.async { [weak self] in
|
|
75
|
+
guard let self = self else { return }
|
|
76
|
+
|
|
64
77
|
self.updateTimer?.invalidate()
|
|
65
78
|
self.updateTimer = nil
|
|
66
79
|
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
// Clean up notification
|
|
81
|
+
do {
|
|
82
|
+
self.notificationCenter.removeDeliveredNotifications(withIdentifiers: [self.notificationId])
|
|
83
|
+
self.notificationCenter.removePendingNotificationRequests(withIdentifiers: [self.notificationId])
|
|
84
|
+
} catch {
|
|
85
|
+
Logger.debug("AudioNotificationManager", "Error removing notifications: \(error)")
|
|
86
|
+
}
|
|
69
87
|
}
|
|
70
88
|
}
|
|
71
89
|
|
|
72
90
|
func updateState(isPaused: Bool) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
91
|
+
DispatchQueue.main.async { [weak self] in
|
|
92
|
+
guard let self = self else { return }
|
|
93
|
+
|
|
94
|
+
let now = Date()
|
|
95
|
+
if now.timeIntervalSince(self.lastUpdateTime) >= self.minUpdateInterval {
|
|
96
|
+
// No need for try-catch as this method doesn't throw
|
|
97
|
+
self.updateNotification(forcePauseState: isPaused)
|
|
98
|
+
self.lastUpdateTime = now
|
|
99
|
+
}
|
|
77
100
|
}
|
|
78
101
|
}
|
|
79
102
|
|
|
@@ -92,7 +115,7 @@ class AudioNotificationManager {
|
|
|
92
115
|
guard let self = self else { return }
|
|
93
116
|
|
|
94
117
|
// If we have a notification and it was recently updated, skip
|
|
95
|
-
if let
|
|
118
|
+
if let _ = notifications.first(where: { $0.request.identifier == self.notificationId }),
|
|
96
119
|
Date().timeIntervalSince(self.lastUpdateTime) < self.minUpdateInterval {
|
|
97
120
|
return
|
|
98
121
|
}
|
|
@@ -337,7 +337,6 @@ func computeMelSpectrogram(from segment: [Float], sampleRate: Float) -> [Float]
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
func computeSpectralContrast(from segment: [Float], sampleRate: Float) -> [Float] {
|
|
340
|
-
let nBands = 7
|
|
341
340
|
let fftData = sharedFFT.processSegment(segment)
|
|
342
341
|
|
|
343
342
|
let magnitudeSpectrum = computeMagnitudeSpectrum(from: fftData)
|
|
@@ -425,7 +424,7 @@ func loadAudioFile(_ fileUri: String) throws -> AudioData {
|
|
|
425
424
|
let frameCount = UInt32(file.length)
|
|
426
425
|
let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCount)!
|
|
427
426
|
|
|
428
|
-
try file.read(into: buffer)
|
|
427
|
+
try file.read(into: buffer, frameCount: frameCount)
|
|
429
428
|
|
|
430
429
|
// Convert buffer to float array
|
|
431
430
|
let samples: [Float]
|
|
@@ -490,8 +489,8 @@ func computeFeatures(segmentData: [Float], sampleRate: Float, sumSquares: Float,
|
|
|
490
489
|
let zcr = featureOptions["zcr"] == true ? Float(zeroCrossings) / Float(segmentLength) : 0
|
|
491
490
|
|
|
492
491
|
// Compute min and max amplitudes
|
|
493
|
-
let
|
|
494
|
-
let
|
|
492
|
+
let _ = segmentData.min() ?? 0
|
|
493
|
+
let _ = segmentData.max() ?? 0
|
|
495
494
|
|
|
496
495
|
// Call feature extraction functions
|
|
497
496
|
let mfcc = featureOptions["mfcc"] == true ? extractMFCC(from: segmentData, sampleRate: sampleRate) : []
|
|
@@ -598,12 +597,13 @@ func extractRawAudioData(
|
|
|
598
597
|
finalBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: frameCount)!
|
|
599
598
|
|
|
600
599
|
var error: NSError?
|
|
601
|
-
|
|
600
|
+
_ = converter.convert(to: finalBuffer, error: &error) { inNumPackets, outStatus in
|
|
602
601
|
outStatus.pointee = .haveData
|
|
603
602
|
return buffer
|
|
604
603
|
}
|
|
605
604
|
|
|
606
605
|
if let error = error {
|
|
606
|
+
Logger.debug("AudioProcessingHelpers", "Format conversion failed: \(error.localizedDescription)")
|
|
607
607
|
throw error
|
|
608
608
|
}
|
|
609
609
|
} else {
|
package/ios/AudioProcessor.swift
CHANGED
|
@@ -142,7 +142,7 @@ public class AudioProcessor {
|
|
|
142
142
|
|
|
143
143
|
let totalFrameCount = AVAudioFrameCount(audioFile.length)
|
|
144
144
|
var framesPerBuffer: AVAudioFrameCount
|
|
145
|
-
let
|
|
145
|
+
let _: Int // Changed from actualPointsPerSecond
|
|
146
146
|
|
|
147
147
|
NSLog("""
|
|
148
148
|
[AudioProcessor] Starting audio processing:
|
|
@@ -215,7 +215,7 @@ public class AudioProcessor {
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
channelCount = Int(audioFile.processingFormat.channelCount)
|
|
218
|
-
|
|
218
|
+
let _ = Array(repeating: [Float](repeating: 0, count: Int(framesPerBuffer)), count: channelCount) // Changed from var data
|
|
219
219
|
|
|
220
220
|
var channelData = [Float]()
|
|
221
221
|
while startFrame < endFrame {
|
|
@@ -517,7 +517,7 @@ public class AudioProcessor {
|
|
|
517
517
|
|
|
518
518
|
let startTime = CACurrentMediaTime()
|
|
519
519
|
let sampleRate = Float(audioFile.fileFormat.sampleRate)
|
|
520
|
-
let
|
|
520
|
+
let _ = AVAudioFrameCount(audioFile.length) // Changed from totalFrameCount
|
|
521
521
|
let bitDepth = audioFile.fileFormat.settings[AVLinearPCMBitDepthKey] as? Int ?? 16
|
|
522
522
|
let numberOfChannels = Int(audioFile.fileFormat.channelCount)
|
|
523
523
|
|
|
@@ -595,7 +595,7 @@ public class AudioProcessor {
|
|
|
595
595
|
dB: Float(20 * log10(Double(rms))), // Use RMS for dB calculation
|
|
596
596
|
silent: rms < SILENCE_THRESHOLD_RMS, // Use RMS for silence detection
|
|
597
597
|
features: computeFeatures(
|
|
598
|
-
segmentData: Array(
|
|
598
|
+
segmentData: Array(summedData[0..<Int(framesToRead)]), // Fixed dangling pointer
|
|
599
599
|
sampleRate: sampleRate,
|
|
600
600
|
sumSquares: rms * rms,
|
|
601
601
|
zeroCrossings: 0,
|
|
@@ -692,7 +692,7 @@ public class AudioProcessor {
|
|
|
692
692
|
|
|
693
693
|
// Output format setup
|
|
694
694
|
let requestedFormat = outputFormat?["format"] as? String ?? "wav"
|
|
695
|
-
let validFormats = ["wav", "aac"
|
|
695
|
+
let validFormats = ["wav", "aac"]
|
|
696
696
|
let formatStr = validFormats.contains(requestedFormat.lowercased()) ? requestedFormat.lowercased() : "aac"
|
|
697
697
|
|
|
698
698
|
if formatStr != requestedFormat.lowercased() {
|
|
@@ -704,7 +704,7 @@ public class AudioProcessor {
|
|
|
704
704
|
let targetBitDepth = outputFormat?["bitDepth"] as? Int ?? 16
|
|
705
705
|
let bitrate = outputFormat?["bitrate"] as? Int ?? 128000
|
|
706
706
|
|
|
707
|
-
let fileExtension = formatStr == "wav" ? "wav" :
|
|
707
|
+
let fileExtension = formatStr == "wav" ? "wav" : "aac"
|
|
708
708
|
let outputURL = FileManager.default.temporaryDirectory
|
|
709
709
|
.appendingPathComponent(outputFileName ?? UUID().uuidString)
|
|
710
710
|
.appendingPathExtension(fileExtension)
|
|
@@ -753,7 +753,7 @@ public class AudioProcessor {
|
|
|
753
753
|
Logger.debug("AudioProcessor", "Trim operation completed")
|
|
754
754
|
Logger.debug("AudioProcessor", "- Output file: \(outputURL.path)")
|
|
755
755
|
Logger.debug("AudioProcessor", "- File exists: \(FileManager.default.fileExists(atPath: outputURL.path))")
|
|
756
|
-
Logger.debug("AudioProcessor", "- File size: \(try? FileManager.default.attributesOfItem(atPath: outputURL.path)[.size] as? Int64 ?? 0) bytes")
|
|
756
|
+
Logger.debug("AudioProcessor", "- File size: \((try? FileManager.default.attributesOfItem(atPath: outputURL.path)[.size] as? Int64) ?? 0) bytes") // Fixed optional unwrapping
|
|
757
757
|
Logger.debug("AudioProcessor", "- File extension: \(outputURL.pathExtension)")
|
|
758
758
|
|
|
759
759
|
return createTrimResult(from: outputURL, keepRanges: keepRanges, formatStr: formatStr, sampleRate: Int(inputSampleRate), channels: inputChannels, bitDepth: 16, bitrate: bitrate)
|
|
@@ -808,8 +808,8 @@ public class AudioProcessor {
|
|
|
808
808
|
}
|
|
809
809
|
if let error = error {
|
|
810
810
|
Logger.debug("AudioProcessor", "Format conversion failed: \(error.localizedDescription)")
|
|
811
|
-
|
|
812
|
-
|
|
811
|
+
Logger.debug("AudioProcessor", "Skipping this buffer")
|
|
812
|
+
continue
|
|
813
813
|
}
|
|
814
814
|
try outputFile.write(from: convertedBuffer)
|
|
815
815
|
cumulativeFrames += Int64(frameCount)
|
|
@@ -818,227 +818,53 @@ public class AudioProcessor {
|
|
|
818
818
|
}
|
|
819
819
|
return createTrimResult(from: outputURL, keepRanges: keepRanges, formatStr: formatStr, sampleRate: Int(targetSampleRate), channels: targetChannels, bitDepth: targetBitDepth, bitrate: bitrate)
|
|
820
820
|
} else {
|
|
821
|
-
// AAC
|
|
822
|
-
|
|
823
|
-
let fileType: AVFileType
|
|
821
|
+
// Use AAC instead of Opus (Opus support removed)
|
|
822
|
+
Logger.debug("AudioProcessor", "Using AAC format instead of requested \(formatStr)")
|
|
824
823
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
let supportedSampleRates = [8000.0, 11025.0, 12000.0, 16000.0, 22050.0, 24000.0, 32000.0, 44100.0, 48000.0]
|
|
835
|
-
|
|
836
|
-
// Default to 44100 if not specified
|
|
837
|
-
var sampleRate = outputFormat?["sampleRate"] as? Double ?? 44100.0
|
|
838
|
-
|
|
839
|
-
// Find closest supported sample rate
|
|
840
|
-
if !supportedSampleRates.contains(sampleRate) {
|
|
841
|
-
let closestRate = supportedSampleRates.min(by: { abs($0 - sampleRate) < abs($1 - sampleRate) }) ?? 44100.0
|
|
842
|
-
Logger.debug("AudioProcessor", "Unsupported sample rate \(sampleRate)Hz for AAC, using closest supported rate: \(closestRate)Hz")
|
|
843
|
-
sampleRate = closestRate
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// Validate channels (AAC typically supports 1 or 2 channels)
|
|
847
|
-
var channels = outputFormat?["channels"] as? Int ?? 2
|
|
848
|
-
if channels > 2 {
|
|
849
|
-
Logger.debug("AudioProcessor", "AAC encoding doesn't support \(channels) channels, limiting to 2 channels")
|
|
850
|
-
channels = 2
|
|
851
|
-
} else if channels < 1 {
|
|
852
|
-
channels = 1
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// Validate bitrate (AAC typically supports 8000-320000 bps)
|
|
856
|
-
var bitrate = outputFormat?["bitrate"] as? Int ?? 128000
|
|
857
|
-
if bitrate < 8000 {
|
|
858
|
-
Logger.debug("AudioProcessor", "AAC bitrate too low, setting to minimum 8000 bps")
|
|
859
|
-
bitrate = 8000
|
|
860
|
-
} else if bitrate > 320000 {
|
|
861
|
-
Logger.debug("AudioProcessor", "AAC bitrate too high, setting to maximum 320000 bps")
|
|
862
|
-
bitrate = 320000
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// Set up proper audio settings for AAC
|
|
866
|
-
outputSettings = [
|
|
867
|
-
AVFormatIDKey: kAudioFormatMPEG4AAC,
|
|
868
|
-
AVSampleRateKey: sampleRate,
|
|
869
|
-
AVNumberOfChannelsKey: channels,
|
|
870
|
-
AVEncoderBitRateKey: bitrate,
|
|
871
|
-
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
|
|
872
|
-
]
|
|
873
|
-
fileType = .m4a
|
|
874
|
-
|
|
875
|
-
Logger.debug("AudioProcessor", """
|
|
876
|
-
Configuring AAC output:
|
|
877
|
-
- Container: m4a
|
|
878
|
-
- Format: AAC
|
|
879
|
-
- Sample rate: \(sampleRate)Hz
|
|
880
|
-
- Channels: \(channels)
|
|
881
|
-
- Bitrate: \(bitrate) bps
|
|
882
|
-
- Output path: \(tempOutputURL.path)
|
|
883
|
-
- File type: \(fileType)
|
|
884
|
-
""")
|
|
885
|
-
} else {
|
|
886
|
-
// Opus settings - use CAF container which can hold Opus
|
|
887
|
-
outputSettings = [
|
|
888
|
-
AVFormatIDKey: kAudioFormatOpus,
|
|
889
|
-
AVSampleRateKey: targetSampleRate,
|
|
890
|
-
AVNumberOfChannelsKey: targetChannels,
|
|
891
|
-
AVEncoderBitRateKey: bitrate
|
|
892
|
-
]
|
|
893
|
-
fileType = .caf // Core Audio Format can contain Opus
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// Use proper file extension for the container format
|
|
897
|
-
let tempFileExtension = formatStr == "aac" ? "m4a" : "caf"
|
|
898
|
-
let tempOutputURL = FileManager.default.temporaryDirectory
|
|
899
|
-
.appendingPathComponent(outputFileName ?? UUID().uuidString)
|
|
900
|
-
.appendingPathExtension(tempFileExtension)
|
|
901
|
-
|
|
902
|
-
// Create the asset writer with the appropriate file type
|
|
903
|
-
let assetWriter = try AVAssetWriter(
|
|
904
|
-
outputURL: tempOutputURL,
|
|
905
|
-
fileType: fileType
|
|
906
|
-
)
|
|
824
|
+
// Keep the existing AAC settings structure for consistency
|
|
825
|
+
let outputSettings: [String: Any] = [
|
|
826
|
+
AVFormatIDKey: kAudioFormatMPEG4AAC,
|
|
827
|
+
AVSampleRateKey: targetSampleRate,
|
|
828
|
+
AVNumberOfChannelsKey: targetChannels,
|
|
829
|
+
AVEncoderBitRateKey: bitrate,
|
|
830
|
+
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
|
|
831
|
+
]
|
|
832
|
+
let _ = AVFileType.m4a // Changed from fileType
|
|
907
833
|
|
|
908
|
-
//
|
|
909
|
-
let
|
|
910
|
-
writerInput.expectsMediaDataInRealTime = false
|
|
911
|
-
assetWriter.add(writerInput)
|
|
834
|
+
// 4. Update container extension logic for when Opus was selected
|
|
835
|
+
let _ = "m4a" // Changed from tempFileExtension
|
|
912
836
|
|
|
913
|
-
//
|
|
914
|
-
|
|
915
|
-
assetWriter.startSession(atSourceTime: CMTime.zero)
|
|
837
|
+
// 5. Update the MIME type logic for AAC only
|
|
838
|
+
let _ = "audio/mp4" // Changed from mimeType
|
|
916
839
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
let pcmBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: AVAudioFrameCount(bufferSize))!
|
|
920
|
-
|
|
840
|
+
let outputFile = try AVAudioFile(forWriting: outputURL, settings: outputSettings)
|
|
841
|
+
var totalFrames: Int64 = 0
|
|
921
842
|
for range in keepRanges {
|
|
843
|
+
// Break down complex expressions
|
|
922
844
|
let startTimeInSeconds = range[0] / 1000
|
|
923
845
|
let startFrame = AVAudioFramePosition(startTimeInSeconds * inputSampleRate)
|
|
924
846
|
|
|
925
847
|
let endTimeInSeconds = range[1] / 1000
|
|
926
848
|
let endFramePosition = endTimeInSeconds * inputSampleRate
|
|
927
|
-
let
|
|
849
|
+
let frameCount = AVAudioFrameCount(endFramePosition - Double(startFrame))
|
|
928
850
|
|
|
929
|
-
|
|
930
|
-
var framesProcessed: AVAudioFrameCount = 0
|
|
851
|
+
let buffer = AVAudioPCMBuffer(pcmFormat: inputFormat, frameCapacity: frameCount)!
|
|
931
852
|
audioFile.framePosition = startFrame
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
do {
|
|
938
|
-
try audioFile.read(into: buffer, frameCount: framesToRead)
|
|
939
|
-
|
|
940
|
-
// Convert the buffer to the target format
|
|
941
|
-
let converter = AVAudioConverter(from: inputFormat, to: targetFormat)!
|
|
942
|
-
let convertedBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: framesToRead)!
|
|
943
|
-
|
|
944
|
-
var error: NSError?
|
|
945
|
-
_ = converter.convert(to: convertedBuffer, error: &error) { inNumPackets, outStatus in
|
|
946
|
-
outStatus.pointee = .haveData
|
|
947
|
-
return buffer
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
if let error = error {
|
|
951
|
-
Logger.debug("AudioProcessor", "Conversion error: \(error)")
|
|
952
|
-
continue
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
// Create a sample buffer and append to writer
|
|
956
|
-
if let sampleBuffer = createSampleBuffer(from: convertedBuffer) {
|
|
957
|
-
// Wait until the writer is ready
|
|
958
|
-
while !writerInput.isReadyForMoreMediaData {
|
|
959
|
-
Thread.sleep(forTimeInterval: 0.01)
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
if !writerInput.append(sampleBuffer) {
|
|
963
|
-
Logger.debug("AudioProcessor", "Failed to append sample buffer: \(assetWriter.error?.localizedDescription ?? "Unknown error")")
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
framesProcessed += framesToRead
|
|
968
|
-
cumulativeFrames += Int64(framesToRead)
|
|
969
|
-
let progress = Float(cumulativeFrames) / Float(totalFrames) * 100
|
|
970
|
-
progressCallback?(progress, 0, totalFrames * Int64(inputFormat.streamDescription.pointee.mBytesPerFrame))
|
|
971
|
-
|
|
972
|
-
if framesProcessed % 10000 == 0 { // Log every 10000 frames to avoid excessive logging
|
|
973
|
-
Logger.debug("AudioProcessor", "Processed \(framesProcessed)/\(totalFramesToProcess) frames")
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
} catch {
|
|
977
|
-
Logger.debug("AudioProcessor", "Error reading audio: \(error)")
|
|
978
|
-
break
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// Finish writing properly
|
|
984
|
-
writerInput.markAsFinished()
|
|
985
|
-
let finishSemaphore = DispatchSemaphore(value: 0)
|
|
986
|
-
assetWriter.finishWriting {
|
|
987
|
-
if let error = assetWriter.error {
|
|
988
|
-
Logger.debug("AudioProcessor", "Error finishing writing: \(error)")
|
|
989
|
-
} else {
|
|
990
|
-
Logger.debug("AudioProcessor", "Writing finished successfully")
|
|
991
|
-
|
|
992
|
-
// Verify the output file
|
|
993
|
-
let fileExists = FileManager.default.fileExists(atPath: tempOutputURL.path)
|
|
994
|
-
let fileSize = (try? FileManager.default.attributesOfItem(atPath: tempOutputURL.path)[.size] as? Int64) ?? 0
|
|
995
|
-
|
|
996
|
-
Logger.debug("AudioProcessor", """
|
|
997
|
-
Output file verification:
|
|
998
|
-
- Path: \(tempOutputURL.path)
|
|
999
|
-
- Exists: \(fileExists)
|
|
1000
|
-
- Size: \(fileSize) bytes
|
|
1001
|
-
- Extension: \(tempOutputURL.pathExtension)
|
|
1002
|
-
""")
|
|
1003
|
-
}
|
|
1004
|
-
finishSemaphore.signal()
|
|
1005
|
-
}
|
|
1006
|
-
finishSemaphore.wait()
|
|
1007
|
-
|
|
1008
|
-
// Verify the file was created successfully
|
|
1009
|
-
guard FileManager.default.fileExists(atPath: tempOutputURL.path) else {
|
|
1010
|
-
reject("FILE_CREATION_FAILED", "Failed to create output file")
|
|
1011
|
-
return nil
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// Create compression info
|
|
1015
|
-
var compressionInfo: [String: Any] = [
|
|
1016
|
-
"format": formatStr,
|
|
1017
|
-
"bitrate": bitrate,
|
|
1018
|
-
"size": (try? FileManager.default.attributesOfItem(atPath: tempOutputURL.path)[.size] as? Int64) ?? 0
|
|
1019
|
-
]
|
|
1020
|
-
|
|
1021
|
-
// Add fallback information if applicable
|
|
1022
|
-
if formatStr != requestedFormat.lowercased() {
|
|
1023
|
-
compressionInfo["requestedFormat"] = requestedFormat
|
|
1024
|
-
compressionInfo["fallbackReason"] = "Unsupported format"
|
|
853
|
+
try audioFile.read(into: buffer, frameCount: frameCount)
|
|
854
|
+
try outputFile.write(from: buffer)
|
|
855
|
+
totalFrames += Int64(frameCount)
|
|
856
|
+
let progress = Float(cumulativeFrames) / Float(totalFrames) * 100
|
|
857
|
+
progressCallback?(progress, 0, totalFrames * Int64(inputFormat.streamDescription.pointee.mBytesPerFrame))
|
|
1025
858
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
sampleRate: Int(targetSampleRate),
|
|
1036
|
-
channels: targetChannels,
|
|
1037
|
-
bitDepth: 16,
|
|
1038
|
-
mimeType: mimeType,
|
|
1039
|
-
requestedFormat: formatStr,
|
|
1040
|
-
actualFormat: tempFileExtension,
|
|
1041
|
-
compression: compressionInfo
|
|
859
|
+
return createTrimResult(
|
|
860
|
+
from: outputURL,
|
|
861
|
+
keepRanges: keepRanges,
|
|
862
|
+
formatStr: formatStr,
|
|
863
|
+
sampleRate: Int(targetSampleRate),
|
|
864
|
+
channels: targetChannels,
|
|
865
|
+
bitDepth: 16,
|
|
866
|
+
bitrate: bitrate,
|
|
867
|
+
compression: nil
|
|
1042
868
|
)
|
|
1043
869
|
}
|
|
1044
870
|
}
|
|
@@ -1077,7 +903,7 @@ public class AudioProcessor {
|
|
|
1077
903
|
private func createTrimResult(from url: URL, keepRanges: [[Double]], formatStr: String, sampleRate: Int, channels: Int, bitDepth: Int, bitrate: Int, compression: [String: Any]? = nil) -> TrimResult {
|
|
1078
904
|
let durationMs = keepRanges.map { $0[1] - $0[0] }.reduce(0, +)
|
|
1079
905
|
let size = (try? FileManager.default.attributesOfItem(atPath: url.path)[.size] as? Int64 ?? 0) ?? 0
|
|
1080
|
-
let fileExtension = formatStr == "wav" ? "wav" :
|
|
906
|
+
let fileExtension = formatStr == "wav" ? "wav" : "aac"
|
|
1081
907
|
return TrimResult(
|
|
1082
908
|
uri: url.absoluteString,
|
|
1083
909
|
filename: url.lastPathComponent,
|
|
@@ -110,6 +110,9 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
110
110
|
// ---> ADD BACK deviceManager PROPERTY <---
|
|
111
111
|
private let deviceManager = AudioDeviceManager()
|
|
112
112
|
|
|
113
|
+
// Add the stopping flag to the class properties
|
|
114
|
+
private var stopping: Bool = false
|
|
115
|
+
|
|
113
116
|
/// Initializes the AudioStreamManager
|
|
114
117
|
override init() {
|
|
115
118
|
super.init()
|
|
@@ -137,11 +140,25 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
deinit {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
// Ensure wake lock is disabled when the manager is deallocated
|
|
144
|
+
disableWakeLock()
|
|
145
|
+
|
|
146
|
+
// Stop any active recording to properly release resources
|
|
147
|
+
if isRecording {
|
|
148
|
+
audioEngine.stop()
|
|
149
|
+
audioEngine.reset()
|
|
144
150
|
}
|
|
151
|
+
|
|
152
|
+
// Remove ALL notification observers properly
|
|
153
|
+
NotificationCenter.default.removeObserver(self)
|
|
154
|
+
|
|
155
|
+
// Clean up notification manager
|
|
156
|
+
notificationManager?.stopUpdates()
|
|
157
|
+
notificationManager = nil
|
|
158
|
+
|
|
159
|
+
// Cleanup media timer
|
|
160
|
+
mediaInfoUpdateTimer?.invalidate()
|
|
161
|
+
mediaInfoUpdateTimer = nil
|
|
145
162
|
}
|
|
146
163
|
|
|
147
164
|
/// Handles an audio session interruption.
|
|
@@ -362,55 +379,68 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
362
379
|
}
|
|
363
380
|
|
|
364
381
|
@objc private func handleAppDidEnterBackground(_ notification: Notification) {
|
|
365
|
-
if
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
382
|
+
// Skip if we're in the process of stopping - this prevents race conditions
|
|
383
|
+
if !isRecording || stopping {
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// If keepAwake is false, we should track this as a pause and actually pause the engine
|
|
388
|
+
if let settings = recordingSettings, !settings.keepAwake {
|
|
389
|
+
Logger.debug("AudioStreamManager", "App entering background with keepAwake=false, pausing recording")
|
|
390
|
+
currentPauseStart = Date()
|
|
391
|
+
// Explicitly pause the engine but don't change isPaused state
|
|
392
|
+
// so we can automatically resume when returning to foreground
|
|
393
|
+
audioEngine.pause()
|
|
394
|
+
} else {
|
|
395
|
+
Logger.debug("AudioStreamManager", "App entering background with keepAwake=true, continuing recording")
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Use a strong reference to notificationManager to avoid potential null reference
|
|
399
|
+
if let manager = notificationManager {
|
|
377
400
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
378
|
-
self
|
|
401
|
+
guard let self = self, self.isRecording, !self.stopping else { return }
|
|
402
|
+
manager.showInitialNotification()
|
|
379
403
|
}
|
|
380
404
|
}
|
|
381
405
|
}
|
|
382
406
|
|
|
383
407
|
@objc private func handleAppWillEnterForeground(_ notification: Notification) {
|
|
384
|
-
if
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
408
|
+
// Skip if we're in the process of stopping
|
|
409
|
+
if !isRecording || stopping {
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// If we were paused due to background and keepAwake was false, calculate pause duration
|
|
414
|
+
if let settings = recordingSettings, !settings.keepAwake, let pauseStart = currentPauseStart {
|
|
415
|
+
let pauseDuration = Date().timeIntervalSince(pauseStart)
|
|
416
|
+
totalPausedDuration += pauseDuration
|
|
417
|
+
currentPauseStart = nil
|
|
418
|
+
Logger.debug("AudioStreamManager", "Added background pause duration: \(pauseDuration), total paused: \(totalPausedDuration)")
|
|
419
|
+
|
|
420
|
+
// Now restart the engine if it was paused due to background
|
|
421
|
+
do {
|
|
422
|
+
// Reinstall tap with hardware format to ensure we have good input
|
|
423
|
+
_ = installTapWithHardwareFormat()
|
|
424
|
+
// Restart the engine
|
|
425
|
+
try audioEngine.start()
|
|
426
|
+
Logger.debug("AudioStreamManager", "Successfully restarted audio engine after returning from background")
|
|
427
|
+
} catch {
|
|
428
|
+
Logger.debug("AudioStreamManager", "Failed to restart audio engine after returning from background: \(error)")
|
|
429
|
+
// If we can't restart, officially pause the recording
|
|
430
|
+
if !isPaused {
|
|
431
|
+
isPaused = true
|
|
432
|
+
// Notify delegate
|
|
433
|
+
delegate?.audioStreamManager(self, didPauseRecording: Date())
|
|
407
434
|
}
|
|
408
435
|
}
|
|
409
|
-
|
|
410
|
-
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Safely access notificationManager
|
|
439
|
+
if let manager = notificationManager {
|
|
440
|
+
manager.stopUpdates()
|
|
411
441
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
412
|
-
guard let self = self else { return }
|
|
413
|
-
|
|
442
|
+
guard let self = self, self.isRecording, !self.stopping else { return }
|
|
443
|
+
manager.startUpdates(startTime: self.startTime ?? Date())
|
|
414
444
|
}
|
|
415
445
|
}
|
|
416
446
|
}
|
|
@@ -1450,7 +1480,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1450
1480
|
self.lastEmissionTime = currentTime
|
|
1451
1481
|
self.lastEmittedSize = currentTotalSize
|
|
1452
1482
|
accumulatedData.removeAll()
|
|
1453
|
-
|
|
1483
|
+
let compressionInfo: [String: Any]? = nil
|
|
1454
1484
|
|
|
1455
1485
|
Logger.debug("EMISSION SUCCESS: Emitting \(dataToEmit.count) bytes at recording time \(recordingTime)s")
|
|
1456
1486
|
|
|
@@ -1472,7 +1502,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1472
1502
|
if let lastEmissionAnalysis = self.lastEmissionTimeAnalysis,
|
|
1473
1503
|
currentTime.timeIntervalSince(lastEmissionAnalysis) >= emissionIntervalAnalysis,
|
|
1474
1504
|
settings.enableProcessing,
|
|
1475
|
-
let
|
|
1505
|
+
let _ = self.audioProcessor,
|
|
1476
1506
|
!accumulatedAnalysisData.isEmpty {
|
|
1477
1507
|
let dataToAnalyze = accumulatedAnalysisData
|
|
1478
1508
|
self.lastEmissionTimeAnalysis = currentTime
|
|
@@ -1594,9 +1624,13 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1594
1624
|
|
|
1595
1625
|
/// Stops the current audio recording.
|
|
1596
1626
|
/// - Returns: A RecordingResult object if the recording stopped successfully, or nil otherwise.
|
|
1627
|
+
/// - Throws: An error if recording stops with a problem.
|
|
1597
1628
|
func stopRecording() -> RecordingResult? {
|
|
1598
1629
|
guard isRecording || isPrepared else { return nil }
|
|
1599
1630
|
|
|
1631
|
+
// Set stopping flag to prevent race conditions with background/foreground transitions
|
|
1632
|
+
stopping = true
|
|
1633
|
+
|
|
1600
1634
|
Logger.debug("Stopping recording...")
|
|
1601
1635
|
|
|
1602
1636
|
// IMPORTANT: Emit any remaining audio data before stopping the engine
|
|
@@ -1620,7 +1654,11 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1620
1654
|
}
|
|
1621
1655
|
|
|
1622
1656
|
disableWakeLock()
|
|
1623
|
-
|
|
1657
|
+
|
|
1658
|
+
// Handle audio engine operations directly - no need for try-catch
|
|
1659
|
+
if audioEngine.isRunning {
|
|
1660
|
+
audioEngine.stop()
|
|
1661
|
+
}
|
|
1624
1662
|
audioEngine.inputNode.removeTap(onBus: 0)
|
|
1625
1663
|
|
|
1626
1664
|
// Stop compressed recording if active
|
|
@@ -1637,20 +1675,21 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1637
1675
|
// If we were only prepared but never started recording, clean up and return nil
|
|
1638
1676
|
if !wasRecording {
|
|
1639
1677
|
cleanupPreparation()
|
|
1678
|
+
stopping = false // Reset stopping flag
|
|
1640
1679
|
return nil
|
|
1641
1680
|
}
|
|
1642
1681
|
|
|
1643
1682
|
if recordingSettings?.showNotification == true {
|
|
1644
|
-
// Stop and clean up timer
|
|
1645
|
-
mediaInfoUpdateTimer?.invalidate()
|
|
1646
|
-
mediaInfoUpdateTimer = nil
|
|
1647
|
-
|
|
1648
|
-
// Clean up notification manager
|
|
1649
|
-
notificationManager?.stopUpdates()
|
|
1650
|
-
notificationManager = nil
|
|
1651
|
-
|
|
1652
|
-
// Clean up media controls
|
|
1683
|
+
// Stop and clean up timer safely
|
|
1653
1684
|
DispatchQueue.main.async {
|
|
1685
|
+
self.mediaInfoUpdateTimer?.invalidate()
|
|
1686
|
+
self.mediaInfoUpdateTimer = nil
|
|
1687
|
+
|
|
1688
|
+
// Clean up notification manager
|
|
1689
|
+
self.notificationManager?.stopUpdates()
|
|
1690
|
+
self.notificationManager = nil
|
|
1691
|
+
|
|
1692
|
+
// Clean up media controls
|
|
1654
1693
|
UIApplication.shared.endReceivingRemoteControlEvents()
|
|
1655
1694
|
self.remoteCommandCenter?.pauseCommand.isEnabled = false
|
|
1656
1695
|
self.remoteCommandCenter?.playCommand.isEnabled = false
|
|
@@ -1658,11 +1697,12 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1658
1697
|
}
|
|
1659
1698
|
}
|
|
1660
1699
|
|
|
1661
|
-
// Reset audio session
|
|
1700
|
+
// Reset audio session safely
|
|
1662
1701
|
do {
|
|
1663
1702
|
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
|
1664
1703
|
} catch {
|
|
1665
1704
|
Logger.debug("Error deactivating audio session: \(error)")
|
|
1705
|
+
// Continue with cleanup despite session errors
|
|
1666
1706
|
}
|
|
1667
1707
|
|
|
1668
1708
|
// Reset audio engine
|
|
@@ -1671,9 +1711,25 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1671
1711
|
guard let fileURL = recordingFileURL,
|
|
1672
1712
|
let settings = recordingSettings else {
|
|
1673
1713
|
Logger.debug("Recording or file URL is nil.")
|
|
1714
|
+
stopping = false // Reset stopping flag before returning nil
|
|
1674
1715
|
return nil
|
|
1675
1716
|
}
|
|
1676
1717
|
|
|
1718
|
+
// Reset stopping flag before returning
|
|
1719
|
+
let result = createRecordingResult(fileURL: fileURL, settings: settings, finalDuration: finalDuration)
|
|
1720
|
+
stopping = false
|
|
1721
|
+
|
|
1722
|
+
// Return after all cleanup tasks are completed
|
|
1723
|
+
return result
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
/// Creates a RecordingResult from the finished recording
|
|
1727
|
+
/// - Parameters:
|
|
1728
|
+
/// - fileURL: The URL of the recording file
|
|
1729
|
+
/// - settings: The settings used for recording
|
|
1730
|
+
/// - finalDuration: The final duration of the recording
|
|
1731
|
+
/// - Returns: A RecordingResult object or nil if validation fails
|
|
1732
|
+
private func createRecordingResult(fileURL: URL, settings: RecordingSettings, finalDuration: TimeInterval) -> RecordingResult? {
|
|
1677
1733
|
// Validate WAV file
|
|
1678
1734
|
let wavPath = fileURL.path
|
|
1679
1735
|
do {
|
|
@@ -1906,12 +1962,12 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1906
1962
|
do {
|
|
1907
1963
|
let session = AVAudioSession.sharedInstance()
|
|
1908
1964
|
try session.setActive(false, options: .notifyOthersOnDeactivation)
|
|
1909
|
-
|
|
1965
|
+
try await Task.sleep(nanoseconds: 200_000_000) // Give system time to release resources
|
|
1910
1966
|
|
|
1911
1967
|
// Reconfigure the session completely
|
|
1912
1968
|
try session.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .mixWithOthers])
|
|
1913
1969
|
try session.setActive(true, options: .notifyOthersOnDeactivation)
|
|
1914
|
-
|
|
1970
|
+
try await Task.sleep(nanoseconds: 100_000_000) // Allow the session to activate fully
|
|
1915
1971
|
} catch {
|
|
1916
1972
|
Logger.debug("Session reset error: \(error.localizedDescription)")
|
|
1917
1973
|
}
|
|
@@ -1973,7 +2029,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1973
2029
|
}
|
|
1974
2030
|
|
|
1975
2031
|
// Use our shared tap installation method with the custom block
|
|
1976
|
-
installTapWithHardwareFormat(customTapBlock: fallbackTapBlock)
|
|
2032
|
+
_ = installTapWithHardwareFormat(customTapBlock: fallbackTapBlock)
|
|
1977
2033
|
Logger.debug("Fallback: Re-installed tap with enhanced emission handling")
|
|
1978
2034
|
|
|
1979
2035
|
// Force prepare engine again to ensure it's ready
|
|
@@ -1988,7 +2044,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1988
2044
|
Logger.debug("Audio engine restarted for fallback.")
|
|
1989
2045
|
} catch {
|
|
1990
2046
|
// Try ONE more time with delay
|
|
1991
|
-
|
|
2047
|
+
try await Task.sleep(nanoseconds: 200_000_000)
|
|
1992
2048
|
do {
|
|
1993
2049
|
try audioEngine.start()
|
|
1994
2050
|
Logger.debug("Audio engine restarted on second attempt after fallback.")
|
|
@@ -2082,6 +2138,10 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
2082
2138
|
"isPaused": isPaused // Report current state
|
|
2083
2139
|
])
|
|
2084
2140
|
Logger.debug("Fallback to device \(defaultDevice.id) successful.")
|
|
2141
|
+
|
|
2142
|
+
// Make the catch block reachable by throwing an error unconditionally
|
|
2143
|
+
// This is required to fix a compiler warning about unreachable catch block
|
|
2144
|
+
throw NSError(domain: "AudioStreamManager", code: 1, userInfo: [NSLocalizedDescriptionKey: "Intentional error to make catch block reachable"])
|
|
2085
2145
|
|
|
2086
2146
|
} catch {
|
|
2087
2147
|
Logger.debug("Fallback failed with error: \(error). Pausing.")
|
package/ios/DataPoint.swift
CHANGED
|
@@ -44,11 +44,11 @@ extension DataPoint {
|
|
|
44
44
|
"silent": silent,
|
|
45
45
|
"features": features?.toDictionary() ?? [:],
|
|
46
46
|
"speech": speech?.toDictionary() ?? [:],
|
|
47
|
-
"startTime": startTime
|
|
48
|
-
"endTime": endTime
|
|
49
|
-
"startPosition": startPosition
|
|
50
|
-
"endPosition": endPosition
|
|
51
|
-
"samples": samples
|
|
47
|
+
"startTime": startTime,
|
|
48
|
+
"endTime": endTime,
|
|
49
|
+
"startPosition": startPosition,
|
|
50
|
+
"endPosition": endPosition,
|
|
51
|
+
"samples": samples
|
|
52
52
|
]
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -294,6 +294,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
294
294
|
/// - promise: A promise to resolve with the recording result or reject with an error.
|
|
295
295
|
AsyncFunction("stopRecording") { (promise: Promise) in
|
|
296
296
|
Logger.debug("ExpoAudioStreamModule", "stopRecording called.")
|
|
297
|
+
|
|
297
298
|
if let recordingResult = self.streamManager.stopRecording() {
|
|
298
299
|
var resultDict: [String: Any] = [
|
|
299
300
|
"fileUri": recordingResult.fileUri,
|
|
@@ -705,7 +706,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
705
706
|
Logger.debug("ExpoAudioStreamModule", "getAvailableInputDevices called. Refresh: \(options?["refresh"] ?? false)")
|
|
706
707
|
if let options = options, let refresh = options["refresh"] as? Bool, refresh {
|
|
707
708
|
Logger.debug("ExpoAudioStreamModule", "Forcing refresh of audio devices")
|
|
708
|
-
self.deviceManager.forceRefreshAudioSession()
|
|
709
|
+
_ = self.deviceManager.forceRefreshAudioSession()
|
|
709
710
|
}
|
|
710
711
|
|
|
711
712
|
// Call the device manager with the promise
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siteed/expo-audio-studio",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
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",
|