@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.
- package/.size-limit.json +6 -0
- package/README.md +6 -6
- 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/AudioAnalysis/AudioAnalysis.types.d.ts +76 -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 +4 -0
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -0
- package/build/AudioAnalysis/extractAudioAnalysis.js +101 -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 +14 -0
- package/build/AudioAnalysis/extractWaveform.js.map +1 -0
- package/build/AudioRecorder.provider.d.ts +14 -1
- package/build/AudioRecorder.provider.d.ts.map +1 -1
- package/build/AudioRecorder.provider.js +18 -5
- 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 +35 -20
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/ExpoAudioStream.web.d.ts +42 -0
- package/build/ExpoAudioStream.web.d.ts.map +1 -0
- package/build/ExpoAudioStream.web.js +185 -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 +16 -3
- package/build/ExpoAudioStreamModule.js.map +1 -1
- package/build/WebRecorder.web.d.ts +51 -0
- package/build/WebRecorder.web.d.ts.map +1 -0
- package/build/WebRecorder.web.js +288 -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 +6 -0
- package/build/events.d.ts.map +1 -0
- package/build/events.js +15 -0
- package/build/events.js.map +1 -0
- package/build/index.d.ts +8 -7
- package/build/index.d.ts.map +1 -1
- package/build/index.js +7 -14
- package/build/index.js.map +1 -1
- package/build/logger.d.ts +9 -0
- package/build/logger.d.ts.map +1 -0
- package/build/logger.js +17 -0
- package/build/logger.js.map +1 -0
- package/build/useAudioRecorder.d.ts +37 -0
- package/build/useAudioRecorder.d.ts.map +1 -0
- package/build/useAudioRecorder.js +271 -0
- package/build/useAudioRecorder.js.map +1 -0
- package/build/utils/convertPCMToFloat32.d.ts +11 -0
- package/build/utils/convertPCMToFloat32.d.ts.map +1 -0
- package/build/utils/convertPCMToFloat32.js +41 -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 +25 -0
- package/build/utils/getWavFileInfo.d.ts.map +1 -0
- package/build/utils/getWavFileInfo.js +89 -0
- package/build/utils/getWavFileInfo.js.map +1 -0
- package/build/utils/writeWavHeader.d.ts +9 -0
- package/build/utils/writeWavHeader.d.ts.map +1 -0
- package/build/utils/writeWavHeader.js +41 -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 +303 -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 +243 -0
- package/build/workers/inlineAudioWebWorker.web.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 +243 -54
- package/ios/AudioStreamManagerDelegate.swift +4 -0
- package/ios/DataPoint.swift +41 -0
- package/ios/ExpoAudioStreamModule.swift +198 -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 +13 -12
- package/plugin/tsconfig.json +13 -8
- package/publish.sh +8 -0
- package/src/AudioAnalysis/AudioAnalysis.types.ts +85 -0
- package/src/AudioAnalysis/extractAudioAnalysis.ts +136 -0
- package/src/AudioAnalysis/extractWaveform.ts +25 -0
- package/src/AudioRecorder.provider.tsx +36 -8
- package/src/ExpoAudioStream.native.ts +6 -0
- package/src/ExpoAudioStream.types.ts +50 -25
- package/src/ExpoAudioStream.web.ts +229 -0
- package/src/ExpoAudioStreamModule.ts +22 -3
- package/src/WebRecorder.web.ts +416 -0
- package/src/constants.ts +18 -0
- package/src/events.ts +25 -0
- package/src/index.ts +14 -29
- package/src/logger.ts +26 -0
- package/src/useAudioRecorder.tsx +415 -0
- package/src/utils/convertPCMToFloat32.ts +48 -0
- package/src/utils/encodingToBitDepth.ts +18 -0
- package/src/utils/getWavFileInfo.ts +125 -0
- package/src/utils/writeWavHeader.ts +56 -0
- package/src/workers/InlineFeaturesExtractor.web.tsx +302 -0
- package/src/workers/inlineAudioWebWorker.web.tsx +242 -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/build/useAudioRecording.d.ts +0 -23
- package/build/useAudioRecording.d.ts.map +0 -1
- package/build/useAudioRecording.js +0 -189
- package/build/useAudioRecording.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,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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
178
|
+
var newSettings = settings // Make settings mutable
|
|
195
179
|
|
|
196
180
|
// Determine the commonFormat based on bitDepth
|
|
197
181
|
let commonFormat: AVAudioCommonFormat
|
|
198
|
-
switch
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
511
|
+
Logger.debug("Failed to open file handle for URL: \(fileURL)")
|
|
370
512
|
return
|
|
371
513
|
}
|
|
372
514
|
|
|
373
|
-
let
|
|
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
|
-
|
|
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
|
-
//
|
|
403
|
-
|
|
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
|
+
}
|