@siteed/expo-audio-stream 1.16.0 → 2.0.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +1 -1
  3. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +68 -22
  4. package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +24 -0
  5. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +836 -386
  6. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +134 -23
  7. package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +35 -29
  8. package/android/src/main/java/net/siteed/audiostream/Constants.kt +1 -0
  9. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +236 -96
  10. package/android/src/main/java/net/siteed/audiostream/FFT.kt +55 -0
  11. package/android/src/main/java/net/siteed/audiostream/Features.kt +49 -7
  12. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +4 -4
  13. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +55 -47
  14. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
  15. package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  16. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +60 -13
  17. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -1
  18. package/build/AudioAnalysis/extractAudioAnalysis.js +147 -162
  19. package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
  20. package/build/ExpoAudioStream.types.d.ts +49 -3
  21. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  22. package/build/ExpoAudioStream.types.js.map +1 -1
  23. package/build/ExpoAudioStream.web.d.ts +2 -0
  24. package/build/ExpoAudioStream.web.d.ts.map +1 -1
  25. package/build/ExpoAudioStream.web.js +8 -1
  26. package/build/ExpoAudioStream.web.js.map +1 -1
  27. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  28. package/build/ExpoAudioStreamModule.js +216 -12
  29. package/build/ExpoAudioStreamModule.js.map +1 -1
  30. package/build/WebRecorder.web.d.ts +67 -13
  31. package/build/WebRecorder.web.d.ts.map +1 -1
  32. package/build/WebRecorder.web.js +178 -173
  33. package/build/WebRecorder.web.js.map +1 -1
  34. package/build/index.d.ts +3 -3
  35. package/build/index.d.ts.map +1 -1
  36. package/build/index.js +2 -2
  37. package/build/index.js.map +1 -1
  38. package/build/useAudioRecorder.d.ts.map +1 -1
  39. package/build/useAudioRecorder.js +12 -8
  40. package/build/useAudioRecorder.js.map +1 -1
  41. package/build/utils/audioProcessing.d.ts +24 -0
  42. package/build/utils/audioProcessing.d.ts.map +1 -0
  43. package/build/utils/audioProcessing.js +133 -0
  44. package/build/utils/audioProcessing.js.map +1 -0
  45. package/build/workers/InlineFeaturesExtractor.web.d.ts +1 -1
  46. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
  47. package/build/workers/InlineFeaturesExtractor.web.js +692 -175
  48. package/build/workers/InlineFeaturesExtractor.web.js.map +1 -1
  49. package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
  50. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
  51. package/build/workers/inlineAudioWebWorker.web.js +3 -2
  52. package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
  53. package/ios/AudioAnalysisData.swift +51 -16
  54. package/ios/AudioProcessingHelpers.swift +710 -26
  55. package/ios/AudioProcessor.swift +334 -185
  56. package/ios/AudioStreamManager.swift +66 -22
  57. package/ios/DataPoint.swift +25 -12
  58. package/ios/DecodingConfig.swift +47 -0
  59. package/ios/ExpoAudioStreamModule.swift +189 -104
  60. package/ios/FFT.swift +62 -0
  61. package/ios/Features.swift +24 -3
  62. package/ios/RecordingSettings.swift +9 -7
  63. package/package.json +2 -1
  64. package/plugin/build/index.d.ts +2 -0
  65. package/plugin/build/index.js +10 -3
  66. package/plugin/src/index.ts +10 -1
  67. package/src/AudioAnalysis/AudioAnalysis.types.ts +68 -52
  68. package/src/AudioAnalysis/extractAudioAnalysis.ts +223 -219
  69. package/src/ExpoAudioStream.types.ts +57 -7
  70. package/src/ExpoAudioStream.web.ts +8 -1
  71. package/src/ExpoAudioStreamModule.ts +255 -10
  72. package/src/WebRecorder.web.ts +231 -243
  73. package/src/index.ts +5 -3
  74. package/src/useAudioRecorder.tsx +14 -10
  75. package/src/utils/audioProcessing.ts +205 -0
  76. package/src/workers/InlineFeaturesExtractor.web.tsx +692 -175
  77. package/src/workers/inlineAudioWebWorker.web.tsx +3 -2
