@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.
Files changed (142) hide show
  1. package/.size-limit.json +6 -0
  2. package/README.md +6 -6
  3. package/android/build.gradle +5 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +120 -0
  5. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +34 -4
  6. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +635 -0
  7. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +194 -79
  8. package/android/src/main/java/net/siteed/audiostream/Constants.kt +1 -0
  9. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +48 -2
  10. package/android/src/main/java/net/siteed/audiostream/FFT.kt +44 -0
  11. package/android/src/main/java/net/siteed/audiostream/Features.kt +56 -0
  12. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +12 -0
  13. package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +56 -0
  14. package/app.plugin.js +1 -1
  15. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +76 -0
  16. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -0
  17. package/build/AudioAnalysis/AudioAnalysis.types.js +3 -0
  18. package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -0
  19. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +4 -0
  20. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -0
  21. package/build/AudioAnalysis/extractAudioAnalysis.js +101 -0
  22. package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -0
  23. package/build/AudioAnalysis/extractWaveform.d.ts +8 -0
  24. package/build/AudioAnalysis/extractWaveform.d.ts.map +1 -0
  25. package/build/AudioAnalysis/extractWaveform.js +14 -0
  26. package/build/AudioAnalysis/extractWaveform.js.map +1 -0
  27. package/build/AudioRecorder.provider.d.ts +14 -1
  28. package/build/AudioRecorder.provider.d.ts.map +1 -1
  29. package/build/AudioRecorder.provider.js +18 -5
  30. package/build/AudioRecorder.provider.js.map +1 -1
  31. package/build/ExpoAudioStream.native.d.ts +3 -0
  32. package/build/ExpoAudioStream.native.d.ts.map +1 -0
  33. package/build/ExpoAudioStream.native.js +6 -0
  34. package/build/ExpoAudioStream.native.js.map +1 -0
  35. package/build/ExpoAudioStream.types.d.ts +35 -20
  36. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  37. package/build/ExpoAudioStream.types.js.map +1 -1
  38. package/build/ExpoAudioStream.web.d.ts +42 -0
  39. package/build/ExpoAudioStream.web.d.ts.map +1 -0
  40. package/build/ExpoAudioStream.web.js +185 -0
  41. package/build/ExpoAudioStream.web.js.map +1 -0
  42. package/build/ExpoAudioStreamModule.d.ts +2 -2
  43. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  44. package/build/ExpoAudioStreamModule.js +16 -3
  45. package/build/ExpoAudioStreamModule.js.map +1 -1
  46. package/build/WebRecorder.web.d.ts +51 -0
  47. package/build/WebRecorder.web.d.ts.map +1 -0
  48. package/build/WebRecorder.web.js +288 -0
  49. package/build/WebRecorder.web.js.map +1 -0
  50. package/build/constants.d.ts +11 -0
  51. package/build/constants.d.ts.map +1 -0
  52. package/build/constants.js +14 -0
  53. package/build/constants.js.map +1 -0
  54. package/build/events.d.ts +6 -0
  55. package/build/events.d.ts.map +1 -0
  56. package/build/events.js +15 -0
  57. package/build/events.js.map +1 -0
  58. package/build/index.d.ts +8 -7
  59. package/build/index.d.ts.map +1 -1
  60. package/build/index.js +7 -14
  61. package/build/index.js.map +1 -1
  62. package/build/logger.d.ts +9 -0
  63. package/build/logger.d.ts.map +1 -0
  64. package/build/logger.js +17 -0
  65. package/build/logger.js.map +1 -0
  66. package/build/useAudioRecorder.d.ts +37 -0
  67. package/build/useAudioRecorder.d.ts.map +1 -0
  68. package/build/useAudioRecorder.js +271 -0
  69. package/build/useAudioRecorder.js.map +1 -0
  70. package/build/utils/convertPCMToFloat32.d.ts +11 -0
  71. package/build/utils/convertPCMToFloat32.d.ts.map +1 -0
  72. package/build/utils/convertPCMToFloat32.js +41 -0
  73. package/build/utils/convertPCMToFloat32.js.map +1 -0
  74. package/build/utils/encodingToBitDepth.d.ts +5 -0
  75. package/build/utils/encodingToBitDepth.d.ts.map +1 -0
  76. package/build/utils/encodingToBitDepth.js +13 -0
  77. package/build/utils/encodingToBitDepth.js.map +1 -0
  78. package/build/utils/getWavFileInfo.d.ts +25 -0
  79. package/build/utils/getWavFileInfo.d.ts.map +1 -0
  80. package/build/utils/getWavFileInfo.js +89 -0
  81. package/build/utils/getWavFileInfo.js.map +1 -0
  82. package/build/utils/writeWavHeader.d.ts +9 -0
  83. package/build/utils/writeWavHeader.d.ts.map +1 -0
  84. package/build/utils/writeWavHeader.js +41 -0
  85. package/build/utils/writeWavHeader.js.map +1 -0
  86. package/build/workers/InlineFeaturesExtractor.web.d.ts +2 -0
  87. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -0
  88. package/build/workers/InlineFeaturesExtractor.web.js +303 -0
  89. package/build/workers/InlineFeaturesExtractor.web.js.map +1 -0
  90. package/build/workers/inlineAudioWebWorker.web.d.ts +2 -0
  91. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -0
  92. package/build/workers/inlineAudioWebWorker.web.js +243 -0
  93. package/build/workers/inlineAudioWebWorker.web.js.map +1 -0
  94. package/expo-module.config.json +13 -4
  95. package/ios/AudioAnalysisData.swift +39 -0
  96. package/ios/AudioProcessingHelpers.swift +59 -0
  97. package/ios/AudioProcessor.swift +317 -0
  98. package/ios/AudioStreamError.swift +7 -0
  99. package/ios/AudioStreamManager.swift +243 -54
  100. package/ios/AudioStreamManagerDelegate.swift +4 -0
  101. package/ios/DataPoint.swift +41 -0
  102. package/ios/ExpoAudioStreamModule.swift +198 -6
  103. package/ios/Features.swift +44 -0
  104. package/ios/RecordingResult.swift +19 -0
  105. package/ios/RecordingSettings.swift +13 -0
  106. package/ios/WaveformExtractor.swift +105 -0
  107. package/package.json +13 -12
  108. package/plugin/tsconfig.json +13 -8
  109. package/publish.sh +8 -0
  110. package/src/AudioAnalysis/AudioAnalysis.types.ts +85 -0
  111. package/src/AudioAnalysis/extractAudioAnalysis.ts +136 -0
  112. package/src/AudioAnalysis/extractWaveform.ts +25 -0
  113. package/src/AudioRecorder.provider.tsx +36 -8
  114. package/src/ExpoAudioStream.native.ts +6 -0
  115. package/src/ExpoAudioStream.types.ts +50 -25
  116. package/src/ExpoAudioStream.web.ts +229 -0
  117. package/src/ExpoAudioStreamModule.ts +22 -3
  118. package/src/WebRecorder.web.ts +416 -0
  119. package/src/constants.ts +18 -0
  120. package/src/events.ts +25 -0
  121. package/src/index.ts +14 -29
  122. package/src/logger.ts +26 -0
  123. package/src/useAudioRecorder.tsx +415 -0
  124. package/src/utils/convertPCMToFloat32.ts +48 -0
  125. package/src/utils/encodingToBitDepth.ts +18 -0
  126. package/src/utils/getWavFileInfo.ts +125 -0
  127. package/src/utils/writeWavHeader.ts +56 -0
  128. package/src/workers/InlineFeaturesExtractor.web.tsx +302 -0
  129. package/src/workers/inlineAudioWebWorker.web.tsx +242 -0
  130. package/build/ExpoAudioStreamModule.web.d.ts +0 -37
  131. package/build/ExpoAudioStreamModule.web.d.ts.map +0 -1
  132. package/build/ExpoAudioStreamModule.web.js +0 -156
  133. package/build/ExpoAudioStreamModule.web.js.map +0 -1
  134. package/build/useAudioRecording.d.ts +0 -23
  135. package/build/useAudioRecording.d.ts.map +0 -1
  136. package/build/useAudioRecording.js +0 -189
  137. package/build/useAudioRecording.js.map +0 -1
  138. package/docs/demo.gif +0 -0
  139. package/release-it.js +0 -18
  140. package/src/ExpoAudioStreamModule.web.ts +0 -181
  141. package/src/useAudioRecording.ts +0 -268
  142. 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
