@siteed/expo-audio-stream 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +7 -18
  2. package/android/build.gradle +5 -0
  3. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +120 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +34 -4
  5. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +635 -0
  6. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +194 -79
  7. package/android/src/main/java/net/siteed/audiostream/Constants.kt +1 -0
  8. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +48 -2
  9. package/android/src/main/java/net/siteed/audiostream/FFT.kt +44 -0
  10. package/android/src/main/java/net/siteed/audiostream/Features.kt +56 -0
  11. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +12 -0
  12. package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +56 -0
  13. package/app.plugin.js +1 -1
  14. package/build/AudioRecorder.provider.js +1 -1
  15. package/build/AudioRecorder.provider.js.map +1 -1
  16. package/build/ExpoAudioStream.native.d.ts +3 -0
  17. package/build/ExpoAudioStream.native.d.ts.map +1 -0
  18. package/build/ExpoAudioStream.native.js +6 -0
  19. package/build/ExpoAudioStream.native.js.map +1 -0
  20. package/build/ExpoAudioStream.types.d.ts +79 -6
  21. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  22. package/build/ExpoAudioStream.types.js.map +1 -1
  23. package/build/ExpoAudioStream.web.d.ts +41 -0
  24. package/build/ExpoAudioStream.web.d.ts.map +1 -0
  25. package/build/ExpoAudioStream.web.js +184 -0
  26. package/build/ExpoAudioStream.web.js.map +1 -0
  27. package/build/ExpoAudioStreamModule.d.ts +2 -2
  28. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  29. package/build/ExpoAudioStreamModule.js +12 -3
  30. package/build/ExpoAudioStreamModule.js.map +1 -1
  31. package/build/WebRecorder.d.ts +47 -0
  32. package/build/WebRecorder.d.ts.map +1 -0
  33. package/build/WebRecorder.js +243 -0
  34. package/build/WebRecorder.js.map +1 -0
  35. package/build/index.d.ts +14 -5
  36. package/build/index.d.ts.map +1 -1
  37. package/build/index.js +106 -7
  38. package/build/index.js.map +1 -1
  39. package/build/inlineAudioWebWorker.d.ts +3 -0
  40. package/build/inlineAudioWebWorker.d.ts.map +1 -0
  41. package/build/inlineAudioWebWorker.js +340 -0
  42. package/build/inlineAudioWebWorker.js.map +1 -0
  43. package/build/useAudioRecording.d.ts +24 -9
  44. package/build/useAudioRecording.d.ts.map +1 -1
  45. package/build/useAudioRecording.js +107 -29
  46. package/build/useAudioRecording.js.map +1 -1
  47. package/build/utils.d.ts +31 -0
  48. package/build/utils.d.ts.map +1 -0
  49. package/build/utils.js +143 -0
  50. package/build/utils.js.map +1 -0
  51. package/expo-module.config.json +13 -4
  52. package/ios/AudioAnalysisData.swift +39 -0
  53. package/ios/AudioProcessingHelpers.swift +59 -0
  54. package/ios/AudioProcessor.swift +317 -0
  55. package/ios/AudioStreamError.swift +7 -0
  56. package/ios/AudioStreamManager.swift +204 -52
  57. package/ios/AudioStreamManagerDelegate.swift +4 -0
  58. package/ios/DataPoint.swift +41 -0
  59. package/ios/ExpoAudioStreamModule.swift +188 -6
  60. package/ios/Features.swift +44 -0
  61. package/ios/RecordingResult.swift +19 -0
  62. package/ios/RecordingSettings.swift +13 -0
  63. package/ios/WaveformExtractor.swift +105 -0
  64. package/package.json +9 -9
  65. package/plugin/tsconfig.json +13 -8
  66. package/publish.sh +8 -0
  67. package/src/AudioRecorder.provider.tsx +1 -1
  68. package/src/ExpoAudioStream.native.ts +6 -0
  69. package/src/ExpoAudioStream.types.ts +97 -11
  70. package/src/ExpoAudioStream.web.ts +228 -0
  71. package/src/ExpoAudioStreamModule.ts +17 -3
  72. package/src/WebRecorder.ts +364 -0
  73. package/src/index.ts +166 -20
  74. package/src/inlineAudioWebWorker.tsx +340 -0
  75. package/src/useAudioRecording.tsx +410 -0
  76. package/src/utils.ts +189 -0
  77. package/build/ExpoAudioStreamModule.web.d.ts +0 -37
  78. package/build/ExpoAudioStreamModule.web.d.ts.map +0 -1
  79. package/build/ExpoAudioStreamModule.web.js +0 -156
  80. package/build/ExpoAudioStreamModule.web.js.map +0 -1
  81. package/docs/demo.gif +0 -0
  82. package/release-it.js +0 -18
  83. package/src/ExpoAudioStreamModule.web.ts +0 -181
  84. package/src/useAudioRecording.ts +0 -268
  85. package/yarn-error.log +0 -7793
@@ -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,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
- "duration": recordingResult.duration,
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
- let settings = manager.recordingSettings else { return }
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.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\" example/ios",
33
- "open:android": "open -a \"Android Studio\" example/android"
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.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
56
  },
57
57
  "peerDependencies": {
58
58
  "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
@@ -9,7 +9,7 @@ import {
9
9
  const AudioRecorderContext = createContext<UseAudioRecorderState>({
10
10
  isRecording: false,
11
11
  isPaused: false,
12
- duration: 0,
12
+ durationMs: 0,
13
13
  size: 0,
14
14
  // other properties filled on useAudioRecorder
15
15
  } as UseAudioRecorderState);
@@ -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");