@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.
Files changed (85) hide show
  1. package/README.md +7 -18
  2. package/android/build.gradle +5 -0
  3. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +120 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +34 -4
  5. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +635 -0
  6. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +194 -79
  7. package/android/src/main/java/net/siteed/audiostream/Constants.kt +1 -0
  8. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +48 -2
  9. package/android/src/main/java/net/siteed/audiostream/FFT.kt +44 -0
  10. package/android/src/main/java/net/siteed/audiostream/Features.kt +56 -0
  11. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +12 -0
  12. package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +56 -0
  13. package/app.plugin.js +1 -1
  14. package/build/AudioRecorder.provider.js +1 -1
  15. package/build/AudioRecorder.provider.js.map +1 -1
  16. package/build/ExpoAudioStream.native.d.ts +3 -0
  17. package/build/ExpoAudioStream.native.d.ts.map +1 -0
  18. package/build/ExpoAudioStream.native.js +6 -0
  19. package/build/ExpoAudioStream.native.js.map +1 -0
  20. package/build/ExpoAudioStream.types.d.ts +79 -6
  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 +41 -0
  24. package/build/ExpoAudioStream.web.d.ts.map +1 -0
  25. package/build/ExpoAudioStream.web.js +184 -0
  26. package/build/ExpoAudioStream.web.js.map +1 -0
  27. package/build/ExpoAudioStreamModule.d.ts +2 -2
  28. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  29. package/build/ExpoAudioStreamModule.js +12 -3
  30. package/build/ExpoAudioStreamModule.js.map +1 -1
  31. package/build/WebRecorder.d.ts +47 -0
  32. package/build/WebRecorder.d.ts.map +1 -0
  33. package/build/WebRecorder.js +243 -0
  34. package/build/WebRecorder.js.map +1 -0
  35. package/build/index.d.ts +14 -5
  36. package/build/index.d.ts.map +1 -1
  37. package/build/index.js +106 -7
  38. package/build/index.js.map +1 -1
  39. package/build/inlineAudioWebWorker.d.ts +3 -0
  40. package/build/inlineAudioWebWorker.d.ts.map +1 -0
  41. package/build/inlineAudioWebWorker.js +340 -0
  42. package/build/inlineAudioWebWorker.js.map +1 -0
  43. package/build/useAudioRecording.d.ts +24 -9
  44. package/build/useAudioRecording.d.ts.map +1 -1
  45. package/build/useAudioRecording.js +107 -29
  46. package/build/useAudioRecording.js.map +1 -1
  47. package/build/utils.d.ts +31 -0
  48. package/build/utils.d.ts.map +1 -0
  49. package/build/utils.js +143 -0
  50. package/build/utils.js.map +1 -0
  51. package/expo-module.config.json +13 -4
  52. package/ios/AudioAnalysisData.swift +39 -0
  53. package/ios/AudioProcessingHelpers.swift +59 -0
  54. package/ios/AudioProcessor.swift +317 -0
  55. package/ios/AudioStreamError.swift +7 -0
  56. package/ios/AudioStreamManager.swift +204 -52
  57. package/ios/AudioStreamManagerDelegate.swift +4 -0
  58. package/ios/DataPoint.swift +41 -0
  59. package/ios/ExpoAudioStreamModule.swift +188 -6
  60. package/ios/Features.swift +44 -0
  61. package/ios/RecordingResult.swift +19 -0
  62. package/ios/RecordingSettings.swift +13 -0
  63. package/ios/WaveformExtractor.swift +105 -0
  64. package/package.json +9 -9
  65. package/plugin/tsconfig.json +13 -8
  66. package/publish.sh +8 -0
  67. package/src/AudioRecorder.provider.tsx +1 -1
  68. package/src/ExpoAudioStream.native.ts +6 -0
  69. package/src/ExpoAudioStream.types.ts +97 -11
  70. package/src/ExpoAudioStream.web.ts +228 -0
  71. package/src/ExpoAudioStreamModule.ts +17 -3
  72. package/src/WebRecorder.ts +364 -0
  73. package/src/index.ts +166 -20
  74. package/src/inlineAudioWebWorker.tsx +340 -0
  75. package/src/useAudioRecording.tsx +410 -0
  76. package/src/utils.ts +189 -0
  77. package/build/ExpoAudioStreamModule.web.d.ts +0 -37
  78. package/build/ExpoAudioStreamModule.web.d.ts.map +0 -1
  79. package/build/ExpoAudioStreamModule.web.js +0 -156
  80. package/build/ExpoAudioStreamModule.web.js.map +0 -1
  81. package/docs/demo.gif +0 -0
  82. package/release-it.js +0 -18
  83. package/src/ExpoAudioStreamModule.web.ts +0 -181
  84. package/src/useAudioRecording.ts +0 -268
  85. package/yarn-error.log +0 -7793