- let settings = RecordingSettings(sampleRate: sampleRate, numberOfChannels: numberOfChannels, bitDepth: bitDepth)
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
- "duration": recordingResult.duration,
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
- let settings = manager.recordingSettings else { return }
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.1",
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\" example/ios",
33
- "open:android": "open -a \"Android Studio\" example/android"
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
- "@release-it/conventional-changelog": "^8.0.1",
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.4.2",
53
- "expo-modules-core": "^1.11.12",
52
+ "expo-module-scripts": "^3.5.2",
53
+ "expo-modules-core": "^1.12.19",
54
54
  "prettier": "^3.2.5",
55
- "release-it": "^17.2.0"
55
+ "react-native": "^0.74.3",
56
+ "size-limit": "^11.1.4"
56
57
  },
57
58
  "peerDependencies": {
58
59
  "expo": "*",
@@ -1,9 +1,14 @@
1
1
  {
2
- "extends": "expo-module-scripts/tsconfig.plugin",
3
- "compilerOptions": {
4
- "outDir": "build",
5
- "rootDir": "src"
6
- },
7
- "include": ["./src"],
8
- "exclude": ["**/__mocks__/*", "**/__tests__/*"]
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,8 @@
1
+ #!/bin/bash
2
+ set -e
3
+ # Bump version
4
+ yarn version patch
5
+ version=$(node -p "require('./package.json').version")
6
+ git add .
7
+ git commit -m 'feat: bump version to $version'
8
+ yarn clean && yarn prepare && npm publish
@@ -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
+ }