@siteed/expo-audio-stream 1.0.1 → 1.0.3

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 (142) hide show
  1. package/.size-limit.json +6 -0
  2. package/README.md +6 -6
  3. package/android/build.gradle +5 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +120 -0
  5. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +34 -4
  6. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +635 -0
  7. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +194 -79
  8. package/android/src/main/java/net/siteed/audiostream/Constants.kt +1 -0
  9. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +48 -2
  10. package/android/src/main/java/net/siteed/audiostream/FFT.kt +44 -0
  11. package/android/src/main/java/net/siteed/audiostream/Features.kt +56 -0
  12. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +12 -0
  13. package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +56 -0
  14. package/app.plugin.js +1 -1
  15. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +76 -0
  16. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -0
  17. package/build/AudioAnalysis/AudioAnalysis.types.js +3 -0
  18. package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -0
  19. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +4 -0
  20. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -0
  21. package/build/AudioAnalysis/extractAudioAnalysis.js +101 -0
  22. package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -0
  23. package/build/AudioAnalysis/extractWaveform.d.ts +8 -0
  24. package/build/AudioAnalysis/extractWaveform.d.ts.map +1 -0
  25. package/build/AudioAnalysis/extractWaveform.js +14 -0
  26. package/build/AudioAnalysis/extractWaveform.js.map +1 -0
  27. package/build/AudioRecorder.provider.d.ts +14 -1
  28. package/build/AudioRecorder.provider.d.ts.map +1 -1
  29. package/build/AudioRecorder.provider.js +18 -5
  30. package/build/AudioRecorder.provider.js.map +1 -1
  31. package/build/ExpoAudioStream.native.d.ts +3 -0
  32. package/build/ExpoAudioStream.native.d.ts.map +1 -0
  33. package/build/ExpoAudioStream.native.js +6 -0
  34. package/build/ExpoAudioStream.native.js.map +1 -0
  35. package/build/ExpoAudioStream.types.d.ts +35 -20
  36. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  37. package/build/ExpoAudioStream.types.js.map +1 -1
  38. package/build/ExpoAudioStream.web.d.ts +42 -0
  39. package/build/ExpoAudioStream.web.d.ts.map +1 -0
  40. package/build/ExpoAudioStream.web.js +185 -0
  41. package/build/ExpoAudioStream.web.js.map +1 -0
  42. package/build/ExpoAudioStreamModule.d.ts +2 -2
  43. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  44. package/build/ExpoAudioStreamModule.js +16 -3
  45. package/build/ExpoAudioStreamModule.js.map +1 -1
  46. package/build/WebRecorder.web.d.ts +51 -0
  47. package/build/WebRecorder.web.d.ts.map +1 -0
  48. package/build/WebRecorder.web.js +288 -0
  49. package/build/WebRecorder.web.js.map +1 -0
  50. package/build/constants.d.ts +11 -0
  51. package/build/constants.d.ts.map +1 -0
  52. package/build/constants.js +14 -0
  53. package/build/constants.js.map +1 -0
  54. package/build/events.d.ts +6 -0
  55. package/build/events.d.ts.map +1 -0
  56. package/build/events.js +15 -0
  57. package/build/events.js.map +1 -0
  58. package/build/index.d.ts +8 -7
  59. package/build/index.d.ts.map +1 -1
  60. package/build/index.js +7 -14
  61. package/build/index.js.map +1 -1
  62. package/build/logger.d.ts +9 -0
  63. package/build/logger.d.ts.map +1 -0
  64. package/build/logger.js +17 -0
  65. package/build/logger.js.map +1 -0
  66. package/build/useAudioRecorder.d.ts +37 -0
  67. package/build/useAudioRecorder.d.ts.map +1 -0
  68. package/build/useAudioRecorder.js +271 -0
  69. package/build/useAudioRecorder.js.map +1 -0
  70. package/build/utils/convertPCMToFloat32.d.ts +11 -0
  71. package/build/utils/convertPCMToFloat32.d.ts.map +1 -0
  72. package/build/utils/convertPCMToFloat32.js +41 -0
  73. package/build/utils/convertPCMToFloat32.js.map +1 -0
  74. package/build/utils/encodingToBitDepth.d.ts +5 -0
  75. package/build/utils/encodingToBitDepth.d.ts.map +1 -0
  76. package/build/utils/encodingToBitDepth.js +13 -0
  77. package/build/utils/encodingToBitDepth.js.map +1 -0
  78. package/build/utils/getWavFileInfo.d.ts +25 -0
  79. package/build/utils/getWavFileInfo.d.ts.map +1 -0
  80. package/build/utils/getWavFileInfo.js +89 -0
  81. package/build/utils/getWavFileInfo.js.map +1 -0
  82. package/build/utils/writeWavHeader.d.ts +9 -0
  83. package/build/utils/writeWavHeader.d.ts.map +1 -0
  84. package/build/utils/writeWavHeader.js +41 -0
  85. package/build/utils/writeWavHeader.js.map +1 -0
  86. package/build/workers/InlineFeaturesExtractor.web.d.ts +2 -0
  87. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -0
  88. package/build/workers/InlineFeaturesExtractor.web.js +303 -0
  89. package/build/workers/InlineFeaturesExtractor.web.js.map +1 -0
  90. package/build/workers/inlineAudioWebWorker.web.d.ts +2 -0
  91. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -0
  92. package/build/workers/inlineAudioWebWorker.web.js +243 -0
  93. package/build/workers/inlineAudioWebWorker.web.js.map +1 -0
  94. package/expo-module.config.json +13 -4
  95. package/ios/AudioAnalysisData.swift +39 -0
  96. package/ios/AudioProcessingHelpers.swift +59 -0
  97. package/ios/AudioProcessor.swift +317 -0
  98. package/ios/AudioStreamError.swift +7 -0
  99. package/ios/AudioStreamManager.swift +243 -54
  100. package/ios/AudioStreamManagerDelegate.swift +4 -0
  101. package/ios/DataPoint.swift +41 -0
  102. package/ios/ExpoAudioStreamModule.swift +198 -6
  103. package/ios/Features.swift +44 -0
  104. package/ios/RecordingResult.swift +19 -0
  105. package/ios/RecordingSettings.swift +13 -0
  106. package/ios/WaveformExtractor.swift +105 -0
  107. package/package.json +13 -12
  108. package/plugin/tsconfig.json +13 -8
  109. package/publish.sh +8 -0
  110. package/src/AudioAnalysis/AudioAnalysis.types.ts +85 -0
  111. package/src/AudioAnalysis/extractAudioAnalysis.ts +136 -0
  112. package/src/AudioAnalysis/extractWaveform.ts +25 -0
  113. package/src/AudioRecorder.provider.tsx +36 -8
  114. package/src/ExpoAudioStream.native.ts +6 -0
  115. package/src/ExpoAudioStream.types.ts +50 -25
  116. package/src/ExpoAudioStream.web.ts +229 -0
  117. package/src/ExpoAudioStreamModule.ts +22 -3
  118. package/src/WebRecorder.web.ts +416 -0
  119. package/src/constants.ts +18 -0
  120. package/src/events.ts +25 -0
  121. package/src/index.ts +14 -29
  122. package/src/logger.ts +26 -0
  123. package/src/useAudioRecorder.tsx +415 -0
  124. package/src/utils/convertPCMToFloat32.ts +48 -0
  125. package/src/utils/encodingToBitDepth.ts +18 -0
  126. package/src/utils/getWavFileInfo.ts +125 -0
  127. package/src/utils/writeWavHeader.ts +56 -0
  128. package/src/workers/InlineFeaturesExtractor.web.tsx +302 -0
  129. package/src/workers/inlineAudioWebWorker.web.tsx +242 -0
  130. package/build/ExpoAudioStreamModule.web.d.ts +0 -37
  131. package/build/ExpoAudioStreamModule.web.d.ts.map +0 -1
  132. package/build/ExpoAudioStreamModule.web.js +0 -156
  133. package/build/ExpoAudioStreamModule.web.js.map +0 -1
  134. package/build/useAudioRecording.d.ts +0 -23
  135. package/build/useAudioRecording.d.ts.map +0 -1
  136. package/build/useAudioRecording.js +0 -189
  137. package/build/useAudioRecording.js.map +0 -1
  138. package/docs/demo.gif +0 -0
  139. package/release-it.js +0 -18
  140. package/src/ExpoAudioStreamModule.web.ts +0 -181
  141. package/src/useAudioRecording.ts +0 -268
  142. package/yarn-error.log +0 -7793