@@ -44,17 +44,29 @@ class AudioStreamManager: NSObject {
44
44
  private var wasIdleTimerDisabled: Bool = false // Track previous idle timer state
45
45
  private var isWakeLockEnabled: Bool = false // Track current wake lock state
46
46
 
47
+
48
+ // Data emission for onAudioStream
47
49
  internal var lastEmissionTime: Date?
48
50
  internal var lastEmittedSize: Int64 = 0
49
51
  internal var lastEmittedCompressedSize: Int64 = 0
50
- private var emissionInterval: TimeInterval = 1.0 // Default to 1 second
51
52
  private var totalDataSize: Int64 = 0
53
+ private var lastBufferTime: AVAudioTime?
54
+ private var accumulatedData = Data()
55
+
56
+ // Data emission for onAudioAnalysis
57
+ internal var lastEmissionTimeAnalysis: Date?
58
+ internal var lastEmittedSizeAnalysis: Int64 = 0
59
+ internal var lastEmittedCompressedSizeAnalysis: Int64 = 0
60
+ private var totalDataSizeAnalysis: Int64 = 0
61
+ private var lastBufferTimeAnalysis: AVAudioTime?
62
+ private var accumulatedAnalysisData = Data()
63
+
64
+
65
+
52
66
  private var fileManager = FileManager.default
53
67
  internal var recordingSettings: RecordingSettings?
54
68
  internal var recordingUUID: UUID?
55
69
  internal var mimeType: String = "audio/wav"
56
- private var lastBufferTime: AVAudioTime?
57
- private var accumulatedData = Data()
58
70
  private var recentData = [Float]() // This property stores the recent audio data
59
71
  private var notificationUpdateTimer: Timer?
60
72
 
@@ -77,6 +89,10 @@ class AudioStreamManager: NSObject {
77
89
  // Add property to track auto-resume preference
78
90
  private var autoResumeAfterInterruption: Bool = false
79
91
 
92
+ // Add these properties
93
+ private var emissionInterval: TimeInterval = 1.0 // Default 1 second
94
+ private var emissionIntervalAnalysis: TimeInterval = 0.5 // Default 0.5 seconds
95
+
80
96
  /// Initializes the AudioStreamManager
81
97
  override init() {
82
98
  super.init()
@@ -537,7 +553,8 @@ class AudioStreamManager: NSObject {
537
553
  "isPaused": isPaused,
538
554
  "mimeType": mimeType,
539
555
  "size": totalDataSize,
540
- "interval": emissionInterval
556
+ "interval": settings.interval,
557
+ "intervalAnalysis": settings.intervalAnalysis
541
558
  ]
542
559
 
543
560
  // Add compression info if enabled
@@ -575,12 +592,11 @@ class AudioStreamManager: NSObject {
575
592
  audioSession.currentRoute.outputs.contains { $0.portType == .builtInReceiver }
576
593
  }
577
594
 
578
- /// Starts a new audio recording with the specified settings and interval.
595
+ /// Starts a new audio recording with the specified settings.
579
596
  /// - Parameters:
580
597
  /// - settings: The recording settings to use.
581
- /// - intervalMilliseconds: The interval in milliseconds for emitting audio data.
582
598
  /// - Returns: A StartRecordingResult object if recording starts successfully, or nil otherwise.
583
- func startRecording(settings: RecordingSettings, intervalMilliseconds: Int) -> StartRecordingResult? {
599
+ func startRecording(settings: RecordingSettings) -> StartRecordingResult? {
584
600
  // Check for active call using the new method
585
601
  if isPhoneCallActive() {
586
602
  Logger.debug("Cannot start recording during an active phone call")
@@ -619,14 +635,19 @@ class AudioStreamManager: NSObject {
619
635
  let session = AVAudioSession.sharedInstance()
620
636
  var newSettings = settings
621
637
 
622
- // Add these initializations back
623
- emissionInterval = max(100.0, Double(intervalMilliseconds)) / 1000.0
638
+ emissionInterval = max(100.0, Double(settings.interval ?? 1000)) / 1000.0
639
+ emissionIntervalAnalysis = max(100.0, Double(settings.intervalAnalysis ?? 500)) / 1000.0
624
640
  lastEmissionTime = Date()
641
+ lastEmissionTimeAnalysis = Date()
625
642
  accumulatedData.removeAll()
643
+ accumulatedAnalysisData.removeAll()
626
644
  totalDataSize = 0
645
+ totalDataSizeAnalysis = 0
627
646
  totalPausedDuration = 0
628
647
  lastEmittedSize = 0
648
+ lastEmittedCompressedSizeAnalysis = 0
629
649
  isPaused = false
650
+
630
651
 
631
652
  // Create recording file first
632
653
  recordingFileURL = createRecordingFile()
@@ -677,6 +698,12 @@ class AudioStreamManager: NSObject {
677
698
  - mode: \(mode)
678
699
  - options: \(options)
679
700
  - keepAwake: \(settings.keepAwake)
701
+ - emission interval: \(emissionInterval * 1000)ms
702
+ - analysis interval: \(emissionIntervalAnalysis * 1000)ms
703
+ - sample rate: \(settings.sampleRate)Hz
704
+ - channels: \(settings.numberOfChannels)
705
+ - bit depth: \(settings.bitDepth)-bit
706
+ - compression enabled: \(settings.enableCompressedOutput)
680
707
  """)
681
708
 
682
709
  try session.setPreferredSampleRate(settings.sampleRate)
@@ -860,7 +887,7 @@ class AudioStreamManager: NSObject {
860
887
  }
861
888
 
862
889
  /// Pauses the current audio recording.
863
- func pauseRecording() {
890
+ public func pauseRecording() {
864
891
  guard isRecording && !isPaused else { return }
865
892
 
866
893
  // Store the current duration when pausing
@@ -929,8 +956,8 @@ class AudioStreamManager: NSObject {
929
956
  }
930
957
 
931
958
  /// Resumes the current audio recording.
932
- func resumeRecording() {
933
- // Check for active call using the new method
959
+ public func resumeRecording() {
960
+ // Check for active phone call
934
961
  if isPhoneCallActive() {
935
962
  Logger.debug("Cannot resume recording during an active phone call")
936
963
  delegate?.audioStreamManager(self, didFailWithError: "Cannot resume recording during an active phone call")
@@ -987,7 +1014,7 @@ class AudioStreamManager: NSObject {
987
1014
  return formatDescription
988
1015
  }
989
1016
 
990
- private func describeCommonFormat(_ format: AVAudioCommonFormat) -> String {
1017
+ func describeCommonFormat(_ format: AVAudioCommonFormat) -> String {
991
1018
  switch format {
992
1019
  case .pcmFormatFloat32:
993
1020
  return "32-bit float"
@@ -1149,9 +1176,12 @@ class AudioStreamManager: NSObject {
1149
1176
  totalPausedDuration = 0
1150
1177
  currentPauseStart = nil
1151
1178
  lastEmissionTime = nil
1179
+ lastEmissionTimeAnalysis = nil
1152
1180
  lastEmittedSize = 0
1181
+ lastEmittedSizeAnalysis = 0
1153
1182
  lastEmittedCompressedSize = 0
1154
1183
  accumulatedData.removeAll()
1184
+ accumulatedAnalysisData.removeAll()
1155
1185
  recordingUUID = nil
1156
1186
 
1157
1187
  return result
@@ -1459,6 +1489,7 @@ class AudioStreamManager: NSObject {
1459
1489
  // Update total size and accumulated data
1460
1490
  totalDataSize += Int64(data.count)
1461
1491
  accumulatedData.append(data)
1492
+ accumulatedAnalysisData.append(data)
1462
1493
 
1463
1494
  // Handle notifications if enabled
1464
1495
  if recordingSettings?.showNotification == true {
@@ -1534,7 +1565,21 @@ class AudioStreamManager: NSObject {
1534
1565
  compressionInfo: compressionInfo
1535
1566
  )
1536
1567
 
1537
- // Process audio if enabled
1568
+ // Update state after emission
1569
+ self.lastEmissionTime = currentTime
1570
+ self.lastEmittedSize = totalDataSize
1571
+ accumulatedData.removeAll()
1572
+ }
1573
+
1574
+
1575
+ if let lastEmissionTimeAnalysis = lastEmissionTimeAnalysis,
1576
+ let startTime = startTime,
1577
+ currentTime.timeIntervalSince(lastEmissionTimeAnalysis) >= emissionIntervalAnalysis {
1578
+
1579
+ let recordingTime = currentTime.timeIntervalSince(startTime)
1580
+ let dataToProcess = accumulatedAnalysisData
1581
+
1582
+ // Process audio if enabled
1538
1583
  if settings.enableProcessing {
1539
1584
  DispatchQueue.global().async { [weak self] in
1540
1585
  guard let self = self else { return }
@@ -1543,9 +1588,8 @@ class AudioStreamManager: NSObject {
1543
1588
  let processingResult = processor.processAudioBuffer(
1544
1589
  data: dataToProcess,
1545
1590
  sampleRate: Float(settings.sampleRate),
1546
- pointsPerSecond: settings.pointsPerSecond ?? 10,
1547
- algorithm: settings.algorithm ?? "rms",
1548
- featureOptions: settings.featureOptions ?? ["rms": true, "zcr": true],
1591
+ segmentDurationMs: settings.segmentDurationMs,
1592
+ featureOptions: settings.featureOptions ?? [:],
1549
1593
  bitDepth: settings.bitDepth,
1550
1594
  numberOfChannels: settings.numberOfChannels
1551
1595
  )
@@ -1555,14 +1599,14 @@ class AudioStreamManager: NSObject {
1555
1599
  self.delegate?.audioStreamManager(self, didReceiveProcessingResult: result)
1556
1600
  }
1557
1601
  }
1602
+
1603
+ // Update state after emission
1604
+ self.lastEmissionTimeAnalysis = currentTime
1605
+ self.lastEmittedSizeAnalysis = totalDataSizeAnalysis
1606
+ accumulatedAnalysisData.removeAll()
1558
1607
  }
1559
1608
  }
1560
1609
  }
1561
-
1562
- // Update state after emission
1563
- self.lastEmissionTime = currentTime
1564
- self.lastEmittedSize = totalDataSize
1565
- accumulatedData.removeAll()
1566
1610
  }
1567
1611
  }
1568
1612
 
@@ -7,19 +7,31 @@
7
7
 
8
8
  import Foundation
9
9
 
10
+ public struct SpeechFeatures {
11
+ public var isActive: Bool
12
+ public var speakerId: Int?
13
+
14
+ func toDictionary() -> [String: Any] {
15
+ return [
16
+ "isActive": isActive,
17
+ "speakerId": speakerId as Any
18
+ ]
19
+ }
20
+ }
10
21
 
11
22
  public struct DataPoint {
12
23
  public var id: Int
13
24
  public var amplitude: Float
14
- public var activeSpeech: Bool?
15
- public var dB: Float?
16
- public var silent: Bool?
25
+ public var rms: Float
26
+ public var dB: Float
27
+ public var silent: Bool
17
28
  public var features: Features?
18
- public var startTime: Float?
19
- public var endTime: Float?
20
- public var startPosition: Int?
21
- public var endPosition: Int?
22
- public var speaker: Int?
29
+ public var speech: SpeechFeatures?
30
+ public let startTime: Float // in seconds
31
+ public let endTime: Float // in seconds
32
+ public let startPosition: Int // byte position in audio file
33
+ public let endPosition: Int // byte position in audio file
34
+ public let samples: Int // number of samples in segment
23
35
  }
24
36
 
25
37
  extension DataPoint {
@@ -27,15 +39,16 @@ extension DataPoint {
27
39
  return [
28
40
  "id": id,
29
41
  "amplitude": amplitude,
30
- "activeSpeech": activeSpeech ?? false,
31
- "dB": dB ?? 0,
32
- "silent": silent ?? false,
42
+ "rms": rms,
43
+ "dB": dB,
44
+ "silent": silent,
33
45
  "features": features?.toDictionary() ?? [:],
46
+ "speech": speech?.toDictionary() ?? [:],
34
47
  "startTime": startTime ?? 0,
35
48
  "endTime": endTime ?? 0,
36
49
  "startPosition": startPosition ?? 0,
37
50
  "endPosition": endPosition ?? 0,
38
- "speaker": speaker ?? 0
51
+ "samples": samples ?? 0
39
52
  ]
40
53
  }
41
54
  }
@@ -0,0 +1,47 @@
1
+ //
2
+ // DecodingConfig.swift
3
+ // Pods
4
+ //
5
+ // Created by Arthur Breton on 24/2/2025.
6
+ //
7
+
8
+ import AVFoundation
9
+
10
+ public struct DecodingConfig {
11
+ let targetSampleRate: Double?
12
+ let targetChannels: Int?
13
+ let targetBitDepth: Int?
14
+ let normalizeAudio: Bool
15
+
16
+ static func fromDictionary(_ dict: [String: Any]?) -> DecodingConfig {
17
+ guard let dict = dict else {
18
+ return DecodingConfig.default
19
+ }
20
+
21
+ return DecodingConfig(
22
+ targetSampleRate: dict["targetSampleRate"] as? Double,
23
+ targetChannels: dict["targetChannels"] as? Int,
24
+ targetBitDepth: dict["targetBitDepth"] as? Int,
25
+ normalizeAudio: dict["normalizeAudio"] as? Bool ?? false
26
+ )
27
+ }
28
+
29
+ static var `default`: DecodingConfig {
30
+ return DecodingConfig(
31
+ targetSampleRate: nil,
32
+ targetChannels: nil,
33
+ targetBitDepth: nil,
34
+ normalizeAudio: false
35
+ )
36
+ }
37
+
38
+ func toAudioFormat(baseFormat: AVAudioFormat) -> AVAudioFormat {
39
+ let sampleRate = targetSampleRate ?? baseFormat.sampleRate
40
+ let channels = targetChannels ?? Int(baseFormat.channelCount)
41
+
42
+ return AVAudioFormat(
43
+ standardFormatWithSampleRate: sampleRate,
44
+ channels: AVAudioChannelCount(channels)
45
+ )!
46
+ }
47
+ }