@siteed/expo-audio-stream 1.5.2 → 1.7.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 +19 -27
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +13 -2
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts +74 -0
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -0
- package/build/AudioAnalysis/AudioAnalysis.types.js +3 -0
- package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -0
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts +22 -0
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -0
- package/build/AudioAnalysis/extractAudioAnalysis.js +86 -0
- package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -0
- package/build/AudioAnalysis/extractWaveform.d.ts +8 -0
- package/build/AudioAnalysis/extractWaveform.d.ts.map +1 -0
- package/build/AudioAnalysis/extractWaveform.js +11 -0
- package/build/AudioAnalysis/extractWaveform.js.map +1 -0
- package/build/AudioRecorder.provider.d.ts +11 -0
- package/build/AudioRecorder.provider.d.ts.map +1 -0
- package/build/AudioRecorder.provider.js +36 -0
- package/build/AudioRecorder.provider.js.map +1 -0
- package/build/ExpoAudioStream.native.d.ts +3 -0
- package/build/ExpoAudioStream.native.d.ts.map +1 -0
- package/build/ExpoAudioStream.native.js +6 -0
- package/build/ExpoAudioStream.native.js.map +1 -0
- package/build/ExpoAudioStream.types.d.ts +127 -0
- package/build/ExpoAudioStream.types.d.ts.map +1 -0
- package/build/ExpoAudioStream.types.js +2 -0
- package/build/ExpoAudioStream.types.js.map +1 -0
- package/build/ExpoAudioStream.web.d.ts +44 -0
- package/build/ExpoAudioStream.web.d.ts.map +1 -0
- package/build/ExpoAudioStream.web.js +206 -0
- package/build/ExpoAudioStream.web.js.map +1 -0
- package/build/ExpoAudioStreamModule.d.ts +3 -0
- package/build/ExpoAudioStreamModule.d.ts.map +1 -0
- package/build/ExpoAudioStreamModule.js +35 -0
- package/build/ExpoAudioStreamModule.js.map +1 -0
- package/build/WebRecorder.web.d.ts +54 -0
- package/build/WebRecorder.web.d.ts.map +1 -0
- package/build/WebRecorder.web.js +336 -0
- package/build/WebRecorder.web.js.map +1 -0
- package/build/constants.d.ts +11 -0
- package/build/constants.d.ts.map +1 -0
- package/build/constants.js +14 -0
- package/build/constants.js.map +1 -0
- package/build/events.d.ts +18 -0
- package/build/events.d.ts.map +1 -0
- package/build/events.js +11 -0
- package/build/events.js.map +1 -0
- package/build/index.d.ts +11 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js.map +1 -0
- package/build/useAudioRecorder.d.ts +20 -0
- package/build/useAudioRecorder.d.ts.map +1 -0
- package/build/useAudioRecorder.js +311 -0
- package/build/useAudioRecorder.js.map +1 -0
- package/build/utils/BlobFix.d.ts +9 -0
- package/build/utils/BlobFix.d.ts.map +1 -0
- package/build/utils/BlobFix.js +498 -0
- package/build/utils/BlobFix.js.map +1 -0
- package/build/utils/concatenateBuffers.d.ts +8 -0
- package/build/utils/concatenateBuffers.d.ts.map +1 -0
- package/build/utils/concatenateBuffers.js +21 -0
- package/build/utils/concatenateBuffers.js.map +1 -0
- package/build/utils/convertPCMToFloat32.d.ts +13 -0
- package/build/utils/convertPCMToFloat32.d.ts.map +1 -0
- package/build/utils/convertPCMToFloat32.js +120 -0
- package/build/utils/convertPCMToFloat32.js.map +1 -0
- package/build/utils/encodingToBitDepth.d.ts +5 -0
- package/build/utils/encodingToBitDepth.d.ts.map +1 -0
- package/build/utils/encodingToBitDepth.js +13 -0
- package/build/utils/encodingToBitDepth.js.map +1 -0
- package/build/utils/getWavFileInfo.d.ts +26 -0
- package/build/utils/getWavFileInfo.d.ts.map +1 -0
- package/build/utils/getWavFileInfo.js +92 -0
- package/build/utils/getWavFileInfo.js.map +1 -0
- package/build/utils/writeWavHeader.d.ts +49 -0
- package/build/utils/writeWavHeader.d.ts.map +1 -0
- package/build/utils/writeWavHeader.js +91 -0
- package/build/utils/writeWavHeader.js.map +1 -0
- package/build/workers/InlineFeaturesExtractor.web.d.ts +2 -0
- package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -0
- package/build/workers/InlineFeaturesExtractor.web.js +311 -0
- package/build/workers/InlineFeaturesExtractor.web.js.map +1 -0
- package/build/workers/inlineAudioWebWorker.web.d.ts +2 -0
- package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -0
- package/build/workers/inlineAudioWebWorker.web.js +251 -0
- package/build/workers/inlineAudioWebWorker.web.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/AudioStreamManager.swift +394 -184
- package/ios/ExpoAudioStreamModule.swift +2 -2
- package/package.json +8 -4
- package/plugin/build/index.d.ts +3 -0
- package/plugin/build/index.js +132 -0
- package/plugin/src/index.ts +176 -0
- package/plugin/tsconfig.json +10 -0
- package/plugin/tsconfig.tsbuildinfo +1 -0
|
@@ -35,7 +35,10 @@ class AudioStreamManager: NSObject {
|
|
|
35
35
|
internal var recordingFileURL: URL?
|
|
36
36
|
private var audioProcessor: AudioProcessor?
|
|
37
37
|
private var startTime: Date?
|
|
38
|
-
private var
|
|
38
|
+
private var totalPausedDuration: TimeInterval = 0 // Track total paused time
|
|
39
|
+
private var currentPauseStart: Date? // Track current pause start
|
|
40
|
+
private var isRecording = false
|
|
41
|
+
private var isPaused = false
|
|
39
42
|
|
|
40
43
|
// Wake lock related properties
|
|
41
44
|
private var wasIdleTimerDisabled: Bool = false // Track previous idle timer state
|
|
@@ -45,9 +48,6 @@ class AudioStreamManager: NSObject {
|
|
|
45
48
|
internal var lastEmittedSize: Int64 = 0
|
|
46
49
|
private var emissionInterval: TimeInterval = 1.0 // Default to 1 second
|
|
47
50
|
private var totalDataSize: Int64 = 0
|
|
48
|
-
private var isRecording = false
|
|
49
|
-
private var isPaused = false
|
|
50
|
-
private var pausedDuration = 0
|
|
51
51
|
private var fileManager = FileManager.default
|
|
52
52
|
internal var recordingSettings: RecordingSettings?
|
|
53
53
|
internal var recordingUUID: UUID?
|
|
@@ -66,6 +66,8 @@ class AudioStreamManager: NSObject {
|
|
|
66
66
|
|
|
67
67
|
weak var delegate: AudioStreamManagerDelegate? // Define the delegate here
|
|
68
68
|
|
|
69
|
+
private var lastValidDuration: TimeInterval? // Add this property
|
|
70
|
+
|
|
69
71
|
/// Initializes the AudioStreamManager
|
|
70
72
|
override init() {
|
|
71
73
|
super.init()
|
|
@@ -208,9 +210,22 @@ class AudioStreamManager: NSObject {
|
|
|
208
210
|
}
|
|
209
211
|
}
|
|
210
212
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
213
|
+
func currentRecordingDuration() -> TimeInterval {
|
|
214
|
+
// If we're paused, return the last valid duration
|
|
215
|
+
if isPaused, let lastDuration = lastValidDuration {
|
|
216
|
+
return lastDuration
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
guard let settings = recordingSettings else { return 0 }
|
|
220
|
+
|
|
221
|
+
// Normal duration calculation from data
|
|
222
|
+
let sampleRate = Double(settings.sampleRate)
|
|
223
|
+
let channels = Double(settings.numberOfChannels)
|
|
224
|
+
let bytesPerSample = Double(settings.bitDepth) / 8.0
|
|
225
|
+
|
|
226
|
+
let durationFromData = Double(totalDataSize) / (sampleRate * channels * bytesPerSample)
|
|
227
|
+
|
|
228
|
+
return durationFromData
|
|
214
229
|
}
|
|
215
230
|
|
|
216
231
|
private func cleanupNotificationObservers() {
|
|
@@ -261,7 +276,7 @@ class AudioStreamManager: NSObject {
|
|
|
261
276
|
// Calculate current duration
|
|
262
277
|
let currentDuration: TimeInterval
|
|
263
278
|
if let startTime = startTime {
|
|
264
|
-
currentDuration = Date().timeIntervalSince(startTime) - TimeInterval(
|
|
279
|
+
currentDuration = Date().timeIntervalSince(startTime) - TimeInterval(totalPausedDuration)
|
|
265
280
|
} else {
|
|
266
281
|
currentDuration = 0
|
|
267
282
|
}
|
|
@@ -282,7 +297,7 @@ class AudioStreamManager: NSObject {
|
|
|
282
297
|
private func updateMediaInfo() {
|
|
283
298
|
guard let startTime = startTime else { return }
|
|
284
299
|
|
|
285
|
-
let currentDuration = Date().timeIntervalSince(startTime) - TimeInterval(
|
|
300
|
+
let currentDuration = Date().timeIntervalSince(startTime) - TimeInterval(totalPausedDuration)
|
|
286
301
|
|
|
287
302
|
var nowPlayingInfo = notificationView?.nowPlayingInfo ?? [:]
|
|
288
303
|
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentDuration
|
|
@@ -372,20 +387,13 @@ class AudioStreamManager: NSObject {
|
|
|
372
387
|
/// Gets the current status of the recording.
|
|
373
388
|
/// - Returns: A dictionary containing the recording status information.
|
|
374
389
|
func getStatus() -> [String: Any] {
|
|
375
|
-
// let currentTime = Date()
|
|
376
|
-
// let totalRecordedTime = startTime != nil ? Int(currentTime.timeIntervalSince(startTime!)) - pausedDuration : 0
|
|
377
390
|
guard let settings = recordingSettings else {
|
|
378
391
|
print("Recording settings are not available.")
|
|
379
392
|
return [:]
|
|
380
393
|
}
|
|
381
394
|
|
|
382
|
-
let
|
|
383
|
-
let
|
|
384
|
-
let bitDepth = Double(settings.bitDepth)
|
|
385
|
-
|
|
386
|
-
// Calculate the duration in seconds
|
|
387
|
-
let durationInSeconds = Double(totalDataSize) / (sampleRate * channels * (bitDepth / 8))
|
|
388
|
-
let durationInMilliseconds = Int(durationInSeconds * 1000) - Int(pausedDuration * 1000)
|
|
395
|
+
let durationInSeconds = currentRecordingDuration()
|
|
396
|
+
let durationInMilliseconds = Int(durationInSeconds * 1000)
|
|
389
397
|
|
|
390
398
|
return [
|
|
391
399
|
"durationMs": durationInMilliseconds,
|
|
@@ -395,7 +403,6 @@ class AudioStreamManager: NSObject {
|
|
|
395
403
|
"size": totalDataSize,
|
|
396
404
|
"interval": emissionInterval
|
|
397
405
|
]
|
|
398
|
-
|
|
399
406
|
}
|
|
400
407
|
|
|
401
408
|
/// Starts a new audio recording with the specified settings and interval.
|
|
@@ -414,38 +421,32 @@ class AudioStreamManager: NSObject {
|
|
|
414
421
|
return nil
|
|
415
422
|
}
|
|
416
423
|
|
|
417
|
-
var newSettings = settings // Make settings mutable
|
|
418
424
|
let session = AVAudioSession.sharedInstance()
|
|
425
|
+
var newSettings = settings
|
|
419
426
|
|
|
420
|
-
//
|
|
421
|
-
let commonFormat: AVAudioCommonFormat
|
|
422
|
-
switch newSettings.bitDepth {
|
|
423
|
-
case 16:
|
|
424
|
-
commonFormat = .pcmFormatInt16
|
|
425
|
-
case 32:
|
|
426
|
-
commonFormat = .pcmFormatInt32
|
|
427
|
-
default:
|
|
428
|
-
Logger.debug("Unsupported bit depth. Defaulting to 16-bit PCM")
|
|
429
|
-
commonFormat = .pcmFormatInt16
|
|
430
|
-
newSettings.bitDepth = 16
|
|
431
|
-
}
|
|
432
|
-
|
|
427
|
+
// Add these initializations back
|
|
433
428
|
emissionInterval = max(100.0, Double(intervalMilliseconds)) / 1000.0
|
|
434
429
|
lastEmissionTime = Date()
|
|
435
430
|
accumulatedData.removeAll()
|
|
436
431
|
totalDataSize = 0
|
|
437
|
-
|
|
432
|
+
totalPausedDuration = 0
|
|
433
|
+
lastEmittedSize = 0
|
|
438
434
|
isPaused = false
|
|
439
435
|
|
|
436
|
+
// Create recording file first
|
|
437
|
+
recordingFileURL = createRecordingFile()
|
|
438
|
+
if recordingFileURL == nil {
|
|
439
|
+
Logger.debug("Error: Failed to create recording file.")
|
|
440
|
+
return nil
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Then set up audio session and tap
|
|
440
444
|
do {
|
|
441
|
-
Logger.debug("
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if hardwareFormat.sampleRate != newSettings.sampleRate {
|
|
447
|
-
Logger.debug("Debug: Preferred sample rate not supported. Falling back to hardware sample rate \(session.sampleRate).")
|
|
448
|
-
newSettings.sampleRate = session.sampleRate
|
|
445
|
+
Logger.debug("Configuring audio session with sample rate: \(settings.sampleRate) Hz")
|
|
446
|
+
|
|
447
|
+
if let currentRoute = session.currentRoute.outputs.first {
|
|
448
|
+
Logger.debug("Current audio output: \(currentRoute.portType)")
|
|
449
|
+
newSettings.sampleRate = settings.sampleRate // Keep original sample rate
|
|
449
450
|
}
|
|
450
451
|
|
|
451
452
|
// Configure audio session based on iOS settings if provided
|
|
@@ -479,19 +480,47 @@ class AudioStreamManager: NSObject {
|
|
|
479
480
|
}
|
|
480
481
|
|
|
481
482
|
try session.setPreferredSampleRate(settings.sampleRate)
|
|
482
|
-
try session.setPreferredIOBufferDuration(1024 / settings.sampleRate)
|
|
483
|
+
try session.setPreferredIOBufferDuration(1024 / Double(settings.sampleRate))
|
|
483
484
|
try session.setActive(true)
|
|
484
|
-
Logger.debug("
|
|
485
|
-
|
|
485
|
+
Logger.debug("Audio session activated successfully.")
|
|
486
486
|
|
|
487
487
|
let actualSampleRate = session.sampleRate
|
|
488
|
-
if actualSampleRate !=
|
|
489
|
-
Logger.debug("
|
|
490
|
-
|
|
488
|
+
if actualSampleRate != settings.sampleRate {
|
|
489
|
+
Logger.debug("Hardware using sample rate \(actualSampleRate)Hz, will resample to \(settings.sampleRate)Hz")
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
recordingSettings = newSettings // Keep original settings with desired sample rate
|
|
493
|
+
enableWakeLock()
|
|
494
|
+
|
|
495
|
+
// Create format matching hardware capabilities
|
|
496
|
+
guard let hardwareFormat = AVAudioFormat(
|
|
497
|
+
commonFormat: .pcmFormatFloat32,
|
|
498
|
+
sampleRate: actualSampleRate,
|
|
499
|
+
channels: AVAudioChannelCount(settings.numberOfChannels),
|
|
500
|
+
interleaved: true
|
|
501
|
+
) else {
|
|
502
|
+
Logger.debug("Failed to create hardware format")
|
|
503
|
+
return nil
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
Logger.debug("""
|
|
507
|
+
Audio format configuration:
|
|
508
|
+
- Hardware format: \(describeAudioFormat(hardwareFormat))
|
|
509
|
+
- Target format: \(describeCommonFormat(hardwareFormat.commonFormat)) at \(actualSampleRate)Hz
|
|
510
|
+
- Bit depth: \(settings.bitDepth)-bit
|
|
511
|
+
- Channels: \(settings.numberOfChannels)
|
|
512
|
+
""")
|
|
513
|
+
|
|
514
|
+
audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: hardwareFormat) { [weak self] (buffer, time) in
|
|
515
|
+
guard let self = self,
|
|
516
|
+
let fileURL = self.recordingFileURL else {
|
|
517
|
+
Logger.debug("Error: File URL or self is nil during buffer processing.")
|
|
518
|
+
return
|
|
519
|
+
}
|
|
520
|
+
self.processAudioBuffer(buffer, fileURL: fileURL)
|
|
521
|
+
self.lastBufferTime = time
|
|
491
522
|
}
|
|
492
523
|
|
|
493
|
-
recordingSettings = newSettings // Update the class property with the new settings
|
|
494
|
-
enableWakeLock() // Will only enable if keepAwake is true
|
|
495
524
|
} catch {
|
|
496
525
|
Logger.debug("Error: Failed to set up audio session with preferred settings: \(error.localizedDescription)")
|
|
497
526
|
return nil
|
|
@@ -499,9 +528,25 @@ class AudioStreamManager: NSObject {
|
|
|
499
528
|
|
|
500
529
|
NotificationCenter.default.addObserver(self, selector: #selector(handleAudioSessionInterruption), name: AVAudioSession.interruptionNotification, object: nil)
|
|
501
530
|
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
|
|
531
|
+
// Create audio format based on recording settings
|
|
532
|
+
let commonFormat: AVAudioCommonFormat
|
|
533
|
+
switch newSettings.bitDepth {
|
|
534
|
+
case 16:
|
|
535
|
+
commonFormat = .pcmFormatInt16
|
|
536
|
+
case 32:
|
|
537
|
+
commonFormat = .pcmFormatFloat32
|
|
538
|
+
default:
|
|
539
|
+
Logger.debug("Unsupported bit depth: \(newSettings.bitDepth), falling back to 16-bit")
|
|
540
|
+
commonFormat = .pcmFormatInt16
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
guard let audioFormat = AVAudioFormat(
|
|
544
|
+
commonFormat: commonFormat,
|
|
545
|
+
sampleRate: newSettings.sampleRate,
|
|
546
|
+
channels: UInt32(newSettings.numberOfChannels),
|
|
547
|
+
interleaved: true
|
|
548
|
+
) else {
|
|
549
|
+
Logger.debug("Error: Failed to create audio format with bit depth: \(newSettings.bitDepth)")
|
|
505
550
|
return nil
|
|
506
551
|
}
|
|
507
552
|
|
|
@@ -515,31 +560,16 @@ class AudioStreamManager: NSObject {
|
|
|
515
560
|
Logger.debug("AudioProcessor activated successfully.")
|
|
516
561
|
}
|
|
517
562
|
|
|
518
|
-
audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: audioFormat) { [weak self] (buffer, time) in
|
|
519
|
-
guard let self = self, let fileURL = self.recordingFileURL else {
|
|
520
|
-
Logger.debug("Error: File URL or self is nil during buffer processing.")
|
|
521
|
-
return
|
|
522
|
-
}
|
|
523
|
-
let formatDescription = describeAudioFormat(buffer.format)
|
|
524
|
-
Logger.debug("Debug: Buffer format - \(formatDescription)")
|
|
525
|
-
|
|
526
|
-
// Processing the current buffer
|
|
527
|
-
self.processAudioBuffer(buffer, fileURL: self.recordingFileURL!)
|
|
528
|
-
self.lastBufferTime = time
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
recordingFileURL = createRecordingFile()
|
|
532
|
-
if recordingFileURL == nil {
|
|
533
|
-
Logger.debug("Error: Failed to create recording file.")
|
|
534
|
-
return nil
|
|
535
|
-
}
|
|
536
|
-
|
|
537
563
|
if settings.showNotification {
|
|
538
564
|
initializeNotifications()
|
|
539
565
|
}
|
|
540
566
|
|
|
541
567
|
do {
|
|
542
568
|
startTime = Date()
|
|
569
|
+
totalPausedDuration = 0 // Reset pause tracking
|
|
570
|
+
currentPauseStart = nil
|
|
571
|
+
Logger.debug("Starting new recording - Reset pause tracking")
|
|
572
|
+
|
|
543
573
|
try audioEngine.start()
|
|
544
574
|
isRecording = true
|
|
545
575
|
isPaused = false
|
|
@@ -562,17 +592,18 @@ class AudioStreamManager: NSObject {
|
|
|
562
592
|
func pauseRecording() {
|
|
563
593
|
guard isRecording && !isPaused else { return }
|
|
564
594
|
|
|
595
|
+
// Store the current duration when pausing
|
|
596
|
+
lastValidDuration = currentRecordingDuration()
|
|
597
|
+
Logger.debug("Storing duration at pause: \(lastValidDuration ?? 0)")
|
|
598
|
+
|
|
565
599
|
disableWakeLock()
|
|
566
600
|
audioEngine.pause()
|
|
567
601
|
isPaused = true
|
|
568
|
-
pauseStartTime = Date()
|
|
569
602
|
|
|
570
603
|
updateNowPlayingInfo(isPaused: true)
|
|
571
604
|
notificationManager?.updateState(isPaused: true)
|
|
572
605
|
delegate?.audioStreamManager(self, didPauseRecording: Date())
|
|
573
606
|
delegate?.audioStreamManager(self, didUpdateNotificationState: true)
|
|
574
|
-
|
|
575
|
-
Logger.debug("Recording paused.")
|
|
576
607
|
}
|
|
577
608
|
|
|
578
609
|
private func initializeNotifications() {
|
|
@@ -627,21 +658,32 @@ class AudioStreamManager: NSObject {
|
|
|
627
658
|
func resumeRecording() {
|
|
628
659
|
guard isRecording && isPaused else { return }
|
|
629
660
|
|
|
661
|
+
lastValidDuration = nil // Clear the stored duration when resuming
|
|
662
|
+
|
|
630
663
|
enableWakeLock()
|
|
631
664
|
audioEngine.prepare()
|
|
632
665
|
do {
|
|
633
666
|
try audioEngine.start()
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
667
|
+
|
|
668
|
+
// Add the completed pause duration to total
|
|
669
|
+
if let pauseStart = currentPauseStart {
|
|
670
|
+
let currentPauseDuration = Date().timeIntervalSince(pauseStart)
|
|
671
|
+
totalPausedDuration += currentPauseDuration
|
|
672
|
+
currentPauseStart = nil
|
|
673
|
+
|
|
674
|
+
Logger.debug("""
|
|
675
|
+
Resume completed:
|
|
676
|
+
- Added pause duration: \(currentPauseDuration)
|
|
677
|
+
- New total pause duration: \(totalPausedDuration)
|
|
678
|
+
""")
|
|
637
679
|
}
|
|
638
680
|
|
|
681
|
+
isPaused = false
|
|
682
|
+
|
|
639
683
|
updateNowPlayingInfo(isPaused: false)
|
|
640
684
|
notificationManager?.updateState(isPaused: false)
|
|
641
685
|
delegate?.audioStreamManager(self, didResumeRecording: Date())
|
|
642
686
|
delegate?.audioStreamManager(self, didUpdateNotificationState: false)
|
|
643
|
-
|
|
644
|
-
Logger.debug("Recording resumed.")
|
|
645
687
|
} catch {
|
|
646
688
|
Logger.debug("Error: Failed to resume recording: \(error.localizedDescription)")
|
|
647
689
|
}
|
|
@@ -651,32 +693,40 @@ class AudioStreamManager: NSObject {
|
|
|
651
693
|
/// - Parameter format: The AVAudioFormat object to describe.
|
|
652
694
|
/// - Returns: A string description of the audio format.
|
|
653
695
|
func describeAudioFormat(_ format: AVAudioFormat) -> String {
|
|
654
|
-
let
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
696
|
+
let formatDescription = """
|
|
697
|
+
- Sample rate: \(format.sampleRate)Hz
|
|
698
|
+
- Channels: \(format.channelCount)
|
|
699
|
+
- Interleaved: \(format.isInterleaved)
|
|
700
|
+
- Common format: \(describeCommonFormat(format.commonFormat))
|
|
701
|
+
"""
|
|
702
|
+
return formatDescription
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
private func describeCommonFormat(_ format: AVAudioCommonFormat) -> String {
|
|
706
|
+
switch format {
|
|
663
707
|
case .pcmFormatFloat32:
|
|
664
|
-
|
|
708
|
+
return "32-bit float"
|
|
665
709
|
case .pcmFormatFloat64:
|
|
666
|
-
|
|
710
|
+
return "64-bit float"
|
|
711
|
+
case .pcmFormatInt16:
|
|
712
|
+
return "16-bit int"
|
|
713
|
+
case .pcmFormatInt32:
|
|
714
|
+
return "32-bit int"
|
|
667
715
|
default:
|
|
668
|
-
|
|
716
|
+
return "Unknown format"
|
|
669
717
|
}
|
|
670
|
-
|
|
671
|
-
return "Sample Rate: \(sampleRate), Channels: \(channelCount), Format: \(bitDepth)"
|
|
672
718
|
}
|
|
673
719
|
|
|
674
720
|
/// Stops the current audio recording.
|
|
675
721
|
/// - Returns: A RecordingResult object if the recording stopped successfully, or nil otherwise.
|
|
676
722
|
func stopRecording() -> RecordingResult? {
|
|
677
|
-
disableWakeLock()
|
|
723
|
+
disableWakeLock()
|
|
678
724
|
audioEngine.stop()
|
|
679
725
|
audioEngine.inputNode.removeTap(onBus: 0)
|
|
726
|
+
|
|
727
|
+
// Get the final duration before changing state
|
|
728
|
+
let finalDuration = currentRecordingDuration()
|
|
729
|
+
|
|
680
730
|
isRecording = false
|
|
681
731
|
isPaused = false
|
|
682
732
|
|
|
@@ -701,21 +751,14 @@ class AudioStreamManager: NSObject {
|
|
|
701
751
|
try? audioSession?.setActive(false)
|
|
702
752
|
}
|
|
703
753
|
|
|
704
|
-
guard let fileURL = recordingFileURL,
|
|
754
|
+
guard let fileURL = recordingFileURL,
|
|
755
|
+
let settings = recordingSettings else {
|
|
705
756
|
Logger.debug("Recording or file URL is nil.")
|
|
706
757
|
return nil
|
|
707
758
|
}
|
|
708
759
|
|
|
709
|
-
//
|
|
710
|
-
|
|
711
|
-
let currentTime = Date()
|
|
712
|
-
let recordingTime = currentTime.timeIntervalSince(startTime)
|
|
713
|
-
delegate?.audioStreamManager(self, didReceiveAudioData: accumulatedData, recordingTime: recordingTime, totalDataSize: totalDataSize)
|
|
714
|
-
accumulatedData.removeAll()
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
let endTime = Date()
|
|
718
|
-
let duration = Int64(endTime.timeIntervalSince(startTime) * 1000) - Int64(pausedDuration * 1000)
|
|
760
|
+
// Use the final duration we captured before state changes
|
|
761
|
+
let durationMs = Int64(finalDuration * 1000)
|
|
719
762
|
|
|
720
763
|
// Calculate the total size of audio data written to the file
|
|
721
764
|
let filePath = fileURL.path
|
|
@@ -736,14 +779,15 @@ class AudioStreamManager: NSObject {
|
|
|
736
779
|
fileUri: fileURL.absoluteString,
|
|
737
780
|
filename: fileURL.lastPathComponent,
|
|
738
781
|
mimeType: mimeType,
|
|
739
|
-
duration:
|
|
782
|
+
duration: durationMs,
|
|
740
783
|
size: fileSize,
|
|
741
784
|
channels: settings.numberOfChannels,
|
|
742
785
|
bitDepth: settings.bitDepth,
|
|
743
786
|
sampleRate: settings.sampleRate
|
|
744
787
|
)
|
|
745
|
-
recordingFileURL = nil
|
|
746
|
-
lastBufferTime = nil
|
|
788
|
+
recordingFileURL = nil
|
|
789
|
+
lastBufferTime = nil
|
|
790
|
+
lastValidDuration = nil
|
|
747
791
|
|
|
748
792
|
return result
|
|
749
793
|
} catch {
|
|
@@ -759,34 +803,131 @@ class AudioStreamManager: NSObject {
|
|
|
759
803
|
/// - targetSampleRate: The desired sample rate to resample to.
|
|
760
804
|
/// - Returns: A new audio buffer resampled to the target sample rate, or nil if resampling fails.
|
|
761
805
|
private func resampleAudioBuffer(_ buffer: AVAudioPCMBuffer, from originalSampleRate: Double, to targetSampleRate: Double) -> AVAudioPCMBuffer? {
|
|
762
|
-
guard let
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
// Calculate the number of frames in the target buffer
|
|
768
|
-
let targetFrameCount = Int(Double(sourceFrameCount) * targetSampleRate / originalSampleRate)
|
|
806
|
+
guard let settings = recordingSettings else {
|
|
807
|
+
Logger.debug("Recording settings not available")
|
|
808
|
+
return nil
|
|
809
|
+
}
|
|
769
810
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
811
|
+
Logger.debug("""
|
|
812
|
+
Starting resampling:
|
|
813
|
+
- Original format: \(describeAudioFormat(buffer.format))
|
|
814
|
+
- Original frames: \(buffer.frameLength)
|
|
815
|
+
- Target settings:
|
|
816
|
+
• Sample rate: \(targetSampleRate)Hz
|
|
817
|
+
• Bit depth: \(settings.bitDepth)
|
|
818
|
+
• Channels: \(settings.numberOfChannels)
|
|
819
|
+
""")
|
|
820
|
+
|
|
821
|
+
// Use settings bit depth for output format
|
|
822
|
+
let targetFormat: AVAudioCommonFormat = settings.bitDepth == 32 ? .pcmFormatFloat32 : .pcmFormatInt16
|
|
823
|
+
|
|
824
|
+
// Create output format matching recording settings exactly
|
|
825
|
+
guard let outputFormat = AVAudioFormat(
|
|
826
|
+
commonFormat: targetFormat,
|
|
827
|
+
sampleRate: targetSampleRate,
|
|
828
|
+
channels: AVAudioChannelCount(settings.numberOfChannels),
|
|
829
|
+
interleaved: true
|
|
830
|
+
) else {
|
|
831
|
+
Logger.debug("Failed to create output format")
|
|
832
|
+
return nil
|
|
833
|
+
}
|
|
773
834
|
|
|
774
|
-
|
|
835
|
+
// Calculate new buffer size
|
|
836
|
+
let ratio = targetSampleRate / originalSampleRate
|
|
837
|
+
let newFrameCount = AVAudioFrameCount(Double(buffer.frameLength) * ratio)
|
|
775
838
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
839
|
+
// Create output buffer
|
|
840
|
+
guard let outputBuffer = AVAudioPCMBuffer(
|
|
841
|
+
pcmFormat: outputFormat,
|
|
842
|
+
frameCapacity: newFrameCount
|
|
843
|
+
) else {
|
|
844
|
+
Logger.debug("Failed to create output buffer")
|
|
845
|
+
return nil
|
|
846
|
+
}
|
|
847
|
+
outputBuffer.frameLength = newFrameCount
|
|
848
|
+
|
|
849
|
+
// Create intermediate format for high-quality conversion if needed
|
|
850
|
+
let needsIntermediate = buffer.format.commonFormat != outputFormat.commonFormat
|
|
851
|
+
if needsIntermediate {
|
|
852
|
+
Logger.debug("Using intermediate Float32 format for high-quality conversion")
|
|
853
|
+
guard let intermediateFormat = AVAudioFormat(
|
|
854
|
+
commonFormat: .pcmFormatFloat32,
|
|
855
|
+
sampleRate: targetSampleRate,
|
|
856
|
+
channels: AVAudioChannelCount(settings.numberOfChannels),
|
|
857
|
+
interleaved: true
|
|
858
|
+
) else {
|
|
859
|
+
Logger.debug("Failed to create intermediate format")
|
|
860
|
+
return nil
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// First convert to intermediate float format
|
|
864
|
+
guard let converter = AVAudioConverter(from: buffer.format, to: intermediateFormat),
|
|
865
|
+
let intermediateBuffer = AVAudioPCMBuffer(
|
|
866
|
+
pcmFormat: intermediateFormat,
|
|
867
|
+
frameCapacity: newFrameCount
|
|
868
|
+
) else {
|
|
869
|
+
Logger.debug("Failed to create converter or intermediate buffer")
|
|
870
|
+
return nil
|
|
871
|
+
}
|
|
872
|
+
intermediateBuffer.frameLength = newFrameCount
|
|
779
873
|
|
|
780
|
-
var
|
|
874
|
+
var error: NSError?
|
|
875
|
+
let inputBlock: AVAudioConverterInputBlock = { inNumPackets, outStatus in
|
|
876
|
+
outStatus.pointee = AVAudioConverterInputStatus.haveData
|
|
877
|
+
return buffer
|
|
878
|
+
}
|
|
781
879
|
|
|
782
|
-
|
|
783
|
-
vDSP_vgenp(input.baseAddress!, vDSP_Stride(1), [Float](stride(from: 0, to: Float(sourceFrameCount), by: resamplingFactor)), vDSP_Stride(1), &y, vDSP_Stride(1), vDSP_Length(targetFrameCount), vDSP_Length(sourceFrameCount))
|
|
880
|
+
converter.convert(to: intermediateBuffer, error: &error, withInputFrom: inputBlock)
|
|
784
881
|
|
|
785
|
-
|
|
786
|
-
|
|
882
|
+
if let error = error {
|
|
883
|
+
Logger.debug("Intermediate conversion failed: \(error.localizedDescription)")
|
|
884
|
+
return nil
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Then convert to final format
|
|
888
|
+
guard let finalConverter = AVAudioConverter(from: intermediateFormat, to: outputFormat) else {
|
|
889
|
+
Logger.debug("Failed to create final converter")
|
|
890
|
+
return nil
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
finalConverter.convert(to: outputBuffer, error: &error) { inNumPackets, outStatus in
|
|
894
|
+
outStatus.pointee = AVAudioConverterInputStatus.haveData
|
|
895
|
+
return intermediateBuffer
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
if let error = error {
|
|
899
|
+
Logger.debug("Final conversion failed: \(error.localizedDescription)")
|
|
900
|
+
return nil
|
|
901
|
+
}
|
|
902
|
+
} else {
|
|
903
|
+
// Direct conversion if formats are compatible
|
|
904
|
+
guard let converter = AVAudioConverter(from: buffer.format, to: outputFormat) else {
|
|
905
|
+
Logger.debug("Failed to create converter")
|
|
906
|
+
return nil
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
var error: NSError?
|
|
910
|
+
let inputBlock: AVAudioConverterInputBlock = { inNumPackets, outStatus in
|
|
911
|
+
outStatus.pointee = AVAudioConverterInputStatus.haveData
|
|
912
|
+
return buffer
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
converter.convert(to: outputBuffer, error: &error, withInputFrom: inputBlock)
|
|
916
|
+
|
|
917
|
+
if let error = error {
|
|
918
|
+
Logger.debug("Conversion failed: \(error.localizedDescription)")
|
|
919
|
+
return nil
|
|
787
920
|
}
|
|
788
921
|
}
|
|
789
|
-
|
|
922
|
+
|
|
923
|
+
Logger.debug("""
|
|
924
|
+
Resampling completed:
|
|
925
|
+
- Final format: \(describeAudioFormat(outputBuffer.format))
|
|
926
|
+
- Final frames: \(outputBuffer.frameLength)
|
|
927
|
+
- Conversion path: \(needsIntermediate ? "With intermediate Float32" : "Direct")
|
|
928
|
+
""")
|
|
929
|
+
|
|
930
|
+
return outputBuffer
|
|
790
931
|
}
|
|
791
932
|
|
|
792
933
|
/// Manually resamples the audio buffer using linear interpolation.
|
|
@@ -868,7 +1009,7 @@ class AudioStreamManager: NSObject {
|
|
|
868
1009
|
guard let startTime = startTime,
|
|
869
1010
|
recordingSettings?.showNotification == true else { return }
|
|
870
1011
|
|
|
871
|
-
let currentDuration = Date().timeIntervalSince(startTime) - TimeInterval(
|
|
1012
|
+
let currentDuration = Date().timeIntervalSince(startTime) - TimeInterval(totalPausedDuration)
|
|
872
1013
|
|
|
873
1014
|
// Update both notification manager and media player
|
|
874
1015
|
notificationManager?.updateDuration(currentDuration)
|
|
@@ -885,25 +1026,50 @@ class AudioStreamManager: NSObject {
|
|
|
885
1026
|
/// - buffer: The audio buffer to process.
|
|
886
1027
|
/// - fileURL: The URL of the file to write the data to.
|
|
887
1028
|
private func processAudioBuffer(_ buffer: AVAudioPCMBuffer, fileURL: URL) {
|
|
1029
|
+
guard let settings = recordingSettings else {
|
|
1030
|
+
Logger.debug("Recording settings not available")
|
|
1031
|
+
return
|
|
1032
|
+
}
|
|
1033
|
+
|
|
888
1034
|
guard let fileHandle = try? FileHandle(forWritingTo: fileURL) else {
|
|
889
1035
|
Logger.debug("Failed to open file handle for URL: \(fileURL)")
|
|
890
1036
|
return
|
|
891
1037
|
}
|
|
1038
|
+
defer {
|
|
1039
|
+
fileHandle.closeFile() // Ensure file is always closed
|
|
1040
|
+
}
|
|
892
1041
|
|
|
893
|
-
let targetSampleRate =
|
|
894
|
-
let
|
|
1042
|
+
let targetSampleRate = Double(settings.sampleRate)
|
|
1043
|
+
let targetFormat: AVAudioCommonFormat = settings.bitDepth == 32 ? .pcmFormatFloat32 : .pcmFormatInt16
|
|
895
1044
|
|
|
1045
|
+
// First handle resampling if needed
|
|
1046
|
+
let resampledBuffer: AVAudioPCMBuffer
|
|
896
1047
|
if buffer.format.sampleRate != targetSampleRate {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
finalBuffer = resampledBuffer
|
|
1048
|
+
if let resampled = resampleAudioBuffer(buffer, from: buffer.format.sampleRate, to: targetSampleRate) {
|
|
1049
|
+
resampledBuffer = resampled
|
|
900
1050
|
} else {
|
|
901
|
-
Logger.debug("
|
|
902
|
-
|
|
1051
|
+
Logger.debug("Resampling failed")
|
|
1052
|
+
return
|
|
1053
|
+
}
|
|
1054
|
+
} else {
|
|
1055
|
+
resampledBuffer = buffer
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Then ensure format matches user settings
|
|
1059
|
+
let finalBuffer: AVAudioPCMBuffer
|
|
1060
|
+
if resampledBuffer.format.commonFormat != targetFormat {
|
|
1061
|
+
guard let converted = convertBufferFormat(resampledBuffer, to: AVAudioFormat(
|
|
1062
|
+
commonFormat: targetFormat,
|
|
1063
|
+
sampleRate: targetSampleRate,
|
|
1064
|
+
channels: AVAudioChannelCount(settings.numberOfChannels),
|
|
1065
|
+
interleaved: true
|
|
1066
|
+
)!) else {
|
|
1067
|
+
Logger.debug("Format conversion failed")
|
|
1068
|
+
return
|
|
903
1069
|
}
|
|
1070
|
+
finalBuffer = converted
|
|
904
1071
|
} else {
|
|
905
|
-
|
|
906
|
-
finalBuffer = buffer
|
|
1072
|
+
finalBuffer = resampledBuffer
|
|
907
1073
|
}
|
|
908
1074
|
|
|
909
1075
|
let audioData = finalBuffer.audioBufferList.pointee.mBuffers
|
|
@@ -911,74 +1077,118 @@ class AudioStreamManager: NSObject {
|
|
|
911
1077
|
Logger.debug("Buffer data is nil.")
|
|
912
1078
|
return
|
|
913
1079
|
}
|
|
1080
|
+
|
|
914
1081
|
var data = Data(bytes: bufferData, count: Int(audioData.mDataByteSize))
|
|
915
1082
|
|
|
916
|
-
// Check if this is the first buffer to process
|
|
1083
|
+
// Check if this is the first buffer to process
|
|
917
1084
|
if totalDataSize == 0 {
|
|
918
|
-
|
|
919
|
-
let header = createWavHeader(dataSize: 0) // Set initial dataSize to 0, update later
|
|
1085
|
+
let header = createWavHeader(dataSize: 0)
|
|
920
1086
|
data.insert(contentsOf: header, at: 0)
|
|
921
1087
|
}
|
|
922
1088
|
|
|
923
|
-
//
|
|
924
|
-
accumulatedData.append(data)
|
|
925
|
-
|
|
926
|
-
// print("Writing data size: \(data.count) bytes") // Debug: Check the size of data being written
|
|
1089
|
+
// Write to file
|
|
927
1090
|
fileHandle.seekToEndOfFile()
|
|
928
1091
|
fileHandle.write(data)
|
|
929
|
-
fileHandle.closeFile()
|
|
930
1092
|
|
|
1093
|
+
// Update total size and accumulated data
|
|
931
1094
|
totalDataSize += Int64(data.count)
|
|
932
|
-
|
|
1095
|
+
accumulatedData.append(data)
|
|
933
1096
|
|
|
1097
|
+
// Handle notifications if enabled
|
|
934
1098
|
if recordingSettings?.showNotification == true {
|
|
935
1099
|
updateNotificationDuration()
|
|
936
1100
|
}
|
|
937
1101
|
|
|
1102
|
+
// Emit data based on interval
|
|
938
1103
|
let currentTime = Date()
|
|
939
|
-
if let lastEmissionTime = lastEmissionTime,
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
} else {
|
|
969
|
-
Logger.debug("Processing failed or returned nil.")
|
|
970
|
-
}
|
|
1104
|
+
if let lastEmissionTime = lastEmissionTime,
|
|
1105
|
+
let startTime = startTime,
|
|
1106
|
+
currentTime.timeIntervalSince(lastEmissionTime) >= emissionInterval {
|
|
1107
|
+
|
|
1108
|
+
let recordingTime = currentTime.timeIntervalSince(startTime)
|
|
1109
|
+
let dataToProcess = accumulatedData
|
|
1110
|
+
|
|
1111
|
+
// Emit the audio data
|
|
1112
|
+
delegate?.audioStreamManager(self, didReceiveAudioData: dataToProcess, recordingTime: recordingTime, totalDataSize: totalDataSize)
|
|
1113
|
+
|
|
1114
|
+
// Process audio if enabled
|
|
1115
|
+
if settings.enableProcessing {
|
|
1116
|
+
DispatchQueue.global().async { [weak self] in
|
|
1117
|
+
guard let self = self else { return }
|
|
1118
|
+
if let processor = self.audioProcessor {
|
|
1119
|
+
Logger.debug("Processing audio buffer of size: \(dataToProcess.count)")
|
|
1120
|
+
let processingResult = processor.processAudioBuffer(
|
|
1121
|
+
data: dataToProcess,
|
|
1122
|
+
sampleRate: Float(settings.sampleRate),
|
|
1123
|
+
pointsPerSecond: settings.pointsPerSecond ?? 10,
|
|
1124
|
+
algorithm: settings.algorithm ?? "rms",
|
|
1125
|
+
featureOptions: settings.featureOptions ?? ["rms": true, "zcr": true],
|
|
1126
|
+
bitDepth: settings.bitDepth,
|
|
1127
|
+
numberOfChannels: settings.numberOfChannels
|
|
1128
|
+
)
|
|
1129
|
+
|
|
1130
|
+
DispatchQueue.main.async {
|
|
1131
|
+
if let result = processingResult {
|
|
1132
|
+
self.delegate?.audioStreamManager(self, didReceiveProcessingResult: result)
|
|
971
1133
|
}
|
|
972
1134
|
}
|
|
973
1135
|
}
|
|
974
1136
|
}
|
|
975
|
-
|
|
976
|
-
self.lastEmissionTime = currentTime // Update last emission time
|
|
977
|
-
self.lastEmittedSize = totalDataSize
|
|
978
|
-
accumulatedData.removeAll() // Reset accumulated data after emission
|
|
979
1137
|
}
|
|
1138
|
+
|
|
1139
|
+
// Update state after emission
|
|
1140
|
+
self.lastEmissionTime = currentTime
|
|
1141
|
+
self.lastEmittedSize = totalDataSize
|
|
1142
|
+
accumulatedData.removeAll()
|
|
980
1143
|
}
|
|
981
1144
|
}
|
|
1145
|
+
|
|
1146
|
+
// Add helper function to calculate average amplitude
|
|
1147
|
+
private func calculateAverageAmplitude(_ data: UnsafePointer<Float>, count: Int) -> Float {
|
|
1148
|
+
var sum: Float = 0
|
|
1149
|
+
vDSP_meanv(data, 1, &sum, vDSP_Length(count))
|
|
1150
|
+
return sum
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Add helper function to calculate RMS
|
|
1154
|
+
private func calculateRMS(_ data: UnsafePointer<Float>, count: Int) -> Float {
|
|
1155
|
+
var sum: Float = 0
|
|
1156
|
+
var squaredSum: Float = 0
|
|
1157
|
+
for i in 0..<count {
|
|
1158
|
+
let value = data[i]
|
|
1159
|
+
sum += value
|
|
1160
|
+
squaredSum += value * value
|
|
1161
|
+
}
|
|
1162
|
+
let average = sum / Float(count)
|
|
1163
|
+
let variance = squaredSum / Float(count) - average * average
|
|
1164
|
+
return sqrt(variance)
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Helper function for format conversion
|
|
1168
|
+
private func convertBufferFormat(_ buffer: AVAudioPCMBuffer, to targetFormat: AVAudioFormat) -> AVAudioPCMBuffer? {
|
|
1169
|
+
guard let converter = AVAudioConverter(from: buffer.format, to: targetFormat),
|
|
1170
|
+
let outputBuffer = AVAudioPCMBuffer(
|
|
1171
|
+
pcmFormat: targetFormat,
|
|
1172
|
+
frameCapacity: buffer.frameLength
|
|
1173
|
+
) else {
|
|
1174
|
+
return nil
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
outputBuffer.frameLength = buffer.frameLength
|
|
1178
|
+
var error: NSError?
|
|
1179
|
+
|
|
1180
|
+
converter.convert(to: outputBuffer, error: &error) { inNumPackets, outStatus in
|
|
1181
|
+
outStatus.pointee = AVAudioConverterInputStatus.haveData
|
|
1182
|
+
return buffer
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
if let error = error {
|
|
1186
|
+
Logger.debug("Format conversion failed: \(error.localizedDescription)")
|
|
1187
|
+
return nil
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
return outputBuffer
|
|
1191
|
+
}
|
|
982
1192
|
}
|
|
983
1193
|
|
|
984
1194
|
extension AudioStreamManager: UNUserNotificationCenterDelegate {
|