@@ -7,12 +7,7 @@
7
7
 
8
8
  import Foundation
9
9
  import AVFoundation
10
-
11
- struct RecordingSettings {
12
- var sampleRate: Double
13
- var numberOfChannels: Int = 1
14
- var bitDepth: Int = 16
15
- }
10
+ import Accelerate
16
11
 
17
12
  // Helper to convert to little-endian byte array
18
13
  extension UInt32 {
@@ -29,42 +24,16 @@ extension UInt16 {
29
24
  }
30
25
  }
31
26
 
32
-
33
- struct RecordingResult {
34
- var fileUri: String
35
- var mimeType: String
36
- var duration: Int64
37
- var size: Int64
38
- var channels: Int
39
- var bitDepth: Int
40
- var sampleRate: Double
41
- }
42
-
43
- struct StartRecordingResult {
44
- var fileUri: String
45
- var mimeType: String
46
- var channels: Int
47
- var bitDepth: Int
48
- var sampleRate: Double
49
- }
50
-
51
- protocol AudioStreamManagerDelegate: AnyObject {
52
- func audioStreamManager(_ manager: AudioStreamManager, didReceiveAudioData data: Data, recordingTime: TimeInterval, totalDataSize: Int64)
53
- }
54
-
55
- enum AudioStreamError: Error {
56
- case audioSessionSetupFailed(String)
57
- case fileCreationFailed(URL)
58
- case audioProcessingError(String)
59
- }
60
-
61
27
  class AudioStreamManager: NSObject {
62
28
  private let audioEngine = AVAudioEngine()
63
29
  private var inputNode: AVAudioInputNode {
64
30
  return audioEngine.inputNode
65
31
  }
66
32
  internal var recordingFileURL: URL?
33
+ private var audioProcessor: AudioProcessor?
67
34
  private var startTime: Date?
35
+ private var pauseStartTime: Date?
36
+
68
37
  internal var lastEmissionTime: Date?
69
38
  internal var lastEmittedSize: Int64 = 0
70
39
  private var emissionInterval: TimeInterval = 1.0 // Default to 1 second
@@ -78,13 +47,17 @@ class AudioStreamManager: NSObject {
78
47
  internal var mimeType: String = "audio/wav"
79
48
  private var lastBufferTime: AVAudioTime?
80
49
  private var accumulatedData = Data()
50
+ private var recentData = [Float]() // This property stores the recent audio data
81
51
 
82
52
  weak var delegate: AudioStreamManagerDelegate? // Define the delegate here
83
53
 
54
+ /// Initializes the AudioStreamManager
84
55
  override init() {
85
56
  super.init()
86
57
  }
87
58
 
59
+ /// Handles audio session interruptions.
60
+ /// - Parameter notification: The notification object containing interruption information.
88
61
  @objc func handleAudioSessionInterruption(notification: Notification) {
89
62
  guard let info = notification.userInfo,
90
63
  let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
@@ -107,6 +80,8 @@ class AudioStreamManager: NSObject {
107
80
  }
108
81
  }
109
82
 
83
+ /// Creates a new recording file.
84
+ /// - Returns: The URL of the newly created recording file, or nil if creation failed.
110
85
  private func createRecordingFile() -> URL? {
111
86
  let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
112
87
  recordingUUID = UUID()
@@ -120,6 +95,9 @@ class AudioStreamManager: NSObject {
120
95
  return fileURL
121
96
  }
122
97
 
98
+ /// Creates a WAV header for the given data size.
99
+ /// - Parameter dataSize: The size of the audio data.
100
+ /// - Returns: A Data object containing the WAV header.
123
101
  private func createWavHeader(dataSize: Int) -> Data {
124
102
  var header = Data()
125
103
 
@@ -152,7 +130,8 @@ class AudioStreamManager: NSObject {
152
130
  return header
153
131
  }
154
132
 
155
-
133
+ /// Gets the current status of the recording.
134
+ /// - Returns: A dictionary containing the recording status information.
156
135
  func getStatus() -> [String: Any] {
157
136
  // let currentTime = Date()
158
137
  // let totalRecordedTime = startTime != nil ? Int(currentTime.timeIntervalSince(startTime!)) - pausedDuration : 0
@@ -167,10 +146,10 @@ class AudioStreamManager: NSObject {
167
146
 
168
147
  // Calculate the duration in seconds
169
148
  let durationInSeconds = Double(totalDataSize) / (sampleRate * channels * (bitDepth / 8))
170
- let durationInMilliseconds = Int(durationInSeconds * 1000)
171
-
149
+ let durationInMilliseconds = Int(durationInSeconds * 1000) - Int(pausedDuration * 1000)
150
+
172
151
  return [
173
- "duration": durationInMilliseconds,
152
+ "durationMs": durationInMilliseconds,
174
153
  "isRecording": isRecording,
175
154
  "isPaused": isPaused,
176
155
  "mimeType": mimeType,
@@ -180,7 +159,12 @@ class AudioStreamManager: NSObject {
180
159
 
181
160
  }
182
161
 
183
- func startRecording(settings: RecordingSettings, intervalMilliseconds: Int) -> StartRecordingResult? {
162
+ /// Starts a new audio recording with the specified settings and interval.
163
+ /// - Parameters:
164
+ /// - settings: The recording settings to use.
165
+ /// - intervalMilliseconds: The interval in milliseconds for emitting audio data.
166
+ /// - Returns: A StartRecordingResult object if recording starts successfully, or nil otherwise.
167
+ func startRecording(settings: RecordingSettings, intervalMilliseconds: Int) -> StartRecordingResult? {
184
168
  guard !isRecording else {
185
169
  Logger.debug("Debug: Recording is already in progress.")
186
170
  return nil
@@ -191,11 +175,11 @@ class AudioStreamManager: NSObject {
191
175
  return nil
192
176
  }
193
177
 
194
- recordingSettings = settings
178
+ var newSettings = settings // Make settings mutable
195
179
 
196
180
  // Determine the commonFormat based on bitDepth
197
181
  let commonFormat: AVAudioCommonFormat
198
- switch settings.bitDepth {
182
+ switch newSettings.bitDepth {
199
183
  case 16:
200
184
  commonFormat = .pcmFormatInt16
201
185
  case 32:
@@ -203,22 +187,45 @@ class AudioStreamManager: NSObject {
203
187
  default:
204
188
  Logger.debug("Unsupported bit depth. Defaulting to 16-bit PCM")
205
189
  commonFormat = .pcmFormatInt16
206
- recordingSettings?.bitDepth = 16
190
+ newSettings.bitDepth = 16
207
191
  }
208
192
 
209
193
  emissionInterval = max(100.0, Double(intervalMilliseconds)) / 1000.0
210
194
  lastEmissionTime = Date()
211
195
  accumulatedData.removeAll()
212
196
  totalDataSize = 0
197
+ pausedDuration = 0
198
+ isPaused = false
213
199
 
214
200
  let session = AVAudioSession.sharedInstance()
215
201
  do {
216
202
  Logger.debug("Debug: Configuring audio session with sample rate: \(settings.sampleRate) Hz")
203
+
204
+ // Create an audio format with the desired sample rate
205
+ let desiredFormat = AVAudioFormat(commonFormat: commonFormat, sampleRate: newSettings.sampleRate, channels: UInt32(newSettings.numberOfChannels), interleaved: true)
206
+
207
+ // Check if the input node supports the desired format
208
+ let inputNode = audioEngine.inputNode
209
+ let hardwareFormat = inputNode.inputFormat(forBus: 0)
210
+ if hardwareFormat.sampleRate != newSettings.sampleRate {
211
+ Logger.debug("Debug: Preferred sample rate not supported. Falling back to hardware sample rate \(session.sampleRate).")
212
+ newSettings.sampleRate = session.sampleRate
213
+ }
214
+
215
+ try session.setCategory(.playAndRecord)
216
+ try session.setMode(.default)
217
217
  try session.setPreferredSampleRate(settings.sampleRate)
218
218
  try session.setPreferredIOBufferDuration(1024 / settings.sampleRate)
219
- try session.setCategory(.playAndRecord)
220
219
  try session.setActive(true)
221
220
  Logger.debug("Debug: Audio session activated successfully.")
221
+
222
+ let actualSampleRate = session.sampleRate
223
+ if actualSampleRate != newSettings.sampleRate {
224
+ Logger.debug("Debug: Preferred sample rate not set. Falling back to hardware sample rate: \(actualSampleRate) Hz")
225
+ newSettings.sampleRate = actualSampleRate
226
+ }
227
+
228
+ recordingSettings = newSettings // Update the class property with the new settings
222
229
  } catch {
223
230
  Logger.debug("Error: Failed to set up audio session with preferred settings: \(error.localizedDescription)")
224
231
  return nil
@@ -227,11 +234,21 @@ class AudioStreamManager: NSObject {
227
234
  NotificationCenter.default.addObserver(self, selector: #selector(handleAudioSessionInterruption), name: AVAudioSession.interruptionNotification, object: nil)
228
235
 
229
236
  // Correct the format to use 16-bit integer (PCM)
230
- guard let audioFormat = AVAudioFormat(commonFormat: commonFormat, sampleRate: settings.sampleRate, channels: UInt32(settings.numberOfChannels), interleaved: true) else {
237
+ guard let audioFormat = AVAudioFormat(commonFormat: commonFormat, sampleRate: newSettings.sampleRate, channels: UInt32(newSettings.numberOfChannels), interleaved: true) else {
231
238
  Logger.debug("Error: Failed to create audio format with the specified bit depth.")
232
239
  return nil
233
240
  }
234
241
 
242
+ if newSettings.enableProcessing == true {
243
+ // Initialize the AudioProcessor for buffer-based processing
244
+ self.audioProcessor = AudioProcessor(resolve: { result in
245
+ // Handle the result here if needed
246
+ }, reject: { code, message in
247
+ // Handle the rejection here if needed
248
+ })
249
+ Logger.debug("AudioProcessor activated successfully.")
250
+ }
251
+
235
252
  audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: audioFormat) { [weak self] (buffer, time) in
236
253
  guard let self = self, let fileURL = self.recordingFileURL else {
237
254
  Logger.debug("Error: File URL or self is nil during buffer processing.")
@@ -270,6 +287,43 @@ class AudioStreamManager: NSObject {
270
287
  }
271
288
  }
272
289
 
290
+ /// Pauses the current audio recording.
291
+ func pauseRecording() {
292
+ guard isRecording && !isPaused else {
293
+ Logger.debug("Recording is not in progress or already paused.")
294
+ return
295
+ }
296
+
297
+ audioEngine.pause()
298
+ isPaused = true
299
+ pauseStartTime = Date()
300
+
301
+ Logger.debug("Recording paused.")
302
+ }
303
+
304
+ /// Resumes the current audio recording.
305
+ func resumeRecording() {
306
+ guard isRecording && isPaused else {
307
+ Logger.debug("Recording is not in progress or not paused.")
308
+ return
309
+ }
310
+
311
+ audioEngine.prepare()
312
+ do {
313
+ try audioEngine.start()
314
+ isPaused = false
315
+ if let pauseStartTime = pauseStartTime {
316
+ pausedDuration += Int(Date().timeIntervalSince(pauseStartTime))
317
+ }
318
+ Logger.debug("Recording resumed.")
319
+ } catch {
320
+ Logger.debug("Error: Failed to resume recording: \(error.localizedDescription)")
321
+ }
322
+ }
323
+
324
+ /// Describes the format of the given audio format.
325
+ /// - Parameter format: The AVAudioFormat object to describe.
326
+ /// - Returns: A string description of the audio format.
273
327
  func describeAudioFormat(_ format: AVAudioFormat) -> String {
274
328
  let sampleRate = format.sampleRate
275
329
  let channelCount = format.channelCount
@@ -291,13 +345,15 @@ class AudioStreamManager: NSObject {
291
345
  return "Sample Rate: \(sampleRate), Channels: \(channelCount), Format: \(bitDepth)"
292
346
  }
293
347
 
348
+ /// Stops the current audio recording.
349
+ /// - Returns: A RecordingResult object if the recording stopped successfully, or nil otherwise.
294
350
  func stopRecording() -> RecordingResult? {
295
351
  audioEngine.stop()
296
352
  audioEngine.inputNode.removeTap(onBus: 0)
297
353
  isRecording = false
298
354
 
299
355
  guard let fileURL = recordingFileURL, let startTime = startTime, let settings = recordingSettings else {
300
- print("Recording or file URL is nil.")
356
+ Logger.debug("Recording or file URL is nil.")
301
357
  return nil
302
358
  }
303
359
 
@@ -335,11 +391,93 @@ class AudioStreamManager: NSObject {
335
391
 
336
392
  return result
337
393
  } catch {
338
- print("Failed to fetch file attributes: \(error)")
394
+ Logger.debug("Failed to fetch file attributes: \(error)")
339
395
  return nil
340
396
  }
341
397
  }
342
398
 
399
+ /// Resamples the audio buffer using vDSP. If it fails, falls back to manual resampling.
400
+ /// - Parameters:
401
+ /// - buffer: The original audio buffer to be resampled.
402
+ /// - originalSampleRate: The sample rate of the original audio buffer.
403
+ /// - targetSampleRate: The desired sample rate to resample to.
404
+ /// - Returns: A new audio buffer resampled to the target sample rate, or nil if resampling fails.
405
+ private func resampleAudioBuffer(_ buffer: AVAudioPCMBuffer, from originalSampleRate: Double, to targetSampleRate: Double) -> AVAudioPCMBuffer? {
406
+ guard let channelData = buffer.floatChannelData else { return nil }
407
+
408
+ let sourceFrameCount = Int(buffer.frameLength)
409
+ let sourceChannels = Int(buffer.format.channelCount)
410
+
411
+ // Calculate the number of frames in the target buffer
412
+ let targetFrameCount = Int(Double(sourceFrameCount) * targetSampleRate / originalSampleRate)
413
+
414
+ // Create a new audio buffer for the resampled data
415
+ guard let targetBuffer = AVAudioPCMBuffer(pcmFormat: buffer.format, frameCapacity: AVAudioFrameCount(targetFrameCount)) else { return nil }
416
+ targetBuffer.frameLength = AVAudioFrameCount(targetFrameCount)
417
+
418
+ let resamplingFactor = Float(targetSampleRate / originalSampleRate) // Factor to resample the audio
419
+
420
+ for channel in 0..<sourceChannels {
421
+ let input = UnsafeBufferPointer(start: channelData[channel], count: sourceFrameCount) // Original channel data
422
+ let output = UnsafeMutableBufferPointer(start: targetBuffer.floatChannelData![channel], count: targetFrameCount) // Buffer for resampled data
423
+
424
+ var y: [Float] = Array(repeating: 0, count: targetFrameCount) // Temporary array for resampled data
425
+
426
+ // Resample using vDSP_vgenp which performs interpolation
427
+ 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))
428
+
429
+ for i in 0..<targetFrameCount {
430
+ output[i] = y[i]
431
+ }
432
+ }
433
+ return targetBuffer
434
+ }
435
+
436
+ /// Manually resamples the audio buffer using linear interpolation.
437
+ /// - Parameters:
438
+ /// - buffer: The original audio buffer to be resampled.
439
+ /// - originalSampleRate: The sample rate of the original audio buffer.
440
+ /// - targetSampleRate: The desired sample rate to resample to.
441
+ /// - Returns: A new audio buffer resampled to the target sample rate, or nil if resampling fails.
442
+ private func manualResampleAudioBuffer(_ buffer: AVAudioPCMBuffer, from originalSampleRate: Double, to targetSampleRate: Double) -> AVAudioPCMBuffer? {
443
+ guard let channelData = buffer.floatChannelData else { return nil }
444
+
445
+ let sourceFrameCount = Int(buffer.frameLength)
446
+ let sourceChannels = Int(buffer.format.channelCount)
447
+ let targetFrameCount = Int(Double(sourceFrameCount) * targetSampleRate / originalSampleRate)
448
+
449
+ guard let targetBuffer = AVAudioPCMBuffer(pcmFormat: buffer.format, frameCapacity: AVAudioFrameCount(targetFrameCount)) else { return nil }
450
+ targetBuffer.frameLength = AVAudioFrameCount(targetFrameCount)
451
+
452
+ let resamplingFactor = Float(targetSampleRate / originalSampleRate)
453
+
454
+ for channel in 0..<sourceChannels {
455
+ let input = UnsafeBufferPointer(start: channelData[channel], count: sourceFrameCount)
456
+ let output = UnsafeMutableBufferPointer(start: targetBuffer.floatChannelData![channel], count: targetFrameCount)
457
+
458
+ var y = Array(repeating: Float(0), count: targetFrameCount)
459
+ for i in 0..<targetFrameCount {
460
+ let index = Float(i) / resamplingFactor
461
+ let low = Int(floor(index))
462
+ let high = min(low + 1, sourceFrameCount - 1)
463
+ let weight = index - Float(low)
464
+ y[i] = (1 - weight) * input[low] + weight * input[high]
465
+ }
466
+
467
+ for i in 0..<targetFrameCount {
468
+ output[i] = y[i]
469
+ }
470
+ }
471
+
472
+ return targetBuffer
473
+ }
474
+
475
+
476
+
477
+ /// Updates the WAV header with the correct file size.
478
+ /// - Parameters:
479
+ /// - fileURL: The URL of the WAV file.
480
+ /// - totalDataSize: The total size of the audio data.
343
481
  private func updateWavHeader(fileURL: URL, totalDataSize: Int64) {
344
482
  do {
345
483
  let fileHandle = try FileHandle(forUpdating: fileURL)
@@ -360,19 +498,39 @@ class AudioStreamManager: NSObject {
360
498
  fileHandle.write(Data(dataSizeBytes))
361
499
 
362
500
  } catch let error {
363
- print("Error updating WAV header: \(error)")
501
+ Logger.debug("Error updating WAV header: \(error)")
364
502
  }
365
503
  }
366
504
 
505
+ /// Processes the audio buffer and writes data to the file. Also handles audio processing if enabled.
506
+ /// - Parameters:
507
+ /// - buffer: The audio buffer to process.
508
+ /// - fileURL: The URL of the file to write the data to.
367
509
  private func processAudioBuffer(_ buffer: AVAudioPCMBuffer, fileURL: URL) {
368
510
  guard let fileHandle = try? FileHandle(forWritingTo: fileURL) else {
369
- print("Failed to open file handle for URL: \(fileURL)")
511
+ Logger.debug("Failed to open file handle for URL: \(fileURL)")
370
512
  return
371
513
  }
372
514
 
373
- let audioData = buffer.audioBufferList.pointee.mBuffers
515
+ let targetSampleRate = recordingSettings?.desiredSampleRate ?? buffer.format.sampleRate
516
+ let finalBuffer: AVAudioPCMBuffer
517
+
518
+ if buffer.format.sampleRate != targetSampleRate {
519
+ // Resample the audio buffer if the target sample rate is different from the input sample rate
520
+ if let resampledBuffer = resampleAudioBuffer(buffer, from: buffer.format.sampleRate, to: targetSampleRate) {
521
+ finalBuffer = resampledBuffer
522
+ } else {
523
+ Logger.debug("Failed to resample audio buffer. Using original buffer.")
524
+ finalBuffer = buffer
525
+ }
526
+ } else {
527
+ // Use the original buffer if the sample rates are the same
528
+ finalBuffer = buffer
529
+ }
530
+
531
+ let audioData = finalBuffer.audioBufferList.pointee.mBuffers
374
532
  guard let bufferData = audioData.mData else {
375
- print("Buffer data is nil.")
533
+ Logger.debug("Buffer data is nil.")
376
534
  return
377
535
  }
378
536
  var data = Data(bytes: bufferData, count: Int(audioData.mDataByteSize))
@@ -399,13 +557,44 @@ class AudioStreamManager: NSObject {
399
557
  if let lastEmissionTime = lastEmissionTime, currentTime.timeIntervalSince(lastEmissionTime) >= emissionInterval {
400
558
  if let startTime = startTime {
401
559
  let recordingTime = currentTime.timeIntervalSince(startTime)
402
- // print("Emitting data: Recording time \(recordingTime) seconds, Data size \(totalDataSize) bytes")
403
- self.delegate?.audioStreamManager(self, didReceiveAudioData: accumulatedData, recordingTime: recordingTime, totalDataSize: totalDataSize)
560
+ // Copy accumulated data for processing
561
+ let dataToProcess = accumulatedData
562
+
563
+ // Emit the processed audio data
564
+ self.delegate?.audioStreamManager(self, didReceiveAudioData: dataToProcess, recordingTime: recordingTime, totalDataSize: totalDataSize)
565
+
566
+ if recordingSettings?.enableProcessing == true {
567
+ // Process the copied data and emit result
568
+ DispatchQueue.global().async {
569
+ if let processor = self.audioProcessor, let settings = self.recordingSettings {
570
+ Logger.debug("processAudioBuffer with dataToProcess size --> \(dataToProcess.count)")
571
+
572
+ let processingResult = processor.processAudioBuffer(
573
+ data: dataToProcess,
574
+ sampleRate: Float(settings.sampleRate),
575
+ pointsPerSecond: settings.pointsPerSecond ?? 10,
576
+ algorithm: settings.algorithm ?? "rms",
577
+ featureOptions: settings.featureOptions ?? ["rms": true, "zcr": true],
578
+ bitDepth: settings.bitDepth,
579
+ numberOfChannels: settings.numberOfChannels
580
+ )
581
+ Logger.debug("processingResult \(String(describing: processingResult))")
582
+
583
+ DispatchQueue.main.async {
584
+ if let result = processingResult {
585
+ self.delegate?.audioStreamManager(self, didReceiveProcessingResult: result)
586
+ } else {
587
+ Logger.debug("Processing failed or returned nil.")
588
+ }
589
+ }
590
+ }
591
+ }
592
+ }
593
+
404
594
  self.lastEmissionTime = currentTime // Update last emission time
405
595
  self.lastEmittedSize = totalDataSize
406
596
  accumulatedData.removeAll() // Reset accumulated data after emission
407
597
  }
408
598
  }
409
599
  }
410
-
411
600
  }
@@ -0,0 +1,4 @@
1
+ protocol AudioStreamManagerDelegate: AnyObject {
2
+ func audioStreamManager(_ manager: AudioStreamManager, didReceiveAudioData data: Data, recordingTime: TimeInterval, totalDataSize: Int64)
3
+ func audioStreamManager(_ manager: AudioStreamManager, didReceiveProcessingResult result: AudioAnalysisData?)
4
+ }
@@ -0,0 +1,41 @@
1
+ //
2
+ // DataPoint.swift
3
+ // ExpoAudioStream
4
+ //
5
+ // Created by Arthur Breton on 23/6/2024.
6
+ //
7
+
8
+ import Foundation
9
+
10
+
11
+ public struct DataPoint {
12
+ public var id: Int
13
+ public var amplitude: Float
14
+ public var activeSpeech: Bool?
15
+ public var dB: Float?
16
+ public var silent: Bool?
17
+ 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?
23
+ }
24
+
25
+ extension DataPoint {
26
+ func toDictionary() -> [String: Any] {
27
+ return [
28
+ "id": id,
29
+ "amplitude": amplitude,
30
+ "activeSpeech": activeSpeech ?? false,
31
+ "dB": dB ?? 0,
32
+ "silent": silent ?? false,
33
+ "features": features?.toDictionary() ?? [:],
34
+ "startTime": startTime ?? 0,
35
+ "endTime": endTime ?? 0,
36
+ "startPosition": startPosition ?? 0,
37
+ "endPosition": endPosition ?? 0,
38
+ "speaker": speaker ?? 0
39
+ ]
40
+ }
41
+ }