@@ -0,0 +1,317 @@
1
+ // AudioProcessor.swift
2
+
3
+ import Foundation
4
+ import Accelerate
5
+ import AVFoundation
6
+ import QuartzCore
7
+
8
+ public class AudioProcessor {
9
+ public private(set) var audioFile: AVAudioFile?
10
+ private var result: (Any) -> Void
11
+ private var reject: (String, String) -> Void
12
+ private var waveformData = Array<Float>()
13
+ private var progress: Float = 0.0
14
+ private var channelCount: Int = 1
15
+ private var currentProgress: Float = 0.0
16
+ private let extractionQueue = DispatchQueue(label: "AudioProcessor", attributes: .concurrent)
17
+ private var _abortExtraction: Bool = false
18
+
19
+ // Add a counter for unique IDs
20
+ private var uniqueIdCounter = 0
21
+
22
+ public var abortExtraction: Bool {
23
+ get { _abortExtraction }
24
+ set { _abortExtraction = newValue }
25
+ }
26
+
27
+ // Initializer for file-based processing
28
+ public init(url: URL, resolve: @escaping (Any) -> Void, reject: @escaping (String, String) -> Void) throws {
29
+ self.audioFile = try AVAudioFile(forReading: url)
30
+ self.result = resolve
31
+ self.reject = reject
32
+ }
33
+
34
+ // Initializer for buffer-based processing
35
+ public init(resolve: @escaping (Any) -> Void, reject: @escaping (String, String) -> Void) {
36
+ self.result = resolve
37
+ self.reject = reject
38
+ }
39
+
40
+
41
+ deinit {
42
+ audioFile = nil
43
+ }
44
+
45
+ /// Error types for AudioProcessor
46
+ public enum AudioProcessorError: Error {
47
+ case fileInitializationFailed(String)
48
+ case bufferCreationFailed
49
+ case audioReadError(String)
50
+ }
51
+
52
+
53
+ /// Extracts and processes audio data from the audio file.
54
+ /// - Parameters:
55
+ /// - numberOfSamples: The number of samples to extract (for waveform).
56
+ /// - offset: The offset to start reading from (in samples).
57
+ /// - length: The length of the audio to read (in samples).
58
+ /// - pointsPerSecond: The number of data points to extract per second (for features).
59
+ /// - algorithm: The algorithm to use for feature extraction.
60
+ /// - featureOptions: The features to extract.
61
+ /// - bitDepth: The bit depth of the audio data.
62
+ /// - numberOfChannels: The number of channels in the audio data.
63
+ /// - Returns: An `AudioAnalysisData` object containing the extracted features.
64
+ public func processAudioData(numberOfSamples: Int?, offset: Int? = 0, length: UInt? = nil, pointsPerSecond: Int?, algorithm: String, featureOptions: [String: Bool], bitDepth: Int, numberOfChannels: Int) -> AudioAnalysisData? {
65
+
66
+ guard let audioFile = audioFile else {
67
+ reject("FILE_NOT_INITIALIZED", "Audio file is not initialized.")
68
+ return nil
69
+ }
70
+
71
+ let totalFrameCount = AVAudioFrameCount(audioFile.length)
72
+ var framesPerBuffer: AVAudioFrameCount
73
+ let actualPointsPerSecond: Int
74
+
75
+ if let numberOfSamples = numberOfSamples {
76
+ framesPerBuffer = totalFrameCount / AVAudioFrameCount(numberOfSamples)
77
+ actualPointsPerSecond = Int(Double(totalFrameCount) / audioFile.fileFormat.sampleRate)
78
+ } else if let pointsPerSecond = pointsPerSecond {
79
+ actualPointsPerSecond = pointsPerSecond
80
+ framesPerBuffer = totalFrameCount / AVAudioFrameCount(actualPointsPerSecond)
81
+ } else {
82
+ // Default behavior: set pointsPerSecond to 1000
83
+ actualPointsPerSecond = 1000
84
+ framesPerBuffer = totalFrameCount / AVAudioFrameCount(actualPointsPerSecond)
85
+ }
86
+
87
+ guard let buffer = AVAudioPCMBuffer(pcmFormat: audioFile.processingFormat, frameCapacity: framesPerBuffer) else {
88
+ reject("BUFFER_CREATION_FAILED", "Failed to create AVAudioPCMBuffer.")
89
+ return nil
90
+ }
91
+
92
+ channelCount = Int(audioFile.processingFormat.channelCount)
93
+ var data = Array(repeating: [Float](repeating: 0, count: Int(framesPerBuffer)), count: channelCount)
94
+
95
+ var startFrame: AVAudioFramePosition = offset == nil ? audioFile.framePosition : Int64(offset! * Int(framesPerBuffer))
96
+ var endFrame: AVAudioFramePosition = length == nil ? audioFile.length : min(audioFile.length, startFrame + Int64(length!))
97
+
98
+ var channelData = [Float]()
99
+ while startFrame < endFrame {
100
+ if abortExtraction {
101
+ audioFile.framePosition = startFrame
102
+ abortExtraction = false
103
+ return nil
104
+ }
105
+
106
+ do {
107
+ audioFile.framePosition = startFrame
108
+ try audioFile.read(into: buffer, frameCount: framesPerBuffer)
109
+ } catch {
110
+ reject("AUDIO_READ_ERROR", "Couldn't read into buffer: \(error.localizedDescription)")
111
+ return nil
112
+ }
113
+
114
+ //TODO: check if we need conversion based on bitDepth here
115
+ guard let floatData = buffer.floatChannelData else {
116
+ reject("BUFFER_DATA_ERROR", "Failed to retrieve float data from buffer.")
117
+ return nil
118
+ }
119
+ for frame in 0..<Int(buffer.frameLength) {
120
+ channelData.append(floatData[0][frame])
121
+ }
122
+
123
+ startFrame += AVAudioFramePosition(framesPerBuffer)
124
+ if startFrame + AVAudioFramePosition(framesPerBuffer) > endFrame {
125
+ framesPerBuffer = AVAudioFrameCount(endFrame - startFrame)
126
+ }
127
+ }
128
+
129
+ return processChannelData(channelData: channelData, sampleRate: Float(audioFile.fileFormat.sampleRate), pointsPerSecond: actualPointsPerSecond, algorithm: algorithm, featureOptions: featureOptions, bitDepth: bitDepth, numberOfChannels: numberOfChannels)
130
+ }
131
+
132
+ /// Processes audio data from a buffer.
133
+ /// - Parameters:
134
+ /// - data: The audio data buffer.
135
+ /// - sampleRate: The sample rate of the audio data.
136
+ /// - pointsPerSecond: The number of data points to extract per second (for features).
137
+ /// - algorithm: The algorithm to use for feature extraction.
138
+ /// - featureOptions: The features to extract.
139
+ /// - bitDepth: The bit depth of the audio data.
140
+ /// - numberOfChannels: The number of channels in the audio data.
141
+ /// - Returns: An `AudioAnalysisData` object containing the extracted features.
142
+ public func processAudioBuffer(data: Data, sampleRate: Float, pointsPerSecond: Int, algorithm: String, featureOptions: [String: Bool], bitDepth: Int, numberOfChannels: Int) -> AudioAnalysisData? {
143
+ guard !data.isEmpty else {
144
+ Logger.debug("Data is empty, rejecting")
145
+ reject("DATA_EMPTY", "The audio data is empty.")
146
+ return nil
147
+ }
148
+
149
+ // Convert Data to Float array based on bit depth
150
+ let floatData: [Float]
151
+ switch bitDepth {
152
+ case 16:
153
+ floatData = data.withUnsafeBytes { bufferPointer in
154
+ let int16Pointer = bufferPointer.bindMemory(to: Int16.self)
155
+ return int16Pointer.map { Float($0) / Float(Int16.max) }
156
+ }
157
+ case 32:
158
+ floatData = data.withUnsafeBytes { bufferPointer in
159
+ let int32Pointer = bufferPointer.bindMemory(to: Int32.self)
160
+ return int32Pointer.map { Float($0) / Float(Int32.max) }
161
+ }
162
+ default:
163
+ Logger.debug("Unsupported bit depth. Rejecting")
164
+ reject("UNSUPPORTED_BIT_DEPTH", "Unsupported bit depth: \(bitDepth)")
165
+ return nil
166
+ }
167
+
168
+ return processChannelData(channelData: floatData, sampleRate: sampleRate, pointsPerSecond: pointsPerSecond, algorithm: algorithm, featureOptions: featureOptions, bitDepth: bitDepth, numberOfChannels: numberOfChannels)
169
+ }
170
+
171
+ /// Processes the given audio channel data to extract features.
172
+ /// - Parameters:
173
+ /// - channelData: The audio channel data to process.
174
+ /// - sampleRate: The sample rate of the audio data.
175
+ /// - pointsPerSecond: The number of data points to extract per second (for features).
176
+ /// - algorithm: The algorithm to use for feature extraction.
177
+ /// - featureOptions: The features to extract.
178
+ /// - bitDepth: The bit depth of the audio data.
179
+ /// - numberOfChannels: The number of channels in the audio data.
180
+ /// - Returns: An `AudioAnalysisData` object containing the extracted features.
181
+ private func processChannelData(channelData: [Float], sampleRate: Float, pointsPerSecond: Int, algorithm: String, featureOptions: [String: Bool], bitDepth: Int, numberOfChannels: Int) -> AudioAnalysisData? {
182
+ Logger.debug("Processing audio data with sample rate: \(sampleRate), points per second: \(pointsPerSecond), algorithm: \(algorithm), bitDepth: \(bitDepth), numberOfChannels: \(numberOfChannels)")
183
+
184
+ let startTime = CACurrentMediaTime() // Start the timer with high precision
185
+
186
+ let length = channelData.count
187
+ let pointInterval = Int(sampleRate) / pointsPerSecond
188
+ var dataPoints = [DataPoint]()
189
+ var minAmplitude: Float = .greatestFiniteMagnitude
190
+ var maxAmplitude: Float = -.greatestFiniteMagnitude
191
+ let durationMs = Float(length) / sampleRate * 1000
192
+
193
+ var sumSquares: Float = 0
194
+ var zeroCrossings = 0
195
+ var prevValue: Float = 0
196
+ var localMinAmplitude: Float = .greatestFiniteMagnitude
197
+ var localMaxAmplitude: Float = -.greatestFiniteMagnitude
198
+ var segmentData = [Float]()
199
+ var currentPosition = 0 // Track the current byte position
200
+
201
+ for i in 0..<length {
202
+ updateSegmentData(channelData: channelData, index: i, sumSquares: &sumSquares, zeroCrossings: &zeroCrossings, prevValue: &prevValue, localMinAmplitude: &localMinAmplitude, localMaxAmplitude: &localMaxAmplitude, segmentData: &segmentData)
203
+
204
+ if (i + 1) % pointInterval == 0 || i == length - 1 {
205
+ let features = computeFeatures(segmentData: segmentData, sampleRate: sampleRate, sumSquares: sumSquares, zeroCrossings: zeroCrossings, segmentLength: (i % pointInterval) + 1, featureOptions: featureOptions)
206
+ let rms = features.rms
207
+ let silent = rms < 0.01
208
+ let dB = featureOptions["dB"] == true ? 20 * log10(rms) : 0
209
+ minAmplitude = min(minAmplitude, rms)
210
+ maxAmplitude = max(maxAmplitude, rms)
211
+
212
+ let segmentSize = segmentData.count
213
+ let segmentDuration = Float(segmentSize) / sampleRate
214
+
215
+ // Calculate start time and end time
216
+ let segmentStartTime = Float(i - segmentSize + 1) / sampleRate
217
+ let segmentEndTime = Float(i + 1) / sampleRate
218
+
219
+ // Calculate start position and end position in bytes
220
+ let bytesPerSample = bitDepth / 8
221
+ let startPosition = currentPosition
222
+ let endPosition = startPosition + (segmentSize * bytesPerSample * numberOfChannels)
223
+
224
+ dataPoints.append(DataPoint(
225
+ id: uniqueIdCounter, // Assign unique ID
226
+ amplitude: algorithm == "peak" ? localMaxAmplitude : rms,
227
+ activeSpeech: nil,
228
+ dB: dB,
229
+ silent: silent,
230
+ features: features,
231
+ startTime: segmentStartTime,
232
+ endTime: segmentEndTime,
233
+ startPosition: startPosition,
234
+ endPosition: endPosition,
235
+ speaker: 0
236
+ ))
237
+ uniqueIdCounter += 1 // Increment the unique ID counter
238
+
239
+ resetSegmentData(&sumSquares, &zeroCrossings, &localMinAmplitude, &localMaxAmplitude, &segmentData)
240
+
241
+ // Update the current byte position
242
+ currentPosition = endPosition
243
+ }
244
+ }
245
+
246
+ let endTime = CACurrentMediaTime() // End the timer with high precision
247
+ let processingTimeMs = Float((endTime - startTime) * 1000)
248
+
249
+ Logger.debug("Processed \(dataPoints.count) data points in \(processingTimeMs) ms")
250
+
251
+ return AudioAnalysisData(
252
+ pointsPerSecond: pointsPerSecond,
253
+ durationMs: durationMs,
254
+ bitDepth: bitDepth,
255
+ numberOfChannels: numberOfChannels,
256
+ sampleRate: sampleRate,
257
+ samples: channelData.count,
258
+ dataPoints: dataPoints,
259
+ amplitudeRange: (min: minAmplitude, max: maxAmplitude),
260
+ speakerChanges: [],
261
+ extractionTimeMs: processingTimeMs
262
+ )
263
+ }
264
+
265
+ private func updateSegmentData(channelData: [Float], index: Int, sumSquares: inout Float, zeroCrossings: inout Int, prevValue: inout Float, localMinAmplitude: inout Float, localMaxAmplitude: inout Float, segmentData: inout [Float]) {
266
+ let value = channelData[index]
267
+ sumSquares += value * value
268
+ if index > 0 && value * prevValue < 0 {
269
+ zeroCrossings += 1
270
+ }
271
+ prevValue = value
272
+
273
+ let absValue = abs(value)
274
+ localMinAmplitude = min(localMinAmplitude, absValue)
275
+ localMaxAmplitude = max(localMaxAmplitude, absValue)
276
+
277
+ segmentData.append(value)
278
+ }
279
+
280
+ private func computeFeatures(segmentData: [Float], sampleRate: Float, sumSquares: Float, zeroCrossings: Int, segmentLength: Int, featureOptions: [String: Bool]) -> Features {
281
+ let rms = sqrt(sumSquares / Float(segmentLength))
282
+ let energy = featureOptions["energy"] == true ? sumSquares : 0
283
+ let zcr = featureOptions["zcr"] == true ? Float(zeroCrossings) / Float(segmentLength) : 0
284
+ let mfcc = featureOptions["mfcc"] == true ? extractMFCC(from: segmentData, sampleRate: sampleRate) : []
285
+ let spectralCentroid = featureOptions["spectralCentroid"] == true ? extractSpectralCentroid(from: segmentData, sampleRate: sampleRate) : 0
286
+ let spectralFlatness = featureOptions["spectralFlatness"] == true ? extractSpectralFlatness(from: segmentData) : 0
287
+ let spectralRollOff = featureOptions["spectralRollOff"] == true ? extractSpectralRollOff(from: segmentData, sampleRate: sampleRate) : 0
288
+ let spectralBandwidth = featureOptions["spectralBandwidth"] == true ? extractSpectralBandwidth(from: segmentData, sampleRate: sampleRate) : 0
289
+ let chromagram = featureOptions["chromagram"] == true ? extractChromagram(from: segmentData, sampleRate: sampleRate) : []
290
+ let tempo = featureOptions["tempo"] == true ? extractTempo(from: segmentData, sampleRate: sampleRate) : 0
291
+ let hnr = featureOptions["hnr"] == true ? extractHNR(from: segmentData) : 0
292
+
293
+ return Features(
294
+ energy: energy,
295
+ mfcc: mfcc,
296
+ rms: rms,
297
+ minAmplitude: 0,
298
+ maxAmplitude: 0,
299
+ zcr: zcr,
300
+ spectralCentroid: spectralCentroid,
301
+ spectralFlatness: spectralFlatness,
302
+ spectralRollOff: spectralRollOff,
303
+ spectralBandwidth: spectralBandwidth,
304
+ chromagram: chromagram,
305
+ tempo: tempo,
306
+ hnr: hnr
307
+ )
308
+ }
309
+
310
+ private func resetSegmentData(_ sumSquares: inout Float, _ zeroCrossings: inout Int, _ localMinAmplitude: inout Float, _ localMaxAmplitude: inout Float, _ segmentData: inout [Float]) {
311
+ sumSquares = 0
312
+ zeroCrossings = 0
313
+ localMinAmplitude = .greatestFiniteMagnitude
314
+ localMaxAmplitude = -.greatestFiniteMagnitude
315
+ segmentData.removeAll()
316
+ }
317
+ }
@@ -0,0 +1,7 @@
1
+ // AudioStreamError.swift
2
+
3
+ enum AudioStreamError: Error {
4
+ case audioSessionSetupFailed(String)
5
+ case fileCreationFailed(URL)
6
+ case audioProcessingError(String)
7
+ }