@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
|
@@ -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,33 @@ 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
|
+
/// Pauses audio recording.
|
|
182
|
+
Function("pauseRecording") {
|
|
183
|
+
self.streamManager.pauseRecording()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// Resumes audio recording.
|
|
187
|
+
Function("resumeRecording") {
|
|
188
|
+
self.streamManager.resumeRecording()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Asynchronously stops audio recording and retrieves the recording result.
|
|
192
|
+
///
|
|
193
|
+
/// - Parameters:
|
|
194
|
+
/// - promise: A promise to resolve with the recording result or reject with an error.
|
|
53
195
|
AsyncFunction("stopRecording") { (promise: Promise) in
|
|
54
196
|
if let recordingResult = self.streamManager.stopRecording() {
|
|
55
197
|
// Convert RecordingResult to a dictionary
|
|
56
198
|
let resultDict: [String: Any] = [
|
|
57
199
|
"fileUri": recordingResult.fileUri,
|
|
58
|
-
"
|
|
200
|
+
"durationMs": recordingResult.duration,
|
|
59
201
|
"size": recordingResult.size,
|
|
60
202
|
"channels": recordingResult.channels,
|
|
61
203
|
"bitDepth": recordingResult.bitDepth,
|
|
@@ -68,19 +210,32 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
68
210
|
}
|
|
69
211
|
}
|
|
70
212
|
|
|
213
|
+
/// Asynchronously lists all audio files stored in the document directory.
|
|
214
|
+
///
|
|
215
|
+
/// - Parameters:
|
|
216
|
+
/// - promise: A promise to resolve with the list of audio file URIs or reject with an error.
|
|
217
|
+
/// - Returns: A promise that resolves with the list of audio file URIs or rejects with an error.
|
|
71
218
|
AsyncFunction("listAudioFiles") { (promise: Promise) in
|
|
72
219
|
let files = listAudioFiles()
|
|
73
220
|
promise.resolve(files)
|
|
74
221
|
}
|
|
75
222
|
|
|
223
|
+
/// Clears all audio files stored in the document directory.
|
|
76
224
|
Function("clearAudioFiles") {
|
|
77
225
|
clearAudioFiles()
|
|
78
226
|
}
|
|
79
227
|
}
|
|
80
228
|
|
|
229
|
+
/// Handles the reception of audio data from the AudioStreamManager.
|
|
230
|
+
///
|
|
231
|
+
/// - Parameters:
|
|
232
|
+
/// - manager: The AudioStreamManager instance.
|
|
233
|
+
/// - data: The received audio data.
|
|
234
|
+
/// - recordingTime: The current recording time.
|
|
235
|
+
/// - totalDataSize: The total size of the received audio data.
|
|
81
236
|
func audioStreamManager(_ manager: AudioStreamManager, didReceiveAudioData data: Data, recordingTime: TimeInterval, totalDataSize: Int64) {
|
|
82
237
|
guard let fileURL = manager.recordingFileURL,
|
|
83
|
-
|
|
238
|
+
let settings = manager.recordingSettings else { return }
|
|
84
239
|
|
|
85
240
|
let encodedData = data.base64EncodedString()
|
|
86
241
|
|
|
@@ -93,7 +248,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
93
248
|
let channels = Double(settings.numberOfChannels)
|
|
94
249
|
let bitDepth = Double(settings.bitDepth)
|
|
95
250
|
let position = Int((Double(manager.lastEmittedSize) / (sampleRate * channels * (bitDepth / 8))) * 1000)
|
|
96
|
-
|
|
251
|
+
|
|
97
252
|
// Construct the event payload similar to Android
|
|
98
253
|
let eventBody: [String: Any] = [
|
|
99
254
|
"fileUri": fileURL.absoluteString,
|
|
@@ -110,6 +265,18 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
110
265
|
sendEvent(audioDataEvent, eventBody)
|
|
111
266
|
}
|
|
112
267
|
|
|
268
|
+
func audioStreamManager(_ manager: AudioStreamManager, didReceiveProcessingResult result: AudioAnalysisData?) {
|
|
269
|
+
// Handle the processed audio data
|
|
270
|
+
// Emit the processing result event to JavaScript
|
|
271
|
+
let resultDict = result?.toDictionary() ?? [:]
|
|
272
|
+
Logger.debug("emitting \(audioAnalysisEvent) event with \(resultDict)")
|
|
273
|
+
sendEvent(audioAnalysisEvent, resultDict)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// Checks microphone permission and calls the completion handler with the result.
|
|
277
|
+
///
|
|
278
|
+
/// - Parameters:
|
|
279
|
+
/// - completion: A completion handler that receives a boolean indicating whether the microphone permission was granted.
|
|
113
280
|
private func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
|
|
114
281
|
switch AVAudioSession.sharedInstance().recordPermission {
|
|
115
282
|
case .granted:
|
|
@@ -127,6 +294,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
127
294
|
}
|
|
128
295
|
}
|
|
129
296
|
|
|
297
|
+
/// Clears all audio files stored in the document directory.
|
|
130
298
|
private func clearAudioFiles() {
|
|
131
299
|
let fileURLs = listAudioFiles() // This now returns full URLs as strings
|
|
132
300
|
fileURLs.forEach { fileURLString in
|
|
@@ -141,9 +309,33 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
141
309
|
print("Invalid URL string: \(fileURLString)")
|
|
142
310
|
}
|
|
143
311
|
}
|
|
144
|
-
|
|
145
312
|
}
|
|
146
313
|
|
|
314
|
+
/// Extracts feature options from the provided options dictionary.
|
|
315
|
+
///
|
|
316
|
+
/// - Parameters:
|
|
317
|
+
/// - options: The options dictionary containing feature flags.
|
|
318
|
+
/// - Returns: A dictionary with feature flags and their boolean values.
|
|
319
|
+
private func extractFeatureOptions(from options: [String: Any]) -> [String: Bool] {
|
|
320
|
+
return [
|
|
321
|
+
"energy": options["energy"] as? Bool ?? false,
|
|
322
|
+
"mfcc": options["mfcc"] as? Bool ?? false,
|
|
323
|
+
"rms": options["rms"] as? Bool ?? false,
|
|
324
|
+
"zcr": options["zcr"] as? Bool ?? false,
|
|
325
|
+
"dB": options["dB"] as? Bool ?? false,
|
|
326
|
+
"spectralCentroid": options["spectralCentroid"] as? Bool ?? false,
|
|
327
|
+
"spectralFlatness": options["spectralFlatness"] as? Bool ?? false,
|
|
328
|
+
"spectralRollOff": options["spectralRollOff"] as? Bool ?? false,
|
|
329
|
+
"spectralBandwidth": options["spectralBandwidth"] as? Bool ?? false,
|
|
330
|
+
"chromagram": options["chromagram"] as? Bool ?? false,
|
|
331
|
+
"tempo": options["tempo"] as? Bool ?? false,
|
|
332
|
+
"hnr": options["hnr"] as? Bool ?? false
|
|
333
|
+
]
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/// Lists all audio files stored in the document directory.
|
|
337
|
+
///
|
|
338
|
+
/// - Returns: An array of file URIs as strings.
|
|
147
339
|
func listAudioFiles() -> [String] {
|
|
148
340
|
guard let documentDirectory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) else {
|
|
149
341
|
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.3",
|
|
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,32 +28,32 @@
|
|
|
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\"
|
|
34
|
-
|
|
35
|
-
"dependencies": {
|
|
36
|
-
"react-native-quick-base64": "^2.1.2"
|
|
32
|
+
"open:ios": "open -a \"Xcode\" ../../apps/playground/ios",
|
|
33
|
+
"open:android": "open -a \"Android Studio\" ../../apps/playground/android",
|
|
34
|
+
"size": "bundle-size && size-limit"
|
|
37
35
|
},
|
|
38
36
|
"devDependencies": {
|
|
39
37
|
"@expo/config-plugins": "^7.9.1",
|
|
40
|
-
"@
|
|
38
|
+
"@size-limit/preset-big-lib": "^11.1.4",
|
|
41
39
|
"@types/debug": "^4.1.12",
|
|
42
40
|
"@types/node": "^20.12.7",
|
|
43
41
|
"@types/react": "^18.0.25",
|
|
44
42
|
"@typescript-eslint/eslint-plugin": "^7.7.0",
|
|
45
43
|
"@typescript-eslint/parser": "^7.7.0",
|
|
44
|
+
"bundle-size": "^1.1.5",
|
|
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
|
+
"size-limit": "^11.1.4"
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|
|
58
59
|
"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,85 @@
|
|
|
1
|
+
// packages/expo-audio-stream/src/AudioAnalysis/AudioAnalysis.types.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents various audio features extracted from an audio signal.
|
|
5
|
+
*/
|
|
6
|
+
export interface AudioFeatures {
|
|
7
|
+
energy: number; // The infinite integral of the squared signal, representing the overall energy of the audio.
|
|
8
|
+
mfcc: number[]; // Mel-frequency cepstral coefficients, describing the short-term power spectrum of a sound.
|
|
9
|
+
rms: number; // Root mean square value, indicating the amplitude of the audio signal.
|
|
10
|
+
minAmplitude: number; // Minimum amplitude value in the audio signal.
|
|
11
|
+
maxAmplitude: number; // Maximum amplitude value in the audio signal.
|
|
12
|
+
zcr: number; // Zero-crossing rate, indicating the rate at which the signal changes sign.
|
|
13
|
+
spectralCentroid: number; // The center of mass of the spectrum, indicating the brightness of the sound.
|
|
14
|
+
spectralFlatness: number; // Measure of the flatness of the spectrum, indicating how noise-like the signal is.
|
|
15
|
+
spectralRolloff: number; // The frequency below which a specified percentage (usually 85%) of the total spectral energy lies.
|
|
16
|
+
spectralBandwidth: number; // The width of the spectrum, indicating the range of frequencies present.
|
|
17
|
+
chromagram: number[]; // Chromagram, representing the 12 different pitch classes of the audio.
|
|
18
|
+
tempo: number; // Estimated tempo of the audio signal, measured in beats per minute (BPM).
|
|
19
|
+
hnr: number; // Harmonics-to-noise ratio, indicating the proportion of harmonics to noise in the audio signal.
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Options to specify which audio features to extract.
|
|
24
|
+
*/
|
|
25
|
+
export interface AudioFeaturesOptions {
|
|
26
|
+
energy?: boolean;
|
|
27
|
+
mfcc?: boolean;
|
|
28
|
+
rms?: boolean;
|
|
29
|
+
zcr?: boolean;
|
|
30
|
+
spectralCentroid?: boolean;
|
|
31
|
+
spectralFlatness?: boolean;
|
|
32
|
+
spectralRolloff?: boolean;
|
|
33
|
+
spectralBandwidth?: boolean;
|
|
34
|
+
chromagram?: boolean;
|
|
35
|
+
tempo?: boolean;
|
|
36
|
+
hnr?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Represents a single data point in the audio analysis.
|
|
41
|
+
*/
|
|
42
|
+
export interface DataPoint {
|
|
43
|
+
id: number;
|
|
44
|
+
amplitude: number;
|
|
45
|
+
activeSpeech?: boolean;
|
|
46
|
+
dB?: number;
|
|
47
|
+
silent?: boolean;
|
|
48
|
+
features?: AudioFeatures;
|
|
49
|
+
startTime?: number;
|
|
50
|
+
endTime?: number;
|
|
51
|
+
// start / end position in bytes
|
|
52
|
+
startPosition?: number;
|
|
53
|
+
endPosition?: number;
|
|
54
|
+
// number of audio samples for this point (samples size depends on bit depth)
|
|
55
|
+
samples?: number;
|
|
56
|
+
// TODO: speaker detection
|
|
57
|
+
speaker?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Represents the complete data from the audio analysis.
|
|
62
|
+
*/
|
|
63
|
+
export interface AudioAnalysisData {
|
|
64
|
+
pointsPerSecond: number; // How many consolidated value per second
|
|
65
|
+
durationMs: number; // Duration of the audio in milliseconds
|
|
66
|
+
bitDepth: number; // Bit depth of the audio
|
|
67
|
+
samples: number; // Size of the audio in bytes
|
|
68
|
+
numberOfChannels: number; // Number of audio channels
|
|
69
|
+
sampleRate: number; // Sample rate of the audio
|
|
70
|
+
dataPoints: DataPoint[]; // Array of data points from the analysis.
|
|
71
|
+
amplitudeRange: {
|
|
72
|
+
min: number;
|
|
73
|
+
max: number;
|
|
74
|
+
};
|
|
75
|
+
// TODO: speaker detection
|
|
76
|
+
speakerChanges?: {
|
|
77
|
+
timestamp: number; // Timestamp of the speaker change in milliseconds.
|
|
78
|
+
speaker: number; // Speaker identifier.
|
|
79
|
+
}[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface AudioAnalysisEventPayload {
|
|
83
|
+
analysis: AudioAnalysisData;
|
|
84
|
+
visualizationDuration: number;
|
|
85
|
+
}
|