@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
|
@@ -2,6 +2,7 @@ import ExpoModulesCore
|
|
|
2
2
|
import AVFoundation
|
|
3
3
|
|
|
4
4
|
let audioDataEvent: String = "AudioData"
|
|
5
|
+
let audioAnalysisEvent: String = "AudioAnalysis"
|
|
5
6
|
|
|
6
7
|
public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
7
8
|
private var streamManager = AudioStreamManager()
|
|
@@ -10,13 +11,118 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
10
11
|
Name("ExpoAudioStream")
|
|
11
12
|
|
|
12
13
|
// Defines event names that the module can send to JavaScript.
|
|
13
|
-
Events(audioDataEvent)
|
|
14
|
+
Events([audioDataEvent, audioAnalysisEvent])
|
|
14
15
|
|
|
15
16
|
OnCreate {
|
|
16
17
|
print("Setting streamManager delegate")
|
|
17
18
|
streamManager.delegate = self
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/// Extracts audio analysis data from an audio file.
|
|
22
|
+
///
|
|
23
|
+
/// - Parameters:
|
|
24
|
+
/// - options: A dictionary containing:
|
|
25
|
+
/// - `fileUri`: The URI of the audio file.
|
|
26
|
+
/// - `pointsPerSecond`: The number of data points to extract per second of audio.
|
|
27
|
+
/// - `algorithm`: The algorithm to use for extraction.
|
|
28
|
+
/// - `features`: A dictionary specifying which features to extract (e.g., `energy`, `mfcc`, `rms`, etc.).
|
|
29
|
+
/// - promise: A promise to resolve with the extracted audio analysis data or reject with an error.
|
|
30
|
+
/// - Returns: Promise to be resolved with audio analysis data.
|
|
31
|
+
AsyncFunction("extractAudioAnalysis") { (options: [String: Any], promise: Promise) in
|
|
32
|
+
guard let fileUri = options["fileUri"] as? String,
|
|
33
|
+
let url = URL(string: fileUri),
|
|
34
|
+
let pointsPerSecond = options["pointsPerSecond"] as? Int,
|
|
35
|
+
let algorithm = options["algorithm"] as? String else {
|
|
36
|
+
promise.reject("INVALID_ARGUMENTS", "Invalid arguments provided")
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let features = options["features"] as? [String: Bool] ?? [:]
|
|
41
|
+
let featureOptions = self.extractFeatureOptions(from: features)
|
|
42
|
+
|
|
43
|
+
DispatchQueue.global().async {
|
|
44
|
+
do {
|
|
45
|
+
let audioFile = try AVAudioFile(forReading: url)
|
|
46
|
+
let bitDepth = audioFile.fileFormat.settings[AVLinearPCMBitDepthKey] as? Int ?? 16
|
|
47
|
+
let numberOfChannels = Int(audioFile.fileFormat.channelCount)
|
|
48
|
+
|
|
49
|
+
let audioProcessor = try AudioProcessor(url: url, resolve: { result in
|
|
50
|
+
promise.resolve(result)
|
|
51
|
+
}, reject: { code, message in
|
|
52
|
+
promise.reject(code, message)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if let result = audioProcessor.processAudioData(numberOfSamples: nil, pointsPerSecond: pointsPerSecond, algorithm: algorithm, featureOptions: featureOptions, bitDepth: bitDepth, numberOfChannels: numberOfChannels) {
|
|
56
|
+
promise.resolve(result.toDictionary())
|
|
57
|
+
} else {
|
|
58
|
+
promise.reject("PROCESSING_ERROR", "Failed to process audio data")
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
promise.reject("PROCESSING_ERROR", "Failed to initialize audio processor: \(error.localizedDescription)")
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Extracts waveform data from an audio file.
|
|
67
|
+
///
|
|
68
|
+
/// - Parameters:
|
|
69
|
+
/// - options: A dictionary containing:
|
|
70
|
+
/// - `fileUri`: The URI of the audio file.
|
|
71
|
+
/// - `numberOfSamples`: The number of samples to extract for the waveform.
|
|
72
|
+
/// - `offset`: The optional offset to start reading from. Defaults to 0 if not provided.
|
|
73
|
+
/// - `length`: The optional length of the audio to read. Defaults to the entire file if not provided.
|
|
74
|
+
/// - promise: A promise to resolve with the extracted waveform data or reject with an error.
|
|
75
|
+
/// - Returns: Promise to be resolved with waveform data.
|
|
76
|
+
AsyncFunction("extractWaveform") { (options: [String: Any], promise: Promise) in
|
|
77
|
+
guard let fileUri = options["fileUri"] as? String,
|
|
78
|
+
let url = URL(string: fileUri),
|
|
79
|
+
let numberOfSamples = options["numberOfSamples"] as? Int else {
|
|
80
|
+
promise.reject("INVALID_ARGUMENTS", "Invalid arguments provided")
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let offset = options["offset"] as? Int ?? 0
|
|
85
|
+
DispatchQueue.global().async {
|
|
86
|
+
do {
|
|
87
|
+
let audioFile = try AVAudioFile(forReading: url)
|
|
88
|
+
let bitDepth = audioFile.fileFormat.settings[AVLinearPCMBitDepthKey] as? Int ?? 16
|
|
89
|
+
let numberOfChannels = Int(audioFile.fileFormat.channelCount)
|
|
90
|
+
|
|
91
|
+
// If length is not provided, default to the entire file length
|
|
92
|
+
let length = options["length"] as? UInt ?? UInt(audioFile.length - AVAudioFramePosition(offset))
|
|
93
|
+
|
|
94
|
+
let audioProcessor = try AudioProcessor(url: url, resolve: { result in
|
|
95
|
+
promise.resolve(result)
|
|
96
|
+
}, reject: { code, message in
|
|
97
|
+
promise.reject(code, message)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if let result = audioProcessor.processAudioData(numberOfSamples: numberOfSamples, offset: offset, length: length, pointsPerSecond: nil, algorithm: "rms", featureOptions: [:], bitDepth: bitDepth, numberOfChannels: numberOfChannels) {
|
|
101
|
+
promise.resolve(result.toDictionary())
|
|
102
|
+
} else {
|
|
103
|
+
promise.reject("EXTRACTION_ERROR", "Failed to extract waveform")
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
promise.reject("EXTRACTION_ERROR", "Failed to initialize waveform extractor: \(error.localizedDescription)")
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
/// Asynchronously starts audio recording with the given settings.
|
|
113
|
+
///
|
|
114
|
+
/// - Parameters:
|
|
115
|
+
/// - options: A dictionary containing:
|
|
116
|
+
/// - `sampleRate`: The sample rate for recording (default is 16000.0).
|
|
117
|
+
/// - `channelConfig`: The number of channels (default is 1 for mono).
|
|
118
|
+
/// - `audioFormat`: The bit depth for recording (default is 16 bits).
|
|
119
|
+
/// - `interval`: The interval in milliseconds at which to emit recording data (default is 1000 ms).
|
|
120
|
+
/// - `enableProcessing`: Boolean to enable/disable audio processing (default is false).
|
|
121
|
+
/// - `pointsPerSecond`: The number of data points to extract per second of audio (default is 20).
|
|
122
|
+
/// - `algorithm`: The algorithm to use for extraction (default is "rms").
|
|
123
|
+
/// - `featureOptions`: A dictionary of feature options to extract (default is empty).
|
|
124
|
+
/// - `maxRecentDataDuration`: The maximum duration of recent data to keep for processing (default is 10.0 seconds).
|
|
125
|
+
/// - promise: A promise to resolve with the recording settings or reject with an error.
|
|
20
126
|
AsyncFunction("startRecording") { (options: [String: Any], promise: Promise) in
|
|
21
127
|
self.checkMicrophonePermission { granted in
|
|
22
128
|
guard granted else {
|
|
@@ -30,7 +136,26 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
30
136
|
let bitDepth = options["audioFormat"] as? Int ?? 16 // 16bits
|
|
31
137
|
let interval = options["interval"] as? Int ?? 1000
|
|
32
138
|
|
|
33
|
-
|
|
139
|
+
// Extract processing options with default values
|
|
140
|
+
let enableProcessing = options["enableProcessing"] as? Bool ?? false
|
|
141
|
+
let pointsPerSecond = options["pointsPerSecond"] as? Int ?? 20
|
|
142
|
+
let algorithm = options["algorithm"] as? String ?? "rms"
|
|
143
|
+
let featureOptions = options["featureOptions"] as? [String: Bool] ?? [:]
|
|
144
|
+
let maxRecentDataDuration = options["maxRecentDataDuration"] as? Double ?? 10.0
|
|
145
|
+
|
|
146
|
+
// Create recording settings
|
|
147
|
+
let settings = RecordingSettings(
|
|
148
|
+
sampleRate: sampleRate,
|
|
149
|
+
desiredSampleRate: sampleRate,
|
|
150
|
+
numberOfChannels: numberOfChannels,
|
|
151
|
+
bitDepth: bitDepth,
|
|
152
|
+
maxRecentDataDuration: enableProcessing ? maxRecentDataDuration : nil,
|
|
153
|
+
enableProcessing: enableProcessing,
|
|
154
|
+
pointsPerSecond: enableProcessing ? pointsPerSecond : nil,
|
|
155
|
+
algorithm: enableProcessing ? algorithm : nil,
|
|
156
|
+
featureOptions: enableProcessing ? featureOptions : nil
|
|
157
|
+
)
|
|
158
|
+
|
|
34
159
|
if let result = self.streamManager.startRecording(settings: settings, intervalMilliseconds: interval) {
|
|
35
160
|
let resultDict: [String: Any] = [
|
|
36
161
|
"fileUri": result.fileUri,
|
|
@@ -46,16 +171,23 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
46
171
|
}
|
|
47
172
|
}
|
|
48
173
|
|
|
174
|
+
/// Retrieves the current status of the audio stream.
|
|
175
|
+
///
|
|
176
|
+
/// - Returns: The current status of the audio stream.Ï
|
|
49
177
|
Function("status") {
|
|
50
178
|
return self.streamManager.getStatus()
|
|
51
179
|
}
|
|
52
180
|
|
|
181
|
+
/// Asynchronously stops audio recording and retrieves the recording result.
|
|
182
|
+
///
|
|
183
|
+
/// - Parameters:
|
|
184
|
+
/// - promise: A promise to resolve with the recording result or reject with an error.
|
|
53
185
|
AsyncFunction("stopRecording") { (promise: Promise) in
|
|
54
186
|
if let recordingResult = self.streamManager.stopRecording() {
|
|
55
187
|
// Convert RecordingResult to a dictionary
|
|
56
188
|
let resultDict: [String: Any] = [
|
|
57
189
|
"fileUri": recordingResult.fileUri,
|
|
58
|
-
"
|
|
190
|
+
"durationMs": recordingResult.duration,
|
|
59
191
|
"size": recordingResult.size,
|
|
60
192
|
"channels": recordingResult.channels,
|
|
61
193
|
"bitDepth": recordingResult.bitDepth,
|
|
@@ -68,19 +200,32 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
68
200
|
}
|
|
69
201
|
}
|
|
70
202
|
|
|
203
|
+
/// Asynchronously lists all audio files stored in the document directory.
|
|
204
|
+
///
|
|
205
|
+
/// - Parameters:
|
|
206
|
+
/// - promise: A promise to resolve with the list of audio file URIs or reject with an error.
|
|
207
|
+
/// - Returns: A promise that resolves with the list of audio file URIs or rejects with an error.
|
|
71
208
|
AsyncFunction("listAudioFiles") { (promise: Promise) in
|
|
72
209
|
let files = listAudioFiles()
|
|
73
210
|
promise.resolve(files)
|
|
74
211
|
}
|
|
75
212
|
|
|
213
|
+
/// Clears all audio files stored in the document directory.
|
|
76
214
|
Function("clearAudioFiles") {
|
|
77
215
|
clearAudioFiles()
|
|
78
216
|
}
|
|
79
217
|
}
|
|
80
218
|
|
|
219
|
+
/// Handles the reception of audio data from the AudioStreamManager.
|
|
220
|
+
///
|
|
221
|
+
/// - Parameters:
|
|
222
|
+
/// - manager: The AudioStreamManager instance.
|
|
223
|
+
/// - data: The received audio data.
|
|
224
|
+
/// - recordingTime: The current recording time.
|
|
225
|
+
/// - totalDataSize: The total size of the received audio data.
|
|
81
226
|
func audioStreamManager(_ manager: AudioStreamManager, didReceiveAudioData data: Data, recordingTime: TimeInterval, totalDataSize: Int64) {
|
|
82
227
|
guard let fileURL = manager.recordingFileURL,
|
|
83
|
-
|
|
228
|
+
let settings = manager.recordingSettings else { return }
|
|
84
229
|
|
|
85
230
|
let encodedData = data.base64EncodedString()
|
|
86
231
|
|
|
@@ -93,7 +238,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
93
238
|
let channels = Double(settings.numberOfChannels)
|
|
94
239
|
let bitDepth = Double(settings.bitDepth)
|
|
95
240
|
let position = Int((Double(manager.lastEmittedSize) / (sampleRate * channels * (bitDepth / 8))) * 1000)
|
|
96
|
-
|
|
241
|
+
|
|
97
242
|
// Construct the event payload similar to Android
|
|
98
243
|
let eventBody: [String: Any] = [
|
|
99
244
|
"fileUri": fileURL.absoluteString,
|
|
@@ -110,6 +255,18 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
110
255
|
sendEvent(audioDataEvent, eventBody)
|
|
111
256
|
}
|
|
112
257
|
|
|
258
|
+
func audioStreamManager(_ manager: AudioStreamManager, didReceiveProcessingResult result: AudioAnalysisData?) {
|
|
259
|
+
// Handle the processed audio data
|
|
260
|
+
// Emit the processing result event to JavaScript
|
|
261
|
+
let resultDict = result?.toDictionary() ?? [:]
|
|
262
|
+
Logger.debug("emitting \(audioAnalysisEvent) event with \(resultDict)")
|
|
263
|
+
sendEvent(audioAnalysisEvent, resultDict)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// Checks microphone permission and calls the completion handler with the result.
|
|
267
|
+
///
|
|
268
|
+
/// - Parameters:
|
|
269
|
+
/// - completion: A completion handler that receives a boolean indicating whether the microphone permission was granted.
|
|
113
270
|
private func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
|
|
114
271
|
switch AVAudioSession.sharedInstance().recordPermission {
|
|
115
272
|
case .granted:
|
|
@@ -127,6 +284,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
127
284
|
}
|
|
128
285
|
}
|
|
129
286
|
|
|
287
|
+
/// Clears all audio files stored in the document directory.
|
|
130
288
|
private func clearAudioFiles() {
|
|
131
289
|
let fileURLs = listAudioFiles() // This now returns full URLs as strings
|
|
132
290
|
fileURLs.forEach { fileURLString in
|
|
@@ -141,9 +299,33 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
141
299
|
print("Invalid URL string: \(fileURLString)")
|
|
142
300
|
}
|
|
143
301
|
}
|
|
144
|
-
|
|
145
302
|
}
|
|
146
303
|
|
|
304
|
+
/// Extracts feature options from the provided options dictionary.
|
|
305
|
+
///
|
|
306
|
+
/// - Parameters:
|
|
307
|
+
/// - options: The options dictionary containing feature flags.
|
|
308
|
+
/// - Returns: A dictionary with feature flags and their boolean values.
|
|
309
|
+
private func extractFeatureOptions(from options: [String: Any]) -> [String: Bool] {
|
|
310
|
+
return [
|
|
311
|
+
"energy": options["energy"] as? Bool ?? false,
|
|
312
|
+
"mfcc": options["mfcc"] as? Bool ?? false,
|
|
313
|
+
"rms": options["rms"] as? Bool ?? false,
|
|
314
|
+
"zcr": options["zcr"] as? Bool ?? false,
|
|
315
|
+
"dB": options["dB"] as? Bool ?? false,
|
|
316
|
+
"spectralCentroid": options["spectralCentroid"] as? Bool ?? false,
|
|
317
|
+
"spectralFlatness": options["spectralFlatness"] as? Bool ?? false,
|
|
318
|
+
"spectralRollOff": options["spectralRollOff"] as? Bool ?? false,
|
|
319
|
+
"spectralBandwidth": options["spectralBandwidth"] as? Bool ?? false,
|
|
320
|
+
"chromagram": options["chromagram"] as? Bool ?? false,
|
|
321
|
+
"tempo": options["tempo"] as? Bool ?? false,
|
|
322
|
+
"hnr": options["hnr"] as? Bool ?? false
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/// Lists all audio files stored in the document directory.
|
|
327
|
+
///
|
|
328
|
+
/// - Returns: An array of file URIs as strings.
|
|
147
329
|
func listAudioFiles() -> [String] {
|
|
148
330
|
guard let documentDirectory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) else {
|
|
149
331
|
print("Failed to access document directory.")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Features.swift
|
|
3
|
+
// ExpoAudioStream
|
|
4
|
+
//
|
|
5
|
+
// Created by Arthur Breton on 23/6/2024.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
public struct Features {
|
|
11
|
+
var energy: Float
|
|
12
|
+
var mfcc: [Float]
|
|
13
|
+
var rms: Float
|
|
14
|
+
var minAmplitude: Float
|
|
15
|
+
var maxAmplitude: Float
|
|
16
|
+
var zcr: Float
|
|
17
|
+
var spectralCentroid: Float
|
|
18
|
+
var spectralFlatness: Float
|
|
19
|
+
var spectralRollOff: Float?
|
|
20
|
+
var spectralBandwidth: Float?
|
|
21
|
+
var chromagram: [Float]?
|
|
22
|
+
var tempo: Float?
|
|
23
|
+
var hnr: Float?
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
extension Features {
|
|
27
|
+
func toDictionary() -> [String: Any] {
|
|
28
|
+
return [
|
|
29
|
+
"energy": energy,
|
|
30
|
+
"mfcc": mfcc,
|
|
31
|
+
"rms": rms,
|
|
32
|
+
"minAmplitude": minAmplitude,
|
|
33
|
+
"maxAmplitude": maxAmplitude,
|
|
34
|
+
"zcr": zcr,
|
|
35
|
+
"spectralCentroid": spectralCentroid,
|
|
36
|
+
"spectralFlatness": spectralFlatness,
|
|
37
|
+
"spectralRollOff": spectralRollOff ?? 0,
|
|
38
|
+
"spectralBandwidth": spectralBandwidth ?? 0,
|
|
39
|
+
"chromagram": chromagram ?? [],
|
|
40
|
+
"tempo": tempo ?? 0,
|
|
41
|
+
"hnr": hnr ?? 0
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// RecordingResult.swift
|
|
2
|
+
|
|
3
|
+
struct RecordingResult {
|
|
4
|
+
var fileUri: String
|
|
5
|
+
var mimeType: String
|
|
6
|
+
var duration: Int64
|
|
7
|
+
var size: Int64
|
|
8
|
+
var channels: Int
|
|
9
|
+
var bitDepth: Int
|
|
10
|
+
var sampleRate: Double
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
struct StartRecordingResult {
|
|
14
|
+
var fileUri: String
|
|
15
|
+
var mimeType: String
|
|
16
|
+
var channels: Int
|
|
17
|
+
var bitDepth: Int
|
|
18
|
+
var sampleRate: Double
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// RecordingSettings.swift
|
|
2
|
+
|
|
3
|
+
struct RecordingSettings {
|
|
4
|
+
var sampleRate: Double
|
|
5
|
+
var desiredSampleRate: Double
|
|
6
|
+
var numberOfChannels: Int = 1
|
|
7
|
+
var bitDepth: Int = 16
|
|
8
|
+
var maxRecentDataDuration: Double? = 10.0 // Default to 10 seconds
|
|
9
|
+
var enableProcessing: Bool = false // Flag to enable/disable processing
|
|
10
|
+
var pointsPerSecond: Int? = 1000 // Default value
|
|
11
|
+
var algorithm: String? = "rms" // Default algorithm
|
|
12
|
+
var featureOptions: [String: Bool]? = ["rms": true, "zcr": true] // Default features
|
|
13
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// WaveformExtractor.swift
|
|
2
|
+
|
|
3
|
+
import Accelerate
|
|
4
|
+
import AVFoundation
|
|
5
|
+
|
|
6
|
+
/// This class is responsible for extracting waveform data from an audio file.
|
|
7
|
+
public class WaveformExtractor {
|
|
8
|
+
public private(set) var audioFile: AVAudioFile?
|
|
9
|
+
private var result: (Any) -> Void
|
|
10
|
+
private var reject: (String, String) -> Void
|
|
11
|
+
private var waveformData = Array<Float>()
|
|
12
|
+
private var progress: Float = 0.0
|
|
13
|
+
private var channelCount: Int = 1
|
|
14
|
+
private var currentProgress: Float = 0.0
|
|
15
|
+
private let extractionQueue = DispatchQueue(label: "WaveformExtractor", attributes: .concurrent)
|
|
16
|
+
private var _abortWaveformExtraction: Bool = false
|
|
17
|
+
|
|
18
|
+
/// Indicates whether the waveform extraction process should be aborted.
|
|
19
|
+
public var abortWaveformExtraction: Bool {
|
|
20
|
+
get { _abortWaveformExtraction }
|
|
21
|
+
set { _abortWaveformExtraction = newValue }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Initializes the waveform extractor with an audio file URL, resolve, and reject callbacks.
|
|
25
|
+
///
|
|
26
|
+
/// - Parameters:
|
|
27
|
+
/// - url: The URL of the audio file to be read.
|
|
28
|
+
/// - resolve: The callback to be called on successful extraction.
|
|
29
|
+
/// - reject: The callback to be called on extraction failure.
|
|
30
|
+
public init(url: URL, resolve: @escaping (Any) -> Void, reject: @escaping (String, String) -> Void) throws {
|
|
31
|
+
self.audioFile = try AVAudioFile(forReading: url)
|
|
32
|
+
self.result = resolve
|
|
33
|
+
self.reject = reject
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
deinit {
|
|
37
|
+
audioFile = nil
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Extracts the waveform data from the audio file.
|
|
41
|
+
///
|
|
42
|
+
/// - Parameters:
|
|
43
|
+
/// - numberOfSamples: The number of samples to extract for the waveform.
|
|
44
|
+
/// - offset: The offset to start reading from.
|
|
45
|
+
/// - length: The length of the audio to read.
|
|
46
|
+
/// - Returns: A 2D array of floats where each sub-array represents waveform data for a specific channel.
|
|
47
|
+
public func extractWaveform(numberOfSamples: Int?, offset: Int? = 0, length: UInt? = nil) -> [[Float]]? {
|
|
48
|
+
guard let audioFile = audioFile else { return nil }
|
|
49
|
+
|
|
50
|
+
let numberOfSamples = max(1, numberOfSamples ?? 100)
|
|
51
|
+
let totalFrameCount = AVAudioFrameCount(audioFile.length)
|
|
52
|
+
var framesPerBuffer = totalFrameCount / AVAudioFrameCount(numberOfSamples)
|
|
53
|
+
|
|
54
|
+
guard let rmsBuffer = AVAudioPCMBuffer(pcmFormat: audioFile.processingFormat, frameCapacity: AVAudioFrameCount(framesPerBuffer)) else { return nil }
|
|
55
|
+
|
|
56
|
+
channelCount = Int(audioFile.processingFormat.channelCount)
|
|
57
|
+
var data = Array(repeating: [Float](repeating: 0, count: numberOfSamples), count: channelCount)
|
|
58
|
+
|
|
59
|
+
var startFrame: AVAudioFramePosition = offset == nil ? audioFile.framePosition : Int64(offset! * Int(framesPerBuffer))
|
|
60
|
+
var end = numberOfSamples
|
|
61
|
+
if let length = length {
|
|
62
|
+
end = Int(length)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for i in 0..<end {
|
|
66
|
+
if abortWaveformExtraction {
|
|
67
|
+
audioFile.framePosition = startFrame
|
|
68
|
+
abortWaveformExtraction = false
|
|
69
|
+
return nil
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
do {
|
|
73
|
+
audioFile.framePosition = startFrame
|
|
74
|
+
try audioFile.read(into: rmsBuffer, frameCount: framesPerBuffer)
|
|
75
|
+
} catch {
|
|
76
|
+
reject("AUDIO_READ_ERROR", "Couldn't read into buffer")
|
|
77
|
+
return nil
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
guard let floatData = rmsBuffer.floatChannelData else { return nil }
|
|
81
|
+
|
|
82
|
+
for channel in 0..<channelCount {
|
|
83
|
+
var rms: Float = 0.0
|
|
84
|
+
vDSP_rmsqv(floatData[channel], 1, &rms, vDSP_Length(rmsBuffer.frameLength))
|
|
85
|
+
data[channel][i] = rms
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
currentProgress += 1
|
|
89
|
+
progress = currentProgress / Float(numberOfSamples)
|
|
90
|
+
|
|
91
|
+
startFrame += AVAudioFramePosition(framesPerBuffer)
|
|
92
|
+
if startFrame + AVAudioFramePosition(framesPerBuffer) > AVAudioFramePosition(totalFrameCount) {
|
|
93
|
+
framesPerBuffer = totalFrameCount - AVAudioFrameCount(startFrame)
|
|
94
|
+
if framesPerBuffer <= 0 { break }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return data
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Cancels the waveform extraction process.
|
|
102
|
+
public func cancel() {
|
|
103
|
+
abortWaveformExtraction = true
|
|
104
|
+
}
|
|
105
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siteed/expo-audio-stream",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "stream audio crossplatform",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"homepage": "https://github.com/deeeed/expo-audio-stream#readme",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
|
-
"url": "git+https://github.com/deeeed/expo-audio-stream.git"
|
|
12
|
+
"url": "git+https://github.com/deeeed/expo-audio-stream.git",
|
|
13
|
+
"directory": "packages/expo-audio-stream"
|
|
13
14
|
},
|
|
14
15
|
"bugs": {
|
|
15
16
|
"url": "https://github.com/deeeed/expo-audio-stream/issues"
|
|
@@ -27,17 +28,15 @@
|
|
|
27
28
|
"test": "expo-module test",
|
|
28
29
|
"prepare": "expo-module prepare",
|
|
29
30
|
"prepublishOnly": "expo-module prepublishOnly",
|
|
30
|
-
"release": "release-it",
|
|
31
31
|
"expo-module": "expo-module",
|
|
32
|
-
"open:ios": "open -a \"Xcode\"
|
|
33
|
-
"open:android": "open -a \"Android Studio\"
|
|
32
|
+
"open:ios": "open -a \"Xcode\" ../../apps/playground/ios",
|
|
33
|
+
"open:android": "open -a \"Android Studio\" ../../apps/playground/android"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"react-native-quick-base64": "^2.1.2"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@expo/config-plugins": "^7.9.1",
|
|
40
|
-
"@release-it/conventional-changelog": "^8.0.1",
|
|
41
40
|
"@types/debug": "^4.1.12",
|
|
42
41
|
"@types/node": "^20.12.7",
|
|
43
42
|
"@types/react": "^18.0.25",
|
|
@@ -46,13 +45,14 @@
|
|
|
46
45
|
"eslint": "^8.56.0",
|
|
47
46
|
"eslint-config-prettier": "^9.1.0",
|
|
48
47
|
"eslint-config-universe": "^12.0.0",
|
|
48
|
+
"eslint-plugin-import": "^2.29.1",
|
|
49
49
|
"eslint-plugin-prettier": "^5.1.3",
|
|
50
50
|
"eslint-plugin-promise": "^6.1.1",
|
|
51
51
|
"eslint-plugin-react": "^7.34.1",
|
|
52
|
-
"expo-module-scripts": "^3.
|
|
53
|
-
"expo-modules-core": "^1.
|
|
52
|
+
"expo-module-scripts": "^3.5.2",
|
|
53
|
+
"expo-modules-core": "^1.12.19",
|
|
54
54
|
"prettier": "^3.2.5",
|
|
55
|
-
"
|
|
55
|
+
"react-native": "^0.74.3"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"expo": "*",
|
package/plugin/tsconfig.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
|
|
2
|
+
"extends": "expo-module-scripts/tsconfig.plugin",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "build",
|
|
5
|
+
"rootDir": "src"
|
|
6
|
+
},
|
|
7
|
+
"include": [
|
|
8
|
+
"./src"
|
|
9
|
+
],
|
|
10
|
+
"exclude": [
|
|
11
|
+
"**/__mocks__/*",
|
|
12
|
+
"**/__tests__/*"
|
|
13
|
+
]
|
|
14
|
+
}
|
package/publish.sh
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// src/ExpoAudioStreamModule.ts
|
|
2
|
+
import { requireNativeModule } from "expo-modules-core";
|
|
3
|
+
|
|
4
|
+
// It loads the native module object from the JSI or falls back to
|
|
5
|
+
// the bridge module (from NativeModulesProxy) if the remote debugger is on.
|
|
6
|
+
export default requireNativeModule("ExpoAudioStream");
|