@siteed/expo-audio-stream 1.0.0 → 1.0.2
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/README.md +7 -18
- package/android/build.gradle +5 -0
- package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +120 -0
- package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +34 -4
- package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +635 -0
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +194 -79
- package/android/src/main/java/net/siteed/audiostream/Constants.kt +1 -0
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +48 -2
- package/android/src/main/java/net/siteed/audiostream/FFT.kt +44 -0
- package/android/src/main/java/net/siteed/audiostream/Features.kt +56 -0
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +12 -0
- package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +56 -0
- package/app.plugin.js +1 -1
- package/build/AudioRecorder.provider.js +1 -1
- package/build/AudioRecorder.provider.js.map +1 -1
- 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 +79 -6
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/ExpoAudioStream.web.d.ts +41 -0
- package/build/ExpoAudioStream.web.d.ts.map +1 -0
- package/build/ExpoAudioStream.web.js +184 -0
- package/build/ExpoAudioStream.web.js.map +1 -0
- package/build/ExpoAudioStreamModule.d.ts +2 -2
- package/build/ExpoAudioStreamModule.d.ts.map +1 -1
- package/build/ExpoAudioStreamModule.js +12 -3
- package/build/ExpoAudioStreamModule.js.map +1 -1
- package/build/WebRecorder.d.ts +47 -0
- package/build/WebRecorder.d.ts.map +1 -0
- package/build/WebRecorder.js +243 -0
- package/build/WebRecorder.js.map +1 -0
- package/build/index.d.ts +14 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +106 -7
- package/build/index.js.map +1 -1
- package/build/inlineAudioWebWorker.d.ts +3 -0
- package/build/inlineAudioWebWorker.d.ts.map +1 -0
- package/build/inlineAudioWebWorker.js +340 -0
- package/build/inlineAudioWebWorker.js.map +1 -0
- package/build/useAudioRecording.d.ts +24 -9
- package/build/useAudioRecording.d.ts.map +1 -1
- package/build/useAudioRecording.js +107 -29
- package/build/useAudioRecording.js.map +1 -1
- package/build/utils.d.ts +31 -0
- package/build/utils.d.ts.map +1 -0
- package/build/utils.js +143 -0
- package/build/utils.js.map +1 -0
- package/expo-module.config.json +13 -4
- package/ios/AudioAnalysisData.swift +39 -0
- package/ios/AudioProcessingHelpers.swift +59 -0
- package/ios/AudioProcessor.swift +317 -0
- package/ios/AudioStreamError.swift +7 -0
- package/ios/AudioStreamManager.swift +204 -52
- package/ios/AudioStreamManagerDelegate.swift +4 -0
- package/ios/DataPoint.swift +41 -0
- package/ios/ExpoAudioStreamModule.swift +188 -6
- package/ios/Features.swift +44 -0
- package/ios/RecordingResult.swift +19 -0
- package/ios/RecordingSettings.swift +13 -0
- package/ios/WaveformExtractor.swift +105 -0
- package/package.json +9 -9
- package/plugin/tsconfig.json +13 -8
- package/publish.sh +8 -0
- package/src/AudioRecorder.provider.tsx +1 -1
- package/src/ExpoAudioStream.native.ts +6 -0
- package/src/ExpoAudioStream.types.ts +97 -11
- package/src/ExpoAudioStream.web.ts +228 -0
- package/src/ExpoAudioStreamModule.ts +17 -3
- package/src/WebRecorder.ts +364 -0
- package/src/index.ts +166 -20
- package/src/inlineAudioWebWorker.tsx +340 -0
- package/src/useAudioRecording.tsx +410 -0
- package/src/utils.ts +189 -0
- package/build/ExpoAudioStreamModule.web.d.ts +0 -37
- package/build/ExpoAudioStreamModule.web.d.ts.map +0 -1
- package/build/ExpoAudioStreamModule.web.js +0 -156
- package/build/ExpoAudioStreamModule.web.js.map +0 -1
- package/docs/demo.gif +0 -0
- package/release-it.js +0 -18
- package/src/ExpoAudioStreamModule.web.ts +0 -181
- package/src/useAudioRecording.ts +0 -268
- 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,41 +24,13 @@ 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?
|
|
68
35
|
internal var lastEmissionTime: Date?
|
|
69
36
|
internal var lastEmittedSize: Int64 = 0
|
|
@@ -78,13 +45,17 @@ class AudioStreamManager: NSObject {
|
|
|
78
45
|
internal var mimeType: String = "audio/wav"
|
|
79
46
|
private var lastBufferTime: AVAudioTime?
|
|
80
47
|
private var accumulatedData = Data()
|
|
48
|
+
private var recentData = [Float]() // This property stores the recent audio data
|
|
81
49
|
|
|
82
50
|
weak var delegate: AudioStreamManagerDelegate? // Define the delegate here
|
|
83
51
|
|
|
52
|
+
/// Initializes the AudioStreamManager
|
|
84
53
|
override init() {
|
|
85
54
|
super.init()
|
|
86
55
|
}
|
|
87
56
|
|
|
57
|
+
/// Handles audio session interruptions.
|
|
58
|
+
/// - Parameter notification: The notification object containing interruption information.
|
|
88
59
|
@objc func handleAudioSessionInterruption(notification: Notification) {
|
|
89
60
|
guard let info = notification.userInfo,
|
|
90
61
|
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
@@ -107,6 +78,8 @@ class AudioStreamManager: NSObject {
|
|
|
107
78
|
}
|
|
108
79
|
}
|
|
109
80
|
|
|
81
|
+
/// Creates a new recording file.
|
|
82
|
+
/// - Returns: The URL of the newly created recording file, or nil if creation failed.
|
|
110
83
|
private func createRecordingFile() -> URL? {
|
|
111
84
|
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
112
85
|
recordingUUID = UUID()
|
|
@@ -120,6 +93,9 @@ class AudioStreamManager: NSObject {
|
|
|
120
93
|
return fileURL
|
|
121
94
|
}
|
|
122
95
|
|
|
96
|
+
/// Creates a WAV header for the given data size.
|
|
97
|
+
/// - Parameter dataSize: The size of the audio data.
|
|
98
|
+
/// - Returns: A Data object containing the WAV header.
|
|
123
99
|
private func createWavHeader(dataSize: Int) -> Data {
|
|
124
100
|
var header = Data()
|
|
125
101
|
|
|
@@ -152,7 +128,8 @@ class AudioStreamManager: NSObject {
|
|
|
152
128
|
return header
|
|
153
129
|
}
|
|
154
130
|
|
|
155
|
-
|
|
131
|
+
/// Gets the current status of the recording.
|
|
132
|
+
/// - Returns: A dictionary containing the recording status information.
|
|
156
133
|
func getStatus() -> [String: Any] {
|
|
157
134
|
// let currentTime = Date()
|
|
158
135
|
// let totalRecordedTime = startTime != nil ? Int(currentTime.timeIntervalSince(startTime!)) - pausedDuration : 0
|
|
@@ -170,7 +147,7 @@ class AudioStreamManager: NSObject {
|
|
|
170
147
|
let durationInMilliseconds = Int(durationInSeconds * 1000)
|
|
171
148
|
|
|
172
149
|
return [
|
|
173
|
-
"
|
|
150
|
+
"durationMs": durationInMilliseconds,
|
|
174
151
|
"isRecording": isRecording,
|
|
175
152
|
"isPaused": isPaused,
|
|
176
153
|
"mimeType": mimeType,
|
|
@@ -180,7 +157,12 @@ class AudioStreamManager: NSObject {
|
|
|
180
157
|
|
|
181
158
|
}
|
|
182
159
|
|
|
183
|
-
|
|
160
|
+
/// Starts a new audio recording with the specified settings and interval.
|
|
161
|
+
/// - Parameters:
|
|
162
|
+
/// - settings: The recording settings to use.
|
|
163
|
+
/// - intervalMilliseconds: The interval in milliseconds for emitting audio data.
|
|
164
|
+
/// - Returns: A StartRecordingResult object if recording starts successfully, or nil otherwise.
|
|
165
|
+
func startRecording(settings: RecordingSettings, intervalMilliseconds: Int) -> StartRecordingResult? {
|
|
184
166
|
guard !isRecording else {
|
|
185
167
|
Logger.debug("Debug: Recording is already in progress.")
|
|
186
168
|
return nil
|
|
@@ -191,11 +173,11 @@ class AudioStreamManager: NSObject {
|
|
|
191
173
|
return nil
|
|
192
174
|
}
|
|
193
175
|
|
|
194
|
-
|
|
176
|
+
var newSettings = settings // Make settings mutable
|
|
195
177
|
|
|
196
178
|
// Determine the commonFormat based on bitDepth
|
|
197
179
|
let commonFormat: AVAudioCommonFormat
|
|
198
|
-
switch
|
|
180
|
+
switch newSettings.bitDepth {
|
|
199
181
|
case 16:
|
|
200
182
|
commonFormat = .pcmFormatInt16
|
|
201
183
|
case 32:
|
|
@@ -203,7 +185,7 @@ class AudioStreamManager: NSObject {
|
|
|
203
185
|
default:
|
|
204
186
|
Logger.debug("Unsupported bit depth. Defaulting to 16-bit PCM")
|
|
205
187
|
commonFormat = .pcmFormatInt16
|
|
206
|
-
|
|
188
|
+
newSettings.bitDepth = 16
|
|
207
189
|
}
|
|
208
190
|
|
|
209
191
|
emissionInterval = max(100.0, Double(intervalMilliseconds)) / 1000.0
|
|
@@ -214,11 +196,32 @@ class AudioStreamManager: NSObject {
|
|
|
214
196
|
let session = AVAudioSession.sharedInstance()
|
|
215
197
|
do {
|
|
216
198
|
Logger.debug("Debug: Configuring audio session with sample rate: \(settings.sampleRate) Hz")
|
|
199
|
+
|
|
200
|
+
// Create an audio format with the desired sample rate
|
|
201
|
+
let desiredFormat = AVAudioFormat(commonFormat: commonFormat, sampleRate: newSettings.sampleRate, channels: UInt32(newSettings.numberOfChannels), interleaved: true)
|
|
202
|
+
|
|
203
|
+
// Check if the input node supports the desired format
|
|
204
|
+
let inputNode = audioEngine.inputNode
|
|
205
|
+
let hardwareFormat = inputNode.inputFormat(forBus: 0)
|
|
206
|
+
if hardwareFormat.sampleRate != newSettings.sampleRate {
|
|
207
|
+
Logger.debug("Debug: Preferred sample rate not supported. Falling back to hardware sample rate \(session.sampleRate).")
|
|
208
|
+
newSettings.sampleRate = session.sampleRate
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try session.setCategory(.playAndRecord)
|
|
212
|
+
try session.setMode(.default)
|
|
217
213
|
try session.setPreferredSampleRate(settings.sampleRate)
|
|
218
214
|
try session.setPreferredIOBufferDuration(1024 / settings.sampleRate)
|
|
219
|
-
try session.setCategory(.playAndRecord)
|
|
220
215
|
try session.setActive(true)
|
|
221
216
|
Logger.debug("Debug: Audio session activated successfully.")
|
|
217
|
+
|
|
218
|
+
let actualSampleRate = session.sampleRate
|
|
219
|
+
if actualSampleRate != newSettings.sampleRate {
|
|
220
|
+
Logger.debug("Debug: Preferred sample rate not set. Falling back to hardware sample rate: \(actualSampleRate) Hz")
|
|
221
|
+
newSettings.sampleRate = actualSampleRate
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
recordingSettings = newSettings // Update the class property with the new settings
|
|
222
225
|
} catch {
|
|
223
226
|
Logger.debug("Error: Failed to set up audio session with preferred settings: \(error.localizedDescription)")
|
|
224
227
|
return nil
|
|
@@ -227,11 +230,21 @@ class AudioStreamManager: NSObject {
|
|
|
227
230
|
NotificationCenter.default.addObserver(self, selector: #selector(handleAudioSessionInterruption), name: AVAudioSession.interruptionNotification, object: nil)
|
|
228
231
|
|
|
229
232
|
// Correct the format to use 16-bit integer (PCM)
|
|
230
|
-
guard let audioFormat = AVAudioFormat(commonFormat: commonFormat, sampleRate:
|
|
233
|
+
guard let audioFormat = AVAudioFormat(commonFormat: commonFormat, sampleRate: newSettings.sampleRate, channels: UInt32(newSettings.numberOfChannels), interleaved: true) else {
|
|
231
234
|
Logger.debug("Error: Failed to create audio format with the specified bit depth.")
|
|
232
235
|
return nil
|
|
233
236
|
}
|
|
234
237
|
|
|
238
|
+
if newSettings.enableProcessing == true {
|
|
239
|
+
// Initialize the AudioProcessor for buffer-based processing
|
|
240
|
+
self.audioProcessor = AudioProcessor(resolve: { result in
|
|
241
|
+
// Handle the result here if needed
|
|
242
|
+
}, reject: { code, message in
|
|
243
|
+
// Handle the rejection here if needed
|
|
244
|
+
})
|
|
245
|
+
Logger.debug("AudioProcessor activated successfully.")
|
|
246
|
+
}
|
|
247
|
+
|
|
235
248
|
audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: audioFormat) { [weak self] (buffer, time) in
|
|
236
249
|
guard let self = self, let fileURL = self.recordingFileURL else {
|
|
237
250
|
Logger.debug("Error: File URL or self is nil during buffer processing.")
|
|
@@ -270,6 +283,10 @@ class AudioStreamManager: NSObject {
|
|
|
270
283
|
}
|
|
271
284
|
}
|
|
272
285
|
|
|
286
|
+
|
|
287
|
+
/// Describes the format of the given audio format.
|
|
288
|
+
/// - Parameter format: The AVAudioFormat object to describe.
|
|
289
|
+
/// - Returns: A string description of the audio format.
|
|
273
290
|
func describeAudioFormat(_ format: AVAudioFormat) -> String {
|
|
274
291
|
let sampleRate = format.sampleRate
|
|
275
292
|
let channelCount = format.channelCount
|
|
@@ -291,13 +308,15 @@ class AudioStreamManager: NSObject {
|
|
|
291
308
|
return "Sample Rate: \(sampleRate), Channels: \(channelCount), Format: \(bitDepth)"
|
|
292
309
|
}
|
|
293
310
|
|
|
311
|
+
/// Stops the current audio recording.
|
|
312
|
+
/// - Returns: A RecordingResult object if the recording stopped successfully, or nil otherwise.
|
|
294
313
|
func stopRecording() -> RecordingResult? {
|
|
295
314
|
audioEngine.stop()
|
|
296
315
|
audioEngine.inputNode.removeTap(onBus: 0)
|
|
297
316
|
isRecording = false
|
|
298
317
|
|
|
299
318
|
guard let fileURL = recordingFileURL, let startTime = startTime, let settings = recordingSettings else {
|
|
300
|
-
|
|
319
|
+
Logger.debug("Recording or file URL is nil.")
|
|
301
320
|
return nil
|
|
302
321
|
}
|
|
303
322
|
|
|
@@ -335,11 +354,93 @@ class AudioStreamManager: NSObject {
|
|
|
335
354
|
|
|
336
355
|
return result
|
|
337
356
|
} catch {
|
|
338
|
-
|
|
357
|
+
Logger.debug("Failed to fetch file attributes: \(error)")
|
|
339
358
|
return nil
|
|
340
359
|
}
|
|
341
360
|
}
|
|
342
361
|
|
|
362
|
+
/// Resamples the audio buffer using vDSP. If it fails, falls back to manual resampling.
|
|
363
|
+
/// - Parameters:
|
|
364
|
+
/// - buffer: The original audio buffer to be resampled.
|
|
365
|
+
/// - originalSampleRate: The sample rate of the original audio buffer.
|
|
366
|
+
/// - targetSampleRate: The desired sample rate to resample to.
|
|
367
|
+
/// - Returns: A new audio buffer resampled to the target sample rate, or nil if resampling fails.
|
|
368
|
+
private func resampleAudioBuffer(_ buffer: AVAudioPCMBuffer, from originalSampleRate: Double, to targetSampleRate: Double) -> AVAudioPCMBuffer? {
|
|
369
|
+
guard let channelData = buffer.floatChannelData else { return nil }
|
|
370
|
+
|
|
371
|
+
let sourceFrameCount = Int(buffer.frameLength)
|
|
372
|
+
let sourceChannels = Int(buffer.format.channelCount)
|
|
373
|
+
|
|
374
|
+
// Calculate the number of frames in the target buffer
|
|
375
|
+
let targetFrameCount = Int(Double(sourceFrameCount) * targetSampleRate / originalSampleRate)
|
|
376
|
+
|
|
377
|
+
// Create a new audio buffer for the resampled data
|
|
378
|
+
guard let targetBuffer = AVAudioPCMBuffer(pcmFormat: buffer.format, frameCapacity: AVAudioFrameCount(targetFrameCount)) else { return nil }
|
|
379
|
+
targetBuffer.frameLength = AVAudioFrameCount(targetFrameCount)
|
|
380
|
+
|
|
381
|
+
let resamplingFactor = Float(targetSampleRate / originalSampleRate) // Factor to resample the audio
|
|
382
|
+
|
|
383
|
+
for channel in 0..<sourceChannels {
|
|
384
|
+
let input = UnsafeBufferPointer(start: channelData[channel], count: sourceFrameCount) // Original channel data
|
|
385
|
+
let output = UnsafeMutableBufferPointer(start: targetBuffer.floatChannelData![channel], count: targetFrameCount) // Buffer for resampled data
|
|
386
|
+
|
|
387
|
+
var y: [Float] = Array(repeating: 0, count: targetFrameCount) // Temporary array for resampled data
|
|
388
|
+
|
|
389
|
+
// Resample using vDSP_vgenp which performs interpolation
|
|
390
|
+
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))
|
|
391
|
+
|
|
392
|
+
for i in 0..<targetFrameCount {
|
|
393
|
+
output[i] = y[i]
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return targetBuffer
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/// Manually resamples the audio buffer using linear interpolation.
|
|
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 manualResampleAudioBuffer(_ 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
|
+
let targetFrameCount = Int(Double(sourceFrameCount) * targetSampleRate / originalSampleRate)
|
|
411
|
+
|
|
412
|
+
guard let targetBuffer = AVAudioPCMBuffer(pcmFormat: buffer.format, frameCapacity: AVAudioFrameCount(targetFrameCount)) else { return nil }
|
|
413
|
+
targetBuffer.frameLength = AVAudioFrameCount(targetFrameCount)
|
|
414
|
+
|
|
415
|
+
let resamplingFactor = Float(targetSampleRate / originalSampleRate)
|
|
416
|
+
|
|
417
|
+
for channel in 0..<sourceChannels {
|
|
418
|
+
let input = UnsafeBufferPointer(start: channelData[channel], count: sourceFrameCount)
|
|
419
|
+
let output = UnsafeMutableBufferPointer(start: targetBuffer.floatChannelData![channel], count: targetFrameCount)
|
|
420
|
+
|
|
421
|
+
var y = Array(repeating: Float(0), count: targetFrameCount)
|
|
422
|
+
for i in 0..<targetFrameCount {
|
|
423
|
+
let index = Float(i) / resamplingFactor
|
|
424
|
+
let low = Int(floor(index))
|
|
425
|
+
let high = min(low + 1, sourceFrameCount - 1)
|
|
426
|
+
let weight = index - Float(low)
|
|
427
|
+
y[i] = (1 - weight) * input[low] + weight * input[high]
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
for i in 0..<targetFrameCount {
|
|
431
|
+
output[i] = y[i]
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return targetBuffer
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
/// Updates the WAV header with the correct file size.
|
|
441
|
+
/// - Parameters:
|
|
442
|
+
/// - fileURL: The URL of the WAV file.
|
|
443
|
+
/// - totalDataSize: The total size of the audio data.
|
|
343
444
|
private func updateWavHeader(fileURL: URL, totalDataSize: Int64) {
|
|
344
445
|
do {
|
|
345
446
|
let fileHandle = try FileHandle(forUpdating: fileURL)
|
|
@@ -360,19 +461,39 @@ class AudioStreamManager: NSObject {
|
|
|
360
461
|
fileHandle.write(Data(dataSizeBytes))
|
|
361
462
|
|
|
362
463
|
} catch let error {
|
|
363
|
-
|
|
464
|
+
Logger.debug("Error updating WAV header: \(error)")
|
|
364
465
|
}
|
|
365
466
|
}
|
|
366
467
|
|
|
468
|
+
/// Processes the audio buffer and writes data to the file. Also handles audio processing if enabled.
|
|
469
|
+
/// - Parameters:
|
|
470
|
+
/// - buffer: The audio buffer to process.
|
|
471
|
+
/// - fileURL: The URL of the file to write the data to.
|
|
367
472
|
private func processAudioBuffer(_ buffer: AVAudioPCMBuffer, fileURL: URL) {
|
|
368
473
|
guard let fileHandle = try? FileHandle(forWritingTo: fileURL) else {
|
|
369
|
-
|
|
474
|
+
Logger.debug("Failed to open file handle for URL: \(fileURL)")
|
|
370
475
|
return
|
|
371
476
|
}
|
|
372
477
|
|
|
373
|
-
let
|
|
478
|
+
let targetSampleRate = recordingSettings?.desiredSampleRate ?? buffer.format.sampleRate
|
|
479
|
+
let finalBuffer: AVAudioPCMBuffer
|
|
480
|
+
|
|
481
|
+
if buffer.format.sampleRate != targetSampleRate {
|
|
482
|
+
// Resample the audio buffer if the target sample rate is different from the input sample rate
|
|
483
|
+
if let resampledBuffer = resampleAudioBuffer(buffer, from: buffer.format.sampleRate, to: targetSampleRate) {
|
|
484
|
+
finalBuffer = resampledBuffer
|
|
485
|
+
} else {
|
|
486
|
+
Logger.debug("Failed to resample audio buffer. Using original buffer.")
|
|
487
|
+
finalBuffer = buffer
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
// Use the original buffer if the sample rates are the same
|
|
491
|
+
finalBuffer = buffer
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
let audioData = finalBuffer.audioBufferList.pointee.mBuffers
|
|
374
495
|
guard let bufferData = audioData.mData else {
|
|
375
|
-
|
|
496
|
+
Logger.debug("Buffer data is nil.")
|
|
376
497
|
return
|
|
377
498
|
}
|
|
378
499
|
var data = Data(bytes: bufferData, count: Int(audioData.mDataByteSize))
|
|
@@ -399,13 +520,44 @@ class AudioStreamManager: NSObject {
|
|
|
399
520
|
if let lastEmissionTime = lastEmissionTime, currentTime.timeIntervalSince(lastEmissionTime) >= emissionInterval {
|
|
400
521
|
if let startTime = startTime {
|
|
401
522
|
let recordingTime = currentTime.timeIntervalSince(startTime)
|
|
402
|
-
//
|
|
403
|
-
|
|
523
|
+
// Copy accumulated data for processing
|
|
524
|
+
let dataToProcess = accumulatedData
|
|
525
|
+
|
|
526
|
+
// Emit the processed audio data
|
|
527
|
+
self.delegate?.audioStreamManager(self, didReceiveAudioData: dataToProcess, recordingTime: recordingTime, totalDataSize: totalDataSize)
|
|
528
|
+
|
|
529
|
+
if recordingSettings?.enableProcessing == true {
|
|
530
|
+
// Process the copied data and emit result
|
|
531
|
+
DispatchQueue.global().async {
|
|
532
|
+
if let processor = self.audioProcessor, let settings = self.recordingSettings {
|
|
533
|
+
Logger.debug("processAudioBuffer with dataToProcess size --> \(dataToProcess.count)")
|
|
534
|
+
|
|
535
|
+
let processingResult = processor.processAudioBuffer(
|
|
536
|
+
data: dataToProcess,
|
|
537
|
+
sampleRate: Float(settings.sampleRate),
|
|
538
|
+
pointsPerSecond: settings.pointsPerSecond ?? 10,
|
|
539
|
+
algorithm: settings.algorithm ?? "rms",
|
|
540
|
+
featureOptions: settings.featureOptions ?? ["rms": true, "zcr": true],
|
|
541
|
+
bitDepth: settings.bitDepth,
|
|
542
|
+
numberOfChannels: settings.numberOfChannels
|
|
543
|
+
)
|
|
544
|
+
Logger.debug("processingResult \(String(describing: processingResult))")
|
|
545
|
+
|
|
546
|
+
DispatchQueue.main.async {
|
|
547
|
+
if let result = processingResult {
|
|
548
|
+
self.delegate?.audioStreamManager(self, didReceiveProcessingResult: result)
|
|
549
|
+
} else {
|
|
550
|
+
Logger.debug("Processing failed or returned nil.")
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
404
557
|
self.lastEmissionTime = currentTime // Update last emission time
|
|
405
558
|
self.lastEmittedSize = totalDataSize
|
|
406
559
|
accumulatedData.removeAll() // Reset accumulated data after emission
|
|
407
560
|
}
|
|
408
561
|
}
|
|
409
562
|
}
|
|
410
|
-
|
|
411
563
|
}
|
|
@@ -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
|
+
}